diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cce8a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +backend/node_modules/ +frontend/node_modules/ +backend/uploads/ diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..9b22c56 --- /dev/null +++ b/backend/.env @@ -0,0 +1,6 @@ +PORT=3000 +DB_HOST=127.0.0.1 +DB_USER=root +DB_PASSWORD=05WXZcu +DB_DATABASE=game_categories +JWT_SECRET=8f4c9d6e2a7b1f5k9m3n8p4q7r2t5v8x0z \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..e544fdd --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,31 @@ +const jwt = require('jsonwebtoken'); +const db = require('../utils/db'); + +const auth = async (req, res, next) => { + try { + const token = req.header('Authorization').replace('Bearer ', ''); + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const [admins] = await db.query( + 'SELECT a.*, GROUP_CONCAT(ap.permission) as permissions FROM admins a ' + + 'LEFT JOIN admin_permissions ap ON a.id = ap.admin_id ' + + 'WHERE a.id = ? AND a.status = "active" ' + + 'GROUP BY a.id', + [decoded.id] + ); + + if (!admins[0]) { + throw new Error(); + } + + req.admin = { + ...admins[0], + permissions: admins[0].permissions ? admins[0].permissions.split(',') : [] + }; + next(); + } catch (error) { + res.status(401).json({ message: '请先登录' }); + } +}; + +module.exports = auth; \ No newline at end of file diff --git a/backend/middleware/checkPermission.js b/backend/middleware/checkPermission.js new file mode 100644 index 0000000..c680797 --- /dev/null +++ b/backend/middleware/checkPermission.js @@ -0,0 +1,34 @@ +const checkPermission = (requiredPermission) => { + return (req, res, next) => { + if (!req.admin) { + return res.status(401).json({ message: '请先登录' }); + } + + if (req.admin.role === 'superadmin') { + return next(); // 超级管理员拥有所有权限 + } + + // 添加默认权限映射 + const rolePermissions = { + admin: [ + 'game:manage', + 'category:manage', + 'media:manage', + 'message:manage' // 添加消息管理权限 + ], + editor: [ + 'game:manage', + 'media:manage' + ] + }; + + if (!req.admin.permissions.includes(requiredPermission) && + !(rolePermissions[req.admin.role] || []).includes(requiredPermission)) { + return res.status(403).json({ message: '没有操作权限' }); + } + + next(); + }; +}; + +module.exports = checkPermission; \ No newline at end of file diff --git a/backend/models/admin.js b/backend/models/admin.js new file mode 100644 index 0000000..22eb9ad --- /dev/null +++ b/backend/models/admin.js @@ -0,0 +1,89 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); + +const adminSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + email: { + type: String, + required: true, + unique: true + }, + role: { + type: String, + enum: ['superadmin', 'admin', 'editor'], + default: 'editor' + }, + permissions: [{ + type: String, + enum: [ + 'user:manage', // 用户管理权限 + 'game:manage', // 游戏管理权限 + 'category:manage', // 分类管理权限 + 'media:manage', // 媒体管理权限 + 'system:manage' // 系统管理权限 + ] + }], + status: { + type: String, + enum: ['active', 'inactive'], + default: 'active' + }, + lastLogin: Date, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Admin' + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +// 密码加密 +adminSchema.pre('save', async function(next) { + if (this.isModified('password')) { + this.password = await bcrypt.hash(this.password, 10); + } + next(); +}); + +// 根据角色设置默认权限 +adminSchema.pre('save', function(next) { + if (this.isNew || this.isModified('role')) { + switch (this.role) { + case 'superadmin': + this.permissions = [ + 'user:manage', + 'game:manage', + 'category:manage', + 'media:manage', + 'system:manage' + ]; + break; + case 'admin': + this.permissions = [ + 'game:manage', + 'category:manage', + 'media:manage' + ]; + break; + case 'editor': + this.permissions = [ + 'game:manage', + 'media:manage' + ]; + break; + } + } + next(); +}); + +module.exports = mongoose.model('Admin', adminSchema); \ No newline at end of file diff --git a/backend/models/media.js b/backend/models/media.js new file mode 100644 index 0000000..270089c --- /dev/null +++ b/backend/models/media.js @@ -0,0 +1,34 @@ +const mongoose = require('mongoose'); + +const mediaSchema = new mongoose.Schema({ + filename: { + type: String, + required: true + }, + originalname: String, + mimetype: String, + size: Number, + url: { + type: String, + required: true + }, + type: { + type: String, + enum: ['image', 'video', 'audio'], + required: true + }, + gameId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Game' + }, + uploadedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Admin' + }, + uploadedAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('Media', mediaSchema); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..bf15280 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "game-category-backend", + "version": "1.0.0", + "main": "server.js", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.17.1", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.5-lts.1", + "mysql2": "^2.3.3", + "ws": "^8.18.0" + }, + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test-db": "node scripts/test-db.js" + }, + "devDependencies": { + "nodemon": "^2.0.12" + } +} diff --git a/backend/routes/admin.js b/backend/routes/admin.js new file mode 100644 index 0000000..dfc9bca --- /dev/null +++ b/backend/routes/admin.js @@ -0,0 +1,185 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../utils/db'); +const auth = require('../middleware/auth'); +const checkPermission = require('../middleware/checkPermission'); +const bcrypt = require('bcryptjs'); + +// 获取用户列表 +router.get('/users', auth, checkPermission('user:manage'), async (req, res) => { + try { + const [users] = await db.query(` + SELECT a.*, + GROUP_CONCAT(ap.permission) as permissions + FROM admins a + LEFT JOIN admin_permissions ap ON a.id = ap.admin_id + GROUP BY a.id + `); + + users.forEach(user => { + user.permissions = user.permissions ? user.permissions.split(',') : []; + delete user.password; + }); + + res.json(users); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 添加用户 +router.post('/users', auth, checkPermission('user:manage'), async (req, res) => { + const connection = await db.getConnection(); + await connection.beginTransaction(); + + try { + const { username, email, password, role, status, permissions } = req.body; + + // 验证必填字段 + if (!username || !email || !password) { + return res.status(400).json({ message: '用户名、邮箱和密码为必填项' }); + } + + // 检查用户名是否已存在 + const [existingUsers] = await connection.query( + 'SELECT id FROM admins WHERE username = ?', + [username] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ message: '用户名已存在' }); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 插入用户记录 + const [result] = await connection.query( + 'INSERT INTO admins (username, email, password, role, status) VALUES (?, ?, ?, ?, ?)', + [username, email, hashedPassword, role || 'editor', status || 'active'] + ); + + // 添加权限 + if (permissions && permissions.length > 0) { + const permissionValues = permissions.map(permission => [result.insertId, permission]); + await connection.query( + 'INSERT INTO admin_permissions (admin_id, permission) VALUES ?', + [permissionValues] + ); + } + + await connection.commit(); + res.status(201).json({ message: '用户创建成功' }); + } catch (error) { + await connection.rollback(); + res.status(500).json({ message: error.message }); + } finally { + connection.release(); + } +}); + +// 更新用户 +router.put('/users/:id', auth, checkPermission('user:manage'), async (req, res) => { + const connection = await db.getConnection(); + await connection.beginTransaction(); + + try { + const { username, email, password, role, status, permissions } = req.body; + const userId = req.params.id; + + // 检查用户是否存在 + const [existingUser] = await connection.query( + 'SELECT role FROM admins WHERE id = ?', + [userId] + ); + + if (!existingUser.length) { + return res.status(404).json({ message: '用户不存在' }); + } + + // 不允许非超级管理员修改超级管理员 + if (existingUser[0].role === 'superadmin' && req.admin.role !== 'superadmin') { + return res.status(403).json({ message: '无权修改超级管理员' }); + } + + // 更新用户基本信息 + let updateQuery = 'UPDATE admins SET username = ?, email = ?'; + let updateParams = [username, email]; + + if (password) { + const hashedPassword = await bcrypt.hash(password, 10); + updateQuery += ', password = ?'; + updateParams.push(hashedPassword); + } + + if (role) { + updateQuery += ', role = ?'; + updateParams.push(role); + } + + if (status) { + updateQuery += ', status = ?'; + updateParams.push(status); + } + + updateQuery += ' WHERE id = ?'; + updateParams.push(userId); + + await connection.query(updateQuery, updateParams); + + // 更新权限 + if (permissions) { + await connection.query('DELETE FROM admin_permissions WHERE admin_id = ?', [userId]); + + if (permissions.length > 0) { + const permissionValues = permissions.map(permission => [userId, permission]); + await connection.query( + 'INSERT INTO admin_permissions (admin_id, permission) VALUES ?', + [permissionValues] + ); + } + } + + await connection.commit(); + res.json({ message: '用户更新成功' }); + } catch (error) { + await connection.rollback(); + res.status(500).json({ message: error.message }); + } finally { + connection.release(); + } +}); + +// 删除用户 +router.delete('/users/:id', auth, checkPermission('user:manage'), async (req, res) => { + try { + const userId = req.params.id; + + // 检查用户是否存在 + const [existingUser] = await db.query( + 'SELECT role FROM admins WHERE id = ?', + [userId] + ); + + if (!existingUser.length) { + return res.status(404).json({ message: '用户不存在' }); + } + + // 不允许删除超级管理员 + if (existingUser[0].role === 'superadmin') { + return res.status(403).json({ message: '不能删除超级管理员' }); + } + + // 不能删除自己 + if (userId === req.admin.id) { + return res.status(400).json({ message: '不能删除自己的账号' }); + } + + await db.query('DELETE FROM admins WHERE id = ?', [userId]); + res.json({ message: '用户删除成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/messages.js b/backend/routes/messages.js new file mode 100644 index 0000000..48216ac --- /dev/null +++ b/backend/routes/messages.js @@ -0,0 +1,65 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../utils/db'); +const auth = require('../middleware/auth'); +const checkPermission = require('../middleware/checkPermission'); +const { broadcastNewMessage } = require('../websocket'); + +// 提交留言 +router.post('/', async (req, res) => { + try { + const { name, email, message } = req.body; + + const [result] = await db.query( + 'INSERT INTO messages (name, email, content) VALUES (?, ?, ?)', + [name, email, message] + ); + + // 获取新插入的消息详情 + const [newMessage] = await db.query( + 'SELECT * FROM messages WHERE id = ?', + [result.insertId] + ); + + // 广播新消息通知 + broadcastNewMessage(newMessage[0]); + + res.status(201).json({ message: '留言提交成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 获取留言列表(需要管理员权限) +router.get('/', auth, checkPermission('message:manage'), async (req, res) => { + try { + const [messages] = await db.query(` + SELECT m.*, a.username as reader_name + FROM messages m + LEFT JOIN admins a ON m.read_by = a.id + ORDER BY m.created_at DESC + `); + res.json(messages); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 标记留言为已读 +router.put('/:id/read', auth, checkPermission('message:manage'), async (req, res) => { + try { + await db.query( + `UPDATE messages + SET status = 'read', + read_by = ?, + read_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [req.admin.id, req.params.id] + ); + res.json({ message: '标记成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js new file mode 100644 index 0000000..1a77c9d --- /dev/null +++ b/backend/routes/systemSettings.js @@ -0,0 +1,57 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../utils/db'); +const auth = require('../middleware/auth'); +const checkPermission = require('../middleware/checkPermission'); + +// 获取所有设置 +router.get('/', async (req, res) => { + try { + const [settings] = await db.query('SELECT * FROM system_settings'); + const formattedSettings = settings.reduce((acc, setting) => { + if (!acc[setting.category]) { + acc[setting.category] = {}; + } + acc[setting.category][setting.key] = { + value: setting.value, + description: setting.description + }; + return acc; + }, {}); + res.json(formattedSettings); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 获取指定类别的设置 +router.get('/:category', async (req, res) => { + try { + const [settings] = await db.query( + 'SELECT * FROM system_settings WHERE category = ?', + [req.params.category] + ); + res.json(settings); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 更新设置(需要管理员权限) +router.put('/:category/:key', auth, checkPermission('system:manage'), async (req, res) => { + try { + const { category, key } = req.params; + const { value } = req.body; + + await db.query( + 'UPDATE system_settings SET value = ? WHERE category = ? AND `key` = ?', + [value, category, key] + ); + + res.json({ message: '设置更新成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/tags.js b/backend/routes/tags.js new file mode 100644 index 0000000..4c7dd97 --- /dev/null +++ b/backend/routes/tags.js @@ -0,0 +1,70 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../utils/db'); +const auth = require('../middleware/auth'); +const checkPermission = require('../middleware/checkPermission'); + +// 获取所有标签 +router.get('/', async (req, res) => { + try { + const [tags] = await db.query('SELECT * FROM game_tag_master ORDER BY name'); + res.json(tags); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 添加标签 +router.post('/', auth, checkPermission('tag:manage'), async (req, res) => { + try { + const { name, description } = req.body; + + if (!name) { + return res.status(400).json({ message: '标签名称不能为空' }); + } + + const [result] = await db.query( + 'INSERT INTO game_tag_master (name, description) VALUES (?, ?)', + [name, description || ''] + ); + + res.status(201).json({ + message: '标签添加成功', + id: result.insertId + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 更新标签 +router.put('/:id', auth, checkPermission('tag:manage'), async (req, res) => { + try { + const { name, description } = req.body; + + if (!name) { + return res.status(400).json({ message: '标签名称不能为空' }); + } + + await db.query( + 'UPDATE game_tag_master SET name = ?, description = ? WHERE id = ?', + [name, description || '', req.params.id] + ); + + res.json({ message: '标签更新成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 删除标签 +router.delete('/:id', auth, checkPermission('tag:manage'), async (req, res) => { + try { + await db.query('DELETE FROM game_tag_master WHERE id = ?', [req.params.id]); + res.json({ message: '标签删除成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/scripts/test-db.js b/backend/scripts/test-db.js new file mode 100644 index 0000000..a3e4312 --- /dev/null +++ b/backend/scripts/test-db.js @@ -0,0 +1,38 @@ +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function testConnection() { + console.log('Testing database connection...'); + console.log('Connection details:', { + host: process.env.DB_HOST, + user: process.env.DB_USER, + database: process.env.DB_DATABASE + }); + + try { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('Connection established successfully'); + + // 测试查询 + const [rows] = await connection.query('SELECT 1 as test'); + console.log('Test query result:', rows); + + // 测试admins表 + const [admins] = await connection.query('SELECT COUNT(*) as count FROM admins'); + console.log('Admins count:', admins[0].count); + + await connection.end(); + console.log('Connection closed'); + } catch (error) { + console.error('Connection test failed:', error); + process.exit(1); + } +} + +testConnection(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..537ae74 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,724 @@ +// 确保在最开始就加载环境变量 +require('dotenv').config(); +console.log('Environment loaded:', { + DB_HOST: process.env.DB_HOST, + DB_USER: process.env.DB_USER, + DB_DATABASE: process.env.DB_DATABASE, + PORT: process.env.PORT +}); + +const express = require('express'); +const http = require('http'); +const cors = require('cors'); +const multer = require('multer'); +const path = require('path'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const auth = require('./middleware/auth'); +const checkPermission = require('./middleware/checkPermission'); +const db = require('./utils/db'); +const app = express(); +const systemSettingsRoutes = require('./routes/systemSettings'); +const messagesRoutes = require('./routes/messages'); +const tagsRoutes = require('./routes/tags'); +const adminRoutes = require('./routes/admin'); +const server = http.createServer(app); +const { initWebSocket } = require('./websocket'); + +// 请求日志中间件 +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); + next(); +}); + +// 中间件 +app.use(cors()); +app.use(express.json()); + +// 配置文件上传 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + let uploadPath = 'uploads'; + if (file.mimetype.startsWith('image/')) { + uploadPath += '/images'; + } else if (file.mimetype.startsWith('video/')) { + uploadPath += '/videos'; + } + // 确保目录存在 + require('fs').mkdirSync(path.join(__dirname, uploadPath), { recursive: true }); + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + path.extname(file.originalname)); + } +}); + +const upload = multer({ + storage, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB limit + }, + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|gif|mp4|webm/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + if (extname && mimetype) { + return cb(null, true); + } + cb(new Error('只支持图片和视频文件!')); + } +}); + +// API路由 +app.get('/api/categories', async (req, res) => { + try { + const [categories] = await db.query('SELECT * FROM categories'); + res.json(categories); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.get('/api/games', async (req, res) => { + try { + const [games] = await db.query(` + SELECT g.*, c.name as category_name, + GROUP_CONCAT(DISTINCT gp.platform) as platforms, + GROUP_CONCAT(DISTINCT gt.tag) as tags + FROM games g + LEFT JOIN categories c ON g.category_id = c.id + LEFT JOIN game_platforms gp ON g.id = gp.game_id + LEFT JOIN game_tags gt ON g.id = gt.game_id + GROUP BY g.id + `); + + // 处理平台和标签字符串 + games.forEach(game => { + game.platforms = game.platforms ? game.platforms.split(',') : []; + game.tags = game.tags ? game.tags.split(',') : []; + }); + + res.json(games); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.get('/api/games/:id', async (req, res) => { + try { + const game = await Game.findById(req.params.id); + if (!game) { + return res.status(404).json({ message: 'Game not found' }); + } + + // 获取游戏对应的分类信息 + const category = await Category.findById(game.categoryId); + const gameWithCategory = { + ...game.toObject(), + category + }; + + res.json(gameWithCategory); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.get('/api/games/search', async (req, res) => { + try { + const { query, category } = req.query; + let searchQuery = {}; + + if (query) { + searchQuery.$or = [ + { title: new RegExp(query, 'i') }, + { description: new RegExp(query, 'i') } + ]; + } + + if (category) { + searchQuery.categoryId = category; + } + + const games = await Game.find(searchQuery); + res.json(games); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 添加游戏 +app.post('/api/games', auth, upload.single('image'), async (req, res) => { + try { + const { title, description, category_id, developer, release_date, platforms, tags } = req.body; + + // 验证必填字段 + if (!title || !category_id) { + return res.status(400).json({ message: '标题和分类为必填项' }); + } + + // 解析平台和标签数据(因为是通过FormData传输的) + const platformsArray = Array.isArray(platforms) ? platforms : platforms ? [platforms] : []; + const tagsArray = Array.isArray(tags) ? tags : tags ? [tags] : []; + + const [result] = await db.query( + 'INSERT INTO games (title, description, category_id, developer, release_date, image) VALUES (?, ?, ?, ?, ?, ?)', + [ + title, + description || '', + parseInt(category_id), + developer || '', + release_date || null, + req.file ? req.file.path : null + ] + ); + + const gameId = result.insertId; + + // 添加平台信息 + if (platformsArray.length > 0) { + const platformValues = platformsArray.map(platform => [gameId, platform]); + await db.query( + 'INSERT INTO game_platforms (game_id, platform) VALUES ?', + [platformValues] + ); + } + + // 添加标签信息 + if (tagsArray.length > 0) { + const tagValues = tagsArray.map(tag => [gameId, tag]); + await db.query( + 'INSERT INTO game_tags (game_id, tag) VALUES ?', + [tagValues] + ); + } + + res.status(201).json({ + message: '游戏添加成功', + id: gameId + }); + } catch (error) { + console.error('Error adding game:', error); + res.status(500).json({ + message: '添加游戏失败', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +// 管理员注册 +app.post('/api/admin/register', async (req, res) => { + try { + const { username, password, email } = req.body; + + // 验证必填字段 + if (!username || !password || !email) { + return res.status(400).json({ message: '用户名、密码和邮箱都是必填项' }); + } + + // 检查用户名是否已存在 + const [existingUsers] = await db.query( + 'SELECT id FROM admins WHERE username = ?', + [username] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ message: '用户名已存在' }); + } + + // 检查邮箱是否已存在 + const [existingEmails] = await db.query( + 'SELECT id FROM admins WHERE email = ?', + [email] + ); + + if (existingEmails.length > 0) { + return res.status(400).json({ message: '邮箱已被使用' }); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + console.log('Hashed password:', hashedPassword); + + // 开始事务 + const connection = await db.getConnection(); + await connection.beginTransaction(); + + try { + // 插入管理员记录 + const [result] = await connection.query( + 'INSERT INTO admins (username, password, email, role, status) VALUES (?, ?, ?, ?, ?)', + [username, hashedPassword, email, 'editor', 'active'] + ); + + // 插入默认权限 + await connection.query( + 'INSERT INTO admin_permissions (admin_id, permission) VALUES (?, ?), (?, ?)', + [result.insertId, 'game:manage', result.insertId, 'media:manage'] + ); + + await connection.commit(); + + // 生成JWT令牌 + const token = jwt.sign( + { id: result.insertId, role: 'editor' }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.status(201).json({ + message: '注册成功', + token, + admin: { + username, + role: 'editor', + permissions: ['game:manage', 'media:manage'] + } + }); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ message: '注册失败', error: error.message }); + } +}); + +// 管理员登录 +app.post('/api/admin/login', async (req, res) => { + try { + const { username, password } = req.body; + console.log('Login attempt:', { username }); // 添加登录尝试日志 + + const [admins] = await db.query( + 'SELECT a.*, GROUP_CONCAT(ap.permission) as permissions FROM admins a ' + + 'LEFT JOIN admin_permissions ap ON a.id = ap.admin_id ' + + 'WHERE a.username = ? AND a.status = "active" ' + + 'GROUP BY a.id', + [username] + ); + + console.log('Query result:', admins); // 添加查询结果日志 + + const admin = admins[0]; + if (!admin) { + console.log('Admin not found'); + return res.status(401).json({ message: '用户名或密码错误' }); + } + + console.log('Comparing password for:', admin.username); + console.log('Encrypted password:', admin.password); + const hashedPassword = await bcrypt.hash(password, 10); + console.log('Hashed password:', hashedPassword); + const isMatch = await bcrypt.compare(password, admin.password); + console.log('Password match result:', isMatch); + + if (!isMatch) { + console.log('Password does not match'); + return res.status(401).json({ message: '用户名或密码错误' }); + } + + // 更新最后登录时间 + await db.query( + 'UPDATE admins SET last_login = NOW() WHERE id = ?', + [admin.id] + ); + + const token = jwt.sign( + { id: admin.id, role: admin.role }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + console.log('Login successful for:', admin.username); + + res.json({ + token, + admin: { + username: admin.username, + role: admin.role, + permissions: admin.permissions ? admin.permissions.split(',') : [] + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ message: error.message }); + } +}); + +// 媒体文件上传 +app.post('/api/media/upload', auth, upload.single('file'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ message: '请选择要上传的文件' }); + } + + const media = new Media({ + filename: req.file.filename, + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size, + url: `/uploads/${req.file.filename}`, + type: req.file.mimetype.startsWith('image/') ? 'image' : 'video', + uploadedBy: req.admin.id + }); + + await media.save(); + res.json(media); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 获取媒体文件列表 +app.get('/api/media', auth, async (req, res) => { + try { + const { type, gameId } = req.query; + let query = {}; + + if (type) query.type = type; + if (gameId) query.gameId = gameId; + + const media = await Media.find(query) + .populate('uploadedBy', 'username') + .sort('-uploadedAt'); + + res.json(media); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 删除媒体文件 +app.delete('/api/media/:id', auth, async (req, res) => { + try { + const media = await Media.findById(req.params.id); + if (!media) { + return res.status(404).json({ message: '文件不存在' }); + } + + // 删除物理文件 + const filePath = path.join(__dirname, media.url); + fs.unlink(filePath, async (err) => { + if (err) { + console.error('Error deleting file:', err); + } + await media.remove(); + res.json({ message: '文件已删除' }); + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 更新游戏 +app.put('/api/games/:id', auth, upload.single('image'), async (req, res) => { + try { + const { title, description, category_id, developer, release_date, platforms, tags } = req.body; + + // 验证游戏是否存在 + const [existingGame] = await db.query( + 'SELECT * FROM games WHERE id = ?', + [req.params.id] + ); + + if (!existingGame.length) { + return res.status(404).json({ message: '游戏不存在' }); + } + + // 更新游戏基本信息 + await db.query( + `UPDATE games SET + title = ?, + description = ?, + category_id = ?, + developer = ?, + release_date = ?, + image = ? + WHERE id = ?`, + [ + title, + description || '', + parseInt(category_id), + developer || '', + release_date || null, + req.file ? req.file.path : existingGame[0].image, + req.params.id + ] + ); + + // 更新平台信息 + if (platforms) { + // 先删除旧的平台信息 + await db.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]); + + // 添加新的平台信息 + if (platforms.length > 0) { + const platformValues = platforms.map(platform => [req.params.id, platform]); + await db.query( + 'INSERT INTO game_platforms (game_id, platform) VALUES ?', + [platformValues] + ); + } + } + + // 更新标签信息 + if (tags) { + // 先删除旧的标签信息 + await db.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]); + + // 添加新的标签信息 + if (tags.length > 0) { + const tagValues = tags.map(tag => [req.params.id, tag]); + await db.query( + 'INSERT INTO game_tags (game_id, tag) VALUES ?', + [tagValues] + ); + } + } + + res.json({ message: '游戏更新成功' }); + } catch (error) { + console.error('Error updating game:', error); + res.status(500).json({ message: error.message }); + } +}); + +// 添加统计API路由 +app.get('/api/stats', auth, async (req, res) => { + try { + const [gamesCount, categoriesCount, mediaCount] = await Promise.all([ + Game.countDocuments(), + Category.countDocuments(), + Media.countDocuments() + ]); + + res.json({ + gamesCount, + categoriesCount, + mediaCount + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 用户管理API +app.get('/api/admins', auth, checkPermission('user:manage'), async (req, res) => { + try { + const admins = await Admin.find() + .select('-password') + .populate('createdBy', 'username'); + res.json(admins); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.post('/api/admins', auth, checkPermission('user:manage'), async (req, res) => { + try { + const { username, email, password, role } = req.body; + + const admin = new Admin({ + username, + email, + password, + role, + createdBy: req.admin.id + }); + + await admin.save(); + res.status(201).json({ message: '用户创建成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.put('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + // 不允许修改超级管理员 + const admin = await Admin.findById(id); + if (admin.role === 'superadmin' && req.admin.role !== 'superadmin') { + return res.status(403).json({ message: '无权修改超级管理员' }); + } + + // 如果要修改密码,需要加密 + if (updates.password) { + updates.password = await bcrypt.hash(updates.password, 10); + } + + const updatedAdmin = await Admin.findByIdAndUpdate( + id, + updates, + { new: true } + ).select('-password'); + + res.json(updatedAdmin); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +app.delete('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => { + try { + const { id } = req.params; + + // 不允许删除超级管理员 + const admin = await Admin.findById(id); + if (admin.role === 'superadmin') { + return res.status(403).json({ message: '不能删除超级管理员' }); + } + + // 不能删除自己 + if (id === req.admin.id) { + return res.status(400).json({ message: '不能删除自己的账号' }); + } + + await Admin.findByIdAndDelete(id); + res.json({ message: '用户删除成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 获取当前用户信息 +app.get('/api/admins/me', auth, async (req, res) => { + try { + const admin = await Admin.findById(req.admin.id) + .select('-password'); + res.json(admin); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 修改密码 +app.post('/api/admins/change-password', auth, async (req, res) => { + try { + const { oldPassword, newPassword } = req.body; + + const admin = await Admin.findById(req.admin.id); + const isMatch = await bcrypt.compare(oldPassword, admin.password); + + if (!isMatch) { + return res.status(400).json({ message: '原密码错误' }); + } + + admin.password = newPassword; + await admin.save(); + + res.json({ message: '密码修改成功' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +// 系统设置路由 +app.use('/api/settings', systemSettingsRoutes); + +// 留言管理路由 +app.use('/api/messages', messagesRoutes); + +// 标签管理路由 +app.use('/api/tags', tagsRoutes); + +// 用户管理路由 +app.use('/api/admin', adminRoutes); + +// 提供静态文件访问 +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// 删除游戏 +app.delete('/api/games/:id', auth, async (req, res) => { + try { + // 验证游戏是否存在 + const [existingGame] = await db.query( + 'SELECT * FROM games WHERE id = ?', + [req.params.id] + ); + + if (!existingGame.length) { + return res.status(404).json({ message: '游戏不存在' }); + } + + // 开始事务 + const connection = await db.getConnection(); + await connection.beginTransaction(); + + try { + // 删除游戏图片 + if (existingGame[0].image) { + const imagePath = path.join(__dirname, existingGame[0].image); + try { + if (require('fs').existsSync(imagePath)) { + require('fs').unlinkSync(imagePath); + console.log(`Deleted image file: ${imagePath}`); + } + } catch (error) { + console.error('Error deleting image file:', error); + // 继续执行,不因文件删除失败而中断整个操作 + } + } + + // 删除相关的平台信息 + await connection.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]); + + // 删除相关的标签信息 + await connection.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]); + + // 删除游戏记录 + await connection.query('DELETE FROM games WHERE id = ?', [req.params.id]); + + await connection.commit(); + console.log(`Successfully deleted game with ID: ${req.params.id}`); + res.json({ message: '游戏删除成功' }); + } catch (error) { + await connection.rollback(); + console.error('Transaction failed:', error); + throw error; + } finally { + connection.release(); + } + } catch (error) { + console.error('Error deleting game:', error); + res.status(500).json({ + message: '删除游戏失败', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +// 获取管理员数量 +app.get('/api/admin/count', auth, async (req, res) => { + try { + const [result] = await db.query('SELECT COUNT(*) as count FROM admins'); + res.json({ count: result[0].count }); + } catch (error) { + console.error('Error getting admin count:', error); + res.status(500).json({ message: error.message }); + } +}); + +// 错误处理中间件 +app.use((err, req, res, next) => { + console.error('Server error:', err); + res.status(500).json({ + message: 'Internal server error', + error: process.env.NODE_ENV === 'development' ? err.message : undefined + }); +}); + +// 启动服务器 +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); + +// 初始化 WebSocket +initWebSocket(server); \ No newline at end of file diff --git a/backend/utils/db.js b/backend/utils/db.js new file mode 100644 index 0000000..754bc18 --- /dev/null +++ b/backend/utils/db.js @@ -0,0 +1,53 @@ +const mysql = require('mysql2/promise'); + +// 打印数据库连接配置(不包含密码) +console.log('Connecting to database:', { + host: process.env.DB_HOST, + user: process.env.DB_USER, + database: process.env.DB_DATABASE +}); + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + // 添加额外的连接选项 + connectTimeout: 10000, // 10 seconds + ssl: { + // 允许自签名证书 + rejectUnauthorized: false + } +}); + +// 测试数据库连接 +pool.getConnection() + .then(connection => { + console.log('Database connected successfully'); + // 测试查询 + return connection.query('SELECT 1') + .then(() => { + console.log('Database query test successful'); + connection.release(); + }) + .catch(error => { + console.error('Database query test failed:', error); + connection.release(); + throw error; + }); + }) + .catch(error => { + console.error('Database connection error:', { + code: error.code, + errno: error.errno, + sqlState: error.sqlState, + sqlMessage: error.sqlMessage + }); + // 如果是致命错误,终止程序 + process.exit(1); + }); + +module.exports = pool; \ No newline at end of file diff --git a/backend/websocket.js b/backend/websocket.js new file mode 100644 index 0000000..68f08d5 --- /dev/null +++ b/backend/websocket.js @@ -0,0 +1,64 @@ +const WebSocket = require('ws'); +const jwt = require('jsonwebtoken'); + +let wss = null; + +const initWebSocket = (server) => { + wss = new WebSocket.Server({ server }); + + wss.on('connection', (ws, req) => { + // 验证管理员身份 + const token = req.url.split('=')[1]; + if (!token) { + ws.close(); + return; + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + ws.adminId = decoded.id; + ws.isAlive = true; + + ws.on('pong', () => { + ws.isAlive = true; + }); + + ws.on('close', () => { + ws.isAlive = false; + }); + } catch (error) { + ws.close(); + } + }); + + // 心跳检测 + const interval = setInterval(() => { + wss.clients.forEach((ws) => { + if (ws.isAlive === false) return ws.terminate(); + ws.isAlive = false; + ws.ping(); + }); + }, 30000); + + wss.on('close', () => { + clearInterval(interval); + }); +}; + +const broadcastNewMessage = (message) => { + if (!wss) return; + + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ + type: 'new_message', + data: message + })); + } + }); +}; + +module.exports = { + initWebSocket, + broadcastNewMessage +}; \ No newline at end of file diff --git a/database/init.sql b/database/init.sql new file mode 100644 index 0000000..34d8cb3 --- /dev/null +++ b/database/init.sql @@ -0,0 +1,178 @@ +-- 创建数据库 +DROP DATABASE IF EXISTS game_categories; +CREATE DATABASE game_categories DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE game_categories; + +-- 管理员表 +CREATE TABLE admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + role ENUM('superadmin', 'admin', 'editor') DEFAULT 'editor', + status ENUM('active', 'inactive') DEFAULT 'active', + last_login DATETIME, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +-- 管理员权限表 +CREATE TABLE admin_permissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + admin_id INT NOT NULL, + permission VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE, + UNIQUE KEY unique_admin_permission (admin_id, permission) +) ENGINE=InnoDB; + +-- 分类表 +CREATE TABLE categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +-- 游戏表 +CREATE TABLE games ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(100) NOT NULL, + description TEXT, + category_id INT, + image VARCHAR(255), + release_date DATE, + developer VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +) ENGINE=InnoDB; + +-- 游戏平台关联表 +CREATE TABLE game_platforms ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT, + platform VARCHAR(50), + FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- 游戏标签关联表 +CREATE TABLE game_tags ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT, + tag VARCHAR(50), + FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- 游戏标签主表 +CREATE TABLE game_tag_master ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +-- 系统配置表 +CREATE TABLE system_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + category VARCHAR(50) NOT NULL, + `key` VARCHAR(50) NOT NULL, + value TEXT, + description VARCHAR(255), + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_setting (`category`, `key`) +) ENGINE=InnoDB; + +-- 留言表 +CREATE TABLE messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + content TEXT NOT NULL, + status ENUM('unread', 'read') DEFAULT 'unread', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + read_by INT, + read_at TIMESTAMP NULL, + FOREIGN KEY (read_by) REFERENCES admins(id) +) ENGINE=InnoDB; + +-- 创建索引 +CREATE INDEX idx_games_category ON games(category_id); +CREATE INDEX idx_games_title ON games(title); + +-- 初始化数据 +-- 创建超级管理员账号 (密码: 123qwe) +INSERT INTO admins (username, password, email, role, status) VALUES +('admin', '$2a$10$YqFCm8pRCKOPtPDzqvwsUOQY7ZeUFNHDHI8Qc3ej3wqgNWO6GEBqe', 'admin@example.com', 'superadmin', 'active'); + +-- 添加管理员权限 +INSERT INTO admin_permissions (admin_id, permission) VALUES +(1, 'user:manage'), +(1, 'game:manage'), +(1, 'category:manage'), +(1, 'media:manage'), +(1, 'system:manage'), +(1, 'message:manage'); + +-- 插入游戏分类 +INSERT INTO categories (name, description) VALUES +('动作游戏', '包含格斗、射击等动作元素的游戏'), +('角色扮演', '玩家可以扮演角色进行冒险的游戏'), +('策略游戏', '需要战略思维的游戏'), +('体育竞技', '模拟各种体育运动的游戏'), +('休闲益智', '简单有趣的休闲游戏'); + +-- 插入示例游戏数据 +INSERT INTO games (title, description, category_id, developer, release_date) VALUES +('魔兽世界', '著名的大型多人在线角色扮演游戏', 2, 'Blizzard', '2004-11-23'), +('FIFA 23', '最新的足球体育游戏', 4, 'EA Sports', '2022-09-30'), +('俄罗斯方块', '经典的休闲益智游戏', 5, 'Various', '1984-06-06'); + +-- 添加游戏平台数据 +INSERT INTO game_platforms (game_id, platform) VALUES +(1, 'PC'), +(1, 'Mac'), +(2, 'PC'), +(2, 'PS5'), +(2, 'Xbox'), +(3, 'PC'), +(3, 'Mobile'); + +-- 添加游戏标签 +INSERT INTO game_tags (game_id, tag) VALUES +(1, 'MMORPG'), +(1, '奇幻'), +(2, '体育'), +(2, '足球'), +(3, '益智'), +(3, '经典'); + +-- 插入一些默认标签 +INSERT INTO game_tag_master (name, description) VALUES +('RPG', '角色扮演游戏'), +('动作', '动作类游戏'), +('策略', '策略类游戏'), +('射击', '射击类游戏'), +('冒险', '冒险类游戏'), +('体育', '体育类游戏'), +('竞速', '竞速类游戏'), +('模拟', '模拟类游戏'); + +-- 插入初始配置数据 +INSERT INTO system_settings (category, `key`, value, description) VALUES +-- 公司信息 +('company', 'name', 'Game Categories', '公司名称'), +('company', 'description', 'Game Categories 成立于2024年,是一家专注于游戏分类和推荐的创新型科技公司。', '公司简介'), +('company', 'mission', '让每个玩家都能找到最适合自己的游戏,创造快乐的游戏体验。', '公司使命'), +('company', 'vision', '成为全球领先的游戏分类和推荐平台,引领游戏文化的发展。', '公司愿景'), +-- 联系方式 +('contact', 'address', '北京市朝阳区xxx大厦', '公司地址'), +('contact', 'email', 'contact@example.com', '联系邮箱'), +('contact', 'phone', '+86 123 4567 8900', '联系电话'), +-- 核心价值观 +('values', 'value1', '{"title":"用户至上","description":"始终以用户需求为中心,提供最优质的服务"}', '核心价值观1'), +('values', 'value2', '{"title":"创新驱动","description":"持续创新,推动技术与产品的进步"}', '核心价值观2'), +('values', 'value3', '{"title":"品质保证","description":"严格把控质量,确保服务的可靠性"}', '核心价值观3'), +('values', 'value4', '{"title":"开放共赢","description":"与合作伙伴共同成长,实现价值共享"}', '核心价值观4'); + +-- 更新默认权限设置 +UPDATE admins SET role = 'admin' WHERE role = 'editor'; -- 将编辑角色升级为管理员 \ No newline at end of file diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..47b9652 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VUE_APP_API_URL=http://localhost:3000 \ No newline at end of file diff --git a/frontend/assets/logo.png b/frontend/assets/logo.png new file mode 100644 index 0000000..d062d36 Binary files /dev/null and b/frontend/assets/logo.png differ diff --git a/frontend/babel.config.js b/frontend/babel.config.js new file mode 100644 index 0000000..c59f12c --- /dev/null +++ b/frontend/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ab3b988 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "game-category-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "vue": "^3.2.0", + "vue-router": "^4.0.0", + "axios": "^0.21.1", + "element-plus": "^2.3.0", + "@element-plus/icons-vue": "^2.1.0", + "core-js": "^3.6.5", + "echarts": "^5.4.3" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.0", + "@vue/cli-plugin-router": "~4.5.0", + "@vue/cli-service": "~4.5.0", + "@vue/compiler-sfc": "^3.2.0" + } +} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..0f484f8 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..d062d36 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/GameDetail.vue b/frontend/src/components/GameDetail.vue new file mode 100644 index 0000000..0531aa8 --- /dev/null +++ b/frontend/src/components/GameDetail.vue @@ -0,0 +1,116 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/GameList.vue b/frontend/src/components/GameList.vue new file mode 100644 index 0000000..237044f --- /dev/null +++ b/frontend/src/components/GameList.vue @@ -0,0 +1,229 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..e8ef873 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,30 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import axios from 'axios' + +const app = createApp(App) + +// 配置axios +axios.defaults.baseURL = process.env.VUE_APP_API_URL + +// 添加请求拦截器 +axios.interceptors.request.use(config => { + const token = localStorage.getItem('admin_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 注册Element Plus图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(ElementPlus) +app.use(router) +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/admin.js b/frontend/src/router/admin.js new file mode 100644 index 0000000..2731ee9 --- /dev/null +++ b/frontend/src/router/admin.js @@ -0,0 +1,93 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import AdminLogin from '../views/admin/Login.vue'; +import AdminLayout from '../views/admin/Layout.vue'; +import Dashboard from '../views/admin/Dashboard.vue'; +import GameManagement from '../views/admin/GameManagement.vue'; +import MediaManagement from '../views/admin/MediaManagement.vue'; +import CategoryManagement from '../views/admin/CategoryManagement.vue'; +import UserManagement from '../views/admin/UserManagement.vue'; +import axios from 'axios'; + +const routes = [ + { + path: '/admin/login', + name: 'AdminLogin', + component: AdminLogin, + meta: { requiresAuth: false } + }, + { + path: '/admin', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Dashboard', + component: Dashboard + }, + { + path: 'games', + name: 'GameManagement', + component: GameManagement + }, + { + path: 'media', + name: 'MediaManagement', + component: MediaManagement + }, + { + path: 'categories', + name: 'CategoryManagement', + component: CategoryManagement + }, + { + path: 'users', + name: 'UserManagement', + component: UserManagement, + meta: { requiresPermission: 'user:manage' } + } + ] + } +]; + +const router = createRouter({ + history: createWebHistory(), + routes +}); + +// 路由守卫 +router.beforeEach(async (to, from, next) => { + const token = localStorage.getItem('admin_token'); + + if (to.matched.some(record => record.meta.requiresAuth)) { + if (!token) { + next('/admin/login'); + return; + } + + // 检查权限 + if (to.meta.requiresPermission) { + try { + const response = await axios.get('http://localhost:3000/api/admins/me', { + headers: { Authorization: `Bearer ${token}` } + }); + + const user = response.data; + if (user.role !== 'superadmin' && + !user.permissions.includes(to.meta.requiresPermission)) { + next('/admin'); // 重定向到仪表盘 + return; + } + } catch (error) { + next('/admin/login'); + return; + } + } + + next(); + } else { + next(); + } +}); + +export default router; \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..ff9c043 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,109 @@ +import { createRouter, createWebHistory } from 'vue-router' +import AdminLogin from '../views/admin/Login.vue' +import AdminLayout from '../views/admin/Layout.vue' +import Dashboard from '../views/admin/Dashboard.vue' +import GameManagement from '../views/admin/GameManagement.vue' +import CategoryManagement from '../views/admin/CategoryManagement.vue' +import UserManagement from '../views/admin/UserManagement.vue' + +const routes = [ + { + path: '/', + name: 'Home', + component: () => import('../views/Home.vue') + }, + { + path: '/games', + name: 'Games', + component: () => import('../views/Games.vue') + }, + { + path: '/categories', + name: 'Categories', + component: () => import('../views/Categories.vue') + }, + { + path: '/about', + name: 'About', + component: () => import('../views/About.vue') + }, + { + path: '/admin/login', + name: 'AdminLogin', + component: AdminLogin, + meta: { requiresAuth: false } + }, + { + path: '/admin/register', + name: 'AdminRegister', + component: () => import('../views/admin/Register.vue'), + meta: { requiresAuth: false } + }, + { + path: '/admin', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Dashboard', + component: Dashboard + }, + { + path: 'games', + name: 'GameManagement', + component: GameManagement + }, + { + path: 'categories', + name: 'CategoryManagement', + component: CategoryManagement + }, + { + path: 'users', + name: 'UserManagement', + component: UserManagement, + meta: { requiresPermission: 'user:manage' } + }, + { + path: 'messages', + name: 'MessageManagement', + component: () => import('../views/admin/MessageManagement.vue'), + meta: { requiresPermission: 'message:manage' } + }, + { + path: 'tags', + name: 'TagManagement', + component: () => import('../views/admin/TagManagement.vue'), + meta: { requiresPermission: 'tag:manage' } + }, + { + path: 'settings', + name: 'SystemSettings', + component: () => import('../views/admin/SystemSettings.vue'), + meta: { requiresPermission: 'system:manage' } + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const token = localStorage.getItem('admin_token') + if (to.matched.some(record => record.meta.requiresAuth)) { + if (!token) { + next('/admin/login') + } else { + next() + } + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..fe4fb7c --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,97 @@ +import axios from 'axios' + +// 设置基础URL +axios.defaults.baseURL = process.env.VUE_APP_API_URL || 'http://localhost:3000' + +// 添加请求拦截器用于调试 +axios.interceptors.request.use(config => { + console.log('API Request:', { + url: config.url, + method: config.method, + data: config.data + }); + return config; +}); + +// 添加响应拦截器用于调试 +axios.interceptors.response.use( + response => { + console.log('API Response:', { + url: response.config.url, + status: response.status, + data: response.data + }); + return response; + }, + error => { + console.error('API Error:', { + url: error.config?.url, + status: error.response?.status, + data: error.response?.data + }); + return Promise.reject(error); + } +); + +export const login = async (username, password) => { + console.log('API: Sending login request'); + try { + const response = await axios.post('/api/admin/login', { username, password }) + console.log('API: Login response:', response.data); + return response.data + } catch (error) { + console.error('API: Login error:', error); + throw error + } +} + +export const register = async ({ username, password, email }) => { + console.log('API: Sending register request'); + try { + const response = await axios.post('/api/admin/register', { + username, + password, + email + }) + console.log('API: Register response:', response.data); + return response.data + } catch (error) { + console.error('API: Register error:', error); + throw error + } +} + +export const getGames = async () => { + const response = await axios.get('/api/games') + return response.data +} + +export const getCategories = async () => { + const response = await axios.get('/api/categories') + return response.data +} + +export const getAdmins = async () => { + const response = await axios.get('/api/admins') + return response.data +} + +export const createAdmin = async (data) => { + const response = await axios.post('/api/admins', data) + return response.data +} + +export const updateAdmin = async (id, data) => { + const response = await axios.put(`/api/admins/${id}`, data) + return response.data +} + +export const deleteAdmin = async (id) => { + const response = await axios.delete(`/api/admins/${id}`) + return response.data +} + +export const getStats = async () => { + const response = await axios.get('/api/stats') + return response.data +} \ No newline at end of file diff --git a/frontend/src/views/About.vue b/frontend/src/views/About.vue new file mode 100644 index 0000000..b276ccc --- /dev/null +++ b/frontend/src/views/About.vue @@ -0,0 +1,400 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Categories.vue b/frontend/src/views/Categories.vue new file mode 100644 index 0000000..6db372e --- /dev/null +++ b/frontend/src/views/Categories.vue @@ -0,0 +1,231 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Games.vue b/frontend/src/views/Games.vue new file mode 100644 index 0000000..4e08489 --- /dev/null +++ b/frontend/src/views/Games.vue @@ -0,0 +1,286 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..73b1799 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,418 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/CategoryManagement.vue b/frontend/src/views/admin/CategoryManagement.vue new file mode 100644 index 0000000..9d05991 --- /dev/null +++ b/frontend/src/views/admin/CategoryManagement.vue @@ -0,0 +1,282 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Dashboard.vue b/frontend/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..ef09abf --- /dev/null +++ b/frontend/src/views/admin/Dashboard.vue @@ -0,0 +1,262 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/GameManagement.vue b/frontend/src/views/admin/GameManagement.vue new file mode 100644 index 0000000..7110ec7 --- /dev/null +++ b/frontend/src/views/admin/GameManagement.vue @@ -0,0 +1,535 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Layout.vue b/frontend/src/views/admin/Layout.vue new file mode 100644 index 0000000..61f75e0 --- /dev/null +++ b/frontend/src/views/admin/Layout.vue @@ -0,0 +1,219 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Login.vue b/frontend/src/views/admin/Login.vue new file mode 100644 index 0000000..34c169e --- /dev/null +++ b/frontend/src/views/admin/Login.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/MediaManagement.vue b/frontend/src/views/admin/MediaManagement.vue new file mode 100644 index 0000000..025a681 --- /dev/null +++ b/frontend/src/views/admin/MediaManagement.vue @@ -0,0 +1,311 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/MessageManagement.vue b/frontend/src/views/admin/MessageManagement.vue new file mode 100644 index 0000000..e0cb16f --- /dev/null +++ b/frontend/src/views/admin/MessageManagement.vue @@ -0,0 +1,283 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Register.vue b/frontend/src/views/admin/Register.vue new file mode 100644 index 0000000..33eba87 --- /dev/null +++ b/frontend/src/views/admin/Register.vue @@ -0,0 +1,160 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/SystemSettings.vue b/frontend/src/views/admin/SystemSettings.vue new file mode 100644 index 0000000..70dbb1d --- /dev/null +++ b/frontend/src/views/admin/SystemSettings.vue @@ -0,0 +1,198 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/TagManagement.vue b/frontend/src/views/admin/TagManagement.vue new file mode 100644 index 0000000..aca1a50 --- /dev/null +++ b/frontend/src/views/admin/TagManagement.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/UserManagement.vue b/frontend/src/views/admin/UserManagement.vue new file mode 100644 index 0000000..647f8fd --- /dev/null +++ b/frontend/src/views/admin/UserManagement.vue @@ -0,0 +1,301 @@ + + + + + \ No newline at end of file