Skip to content

Commit 4f750f1

Browse files
committed
Initial release: Minecraft服务器前端管理面板
0 parents  commit 4f750f1

83 files changed

Lines changed: 7656 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules/
2+
dist/
3+
build/
4+
.gradle/
5+
*.jar
6+
.env
7+
*.db
8+
*.sqlite
9+
*.log
10+
.DS_Store
11+
Thumbs.db

README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# MinecraftServerFrontend - Minecraft服务器前端管理面板
2+
3+
一个全栈自定义 Minecraft 服务器管理面板,采用自研服务端插件直连架构,无需 RCON,通过 WebSocket 插件桥接实现低延迟、高可靠的服务器管控。
4+
5+
## 功能特性
6+
7+
- **实时仪表盘** — TPS、内存、CPU、在线玩家数等核心指标实时展示
8+
- **玩家管理** — 查看在线玩家列表,支持踢出、封禁、给予物品等操作
9+
- **Web 控制台** — 实时日志流 + 命令输入,支持 Tab 自动补全
10+
- **公告系统** — 创建/编辑/删除公告,一键推送至游戏内
11+
- **插件管理** — 查看已安装插件列表及状态
12+
- **Uptime 监控** — 集成 Uptime Kuma,展示服务器延迟与可用率图表
13+
- **响应式设计** — 桌面端侧边栏 + 移动端底部导航栏,完美适配各种设备
14+
- **暗色/亮色主题** — 支持一键切换,带圆形扩散过渡动画
15+
16+
## 架构
17+
18+
```
19+
┌─────────────┐ HTTP/WS ┌──────────────┐ WebSocket ┌────────────────┐
20+
│ Browser │ ◄──────────────► │ Node.js │ ◄──────────────► │ MC Server │
21+
│ (Vue 3) │ │ Backend │ │ (Paper 1.21) │
22+
│ │ │ Express 5 │ │ Bridge Plugin │
23+
└─────────────┘ └──────────────┘ └────────────────┘
24+
25+
26+
┌──────────────┐
27+
│ SQLite │
28+
│ (用户/公告) │
29+
└──────────────┘
30+
```
31+
32+
**核心特点:** 不依赖 RCON,通过自研 Paper 插件(MuCraftBridge)建立 WebSocket 直连通道,实现命令执行、日志转发、状态推送等功能。
33+
34+
## 技术栈
35+
36+
| 层级 | 技术 |
37+
|------|------|
38+
| 前端 | Vue 3 + TypeScript + Naive UI + ECharts + xterm.js |
39+
| 后端 | Node.js + Express 5 + Socket.IO + better-sqlite3 |
40+
| 插件 | Paper API 1.21 + Java-WebSocket + Gson |
41+
| 构建 | Vite (前端) + tsc (后端) + Gradle Shadow (插件) |
42+
43+
## 快速开始
44+
45+
### 1. 克隆项目
46+
47+
```bash
48+
git clone <repo-url> mc-admin-panel
49+
cd mc-admin-panel
50+
```
51+
52+
### 2. 构建插件
53+
54+
```bash
55+
cd plugin
56+
./gradlew build
57+
```
58+
59+
将生成的 `build/libs/MuCraftBridge.jar` 复制到 MC 服务器的 `plugins/` 目录,重启服务器。
60+
61+
### 3. 配置并启动后端
62+
63+
```bash
64+
cd backend
65+
npm install
66+
cp .env.example .env
67+
# 编辑 .env,设置 JWT_SECRET、BRIDGE_SECRET 等
68+
npm run build
69+
npm start
70+
```
71+
72+
### 4. 构建并部署前端
73+
74+
```bash
75+
cd frontend
76+
npm install
77+
npm run build
78+
```
79+
80+
`dist/` 目录部署到 Nginx 或其他静态文件服务器,配置反向代理将 `/api``/socket.io` 转发到后端。
81+
82+
### 5. 配置插件
83+
84+
编辑 MC 服务器中 `plugins/MuCraftBridge/config.yml`
85+
86+
```yaml
87+
port: 25580
88+
secret: "your-secret-here" # 与后端 .env 中的 BRIDGE_SECRET 保持一致
89+
status-interval: 5000
90+
```
91+
92+
### 6. 访问面板
93+
94+
打开浏览器访问部署地址,使用默认账号登录:
95+
96+
| 账号 | 密码 |
97+
|------|------|
98+
| admin | admin123 |
99+
100+
> ⚠️ **请在首次登录后通过环境变量修改默认凭据**
101+
102+
## 环境变量
103+
104+
| 变量名 | 默认值 | 说明 |
105+
|--------|--------|------|
106+
| `PORT` | `3000` | 后端监听端口 |
107+
| `JWT_SECRET` | — | JWT 签名密钥(**必须修改**) |
108+
| `BRIDGE_HOST` | `127.0.0.1` | 插件 WebSocket 地址 |
109+
| `BRIDGE_PORT` | `25580` | 插件 WebSocket 端口 |
110+
| `BRIDGE_SECRET` | `change-me-secret` | 插件连接密钥(**必须修改**) |
111+
| `MC_DIR` | `/opt/minecraft-server` | MC 服务器目录 |
112+
| `ADMIN_USERNAME` | `admin` | 初始管理员用户名 |
113+
| `ADMIN_PASSWORD` | `admin123` | 初始管理员密码 |
114+
| `UPTIME_URL` | `http://localhost:3001` | Uptime Kuma 地址(可选) |
115+
| `UPTIME_API_KEY` | — | Uptime Kuma API 密钥(可选) |
116+
| `UPTIME_MONITOR_IDS` | `3,6` | 监控项 ID(可选) |
117+
118+
## 项目结构
119+
120+
```
121+
mc-admin-panel/
122+
├── backend/ # Node.js 后端
123+
│ ├── src/ # TypeScript 源码
124+
│ ├── .env.example # 环境变量示例
125+
│ └── package.json
126+
├── frontend/ # Vue 3 前端
127+
│ ├── src/ # Vue 源码
128+
│ ├── public/ # 静态资源
129+
│ └── package.json
130+
├── plugin/ # Paper 服务端插件
131+
│ ├── src/main/ # Java 源码 + 资源文件
132+
│ ├── build.gradle.kts
133+
│ └── settings.gradle.kts
134+
└── README.md
135+
```
136+
137+
## License
138+
139+
MIT

backend/.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
PORT=3000
2+
JWT_SECRET=your-jwt-secret-here
3+
BRIDGE_HOST=127.0.0.1
4+
BRIDGE_PORT=25580
5+
BRIDGE_SECRET=change-me-secret
6+
MC_DIR=/opt/minecraft-server
7+
ADMIN_USERNAME=admin
8+
ADMIN_PASSWORD=admin123
9+
10+
# Uptime Kuma integration (optional)
11+
UPTIME_URL=http://localhost:3001
12+
UPTIME_API_KEY=
13+
UPTIME_MONITOR_IDS=3,6

backend/package.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "minecraft-server-backend",
3+
"version": "1.0.0",
4+
"description": "Minecraft服务器前端管理面板 - 后端服务",
5+
"main": "index.js",
6+
"scripts": {
7+
"dev": "tsx watch src/index.ts",
8+
"build": "tsc",
9+
"start": "node dist/index.js",
10+
"test": "vitest run",
11+
"test:watch": "vitest"
12+
},
13+
"keywords": [],
14+
"author": "",
15+
"license": "ISC",
16+
"type": "module",
17+
"dependencies": {
18+
"@types/ws": "^8.18.1",
19+
"bcrypt": "^6.0.0",
20+
"better-sqlite3": "^12.8.0",
21+
"chokidar": "^5.0.0",
22+
"cors": "^2.8.6",
23+
"dotenv": "^17.3.1",
24+
"express": "^5.2.1",
25+
"jsonwebtoken": "^9.0.3",
26+
"rcon-srcds": "^2.1.0",
27+
"socket.io": "^4.8.3",
28+
"ws": "^8.19.0"
29+
},
30+
"devDependencies": {
31+
"@types/bcrypt": "^6.0.0",
32+
"@types/better-sqlite3": "^7.6.13",
33+
"@types/cors": "^2.8.19",
34+
"@types/express": "^5.0.6",
35+
"@types/jsonwebtoken": "^9.0.10",
36+
"@types/node": "^25.5.0",
37+
"@types/supertest": "^7.2.0",
38+
"supertest": "^7.2.2",
39+
"tsx": "^4.21.0",
40+
"typescript": "^5.9.3",
41+
"vitest": "^4.1.0"
42+
}
43+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
2+
import Database from 'better-sqlite3';
3+
import bcrypt from 'bcrypt';
4+
import request from 'supertest';
5+
6+
// ─── In-memory DB setup ─────────────────────────────────────────
7+
8+
const state = vi.hoisted(() => {
9+
return { db: null as import('better-sqlite3').Database | null };
10+
});
11+
12+
vi.mock('../../db/index.js', () => ({
13+
getDb: () => state.db,
14+
closeDb: vi.fn(),
15+
}));
16+
17+
import { app } from '../../app.js';
18+
19+
// ─── Helpers ────────────────────────────────────────────────────
20+
21+
/** Login as admin and return the JWT token. */
22+
async function getToken(): Promise<string> {
23+
const res = await request(app)
24+
.post('/api/auth/login')
25+
.send({ username: 'admin', password: 'admin123' });
26+
return res.body.token as string;
27+
}
28+
29+
// ─── Test suite ─────────────────────────────────────────────────
30+
31+
describe('Announcements API', () => {
32+
let token: string;
33+
34+
beforeAll(async () => {
35+
const db = new Database(':memory:');
36+
db.exec(`
37+
CREATE TABLE users (
38+
id INTEGER PRIMARY KEY AUTOINCREMENT,
39+
username TEXT UNIQUE NOT NULL,
40+
password_hash TEXT NOT NULL,
41+
created_at TEXT DEFAULT (datetime('now'))
42+
);
43+
CREATE TABLE announcements (
44+
id INTEGER PRIMARY KEY AUTOINCREMENT,
45+
title TEXT NOT NULL,
46+
content TEXT NOT NULL,
47+
is_pinned INTEGER DEFAULT 0,
48+
created_at TEXT DEFAULT (datetime('now')),
49+
updated_at TEXT DEFAULT (datetime('now'))
50+
);
51+
`);
52+
53+
const hash = bcrypt.hashSync('admin123', 4);
54+
db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)').run(
55+
'admin',
56+
hash,
57+
);
58+
59+
state.db = db;
60+
token = await getToken();
61+
});
62+
63+
beforeEach(() => {
64+
// Clear announcements between tests
65+
state.db!.prepare('DELETE FROM announcements').run();
66+
});
67+
68+
afterAll(() => {
69+
state.db?.close();
70+
state.db = null;
71+
});
72+
73+
// ── Auth guard ──────────────────────────────────────────────
74+
75+
it('GET /api/announcements without auth → 401', async () => {
76+
const res = await request(app).get('/api/announcements');
77+
expect(res.status).toBe(401);
78+
});
79+
80+
// ── CRUD ────────────────────────────────────────────────────
81+
82+
it('POST → GET → PUT → DELETE full lifecycle', async () => {
83+
// Create
84+
const createRes = await request(app)
85+
.post('/api/announcements')
86+
.set('Authorization', `Bearer ${token}`)
87+
.send({ title: 'Test Title', content: 'Test Content', is_pinned: 0 });
88+
89+
expect(createRes.status).toBe(201);
90+
expect(createRes.body).toHaveProperty('id');
91+
expect(createRes.body.title).toBe('Test Title');
92+
93+
const id = createRes.body.id;
94+
95+
// Read
96+
const listRes = await request(app)
97+
.get('/api/announcements')
98+
.set('Authorization', `Bearer ${token}`);
99+
100+
expect(listRes.status).toBe(200);
101+
expect(listRes.body.announcements).toHaveLength(1);
102+
expect(listRes.body.total).toBe(1);
103+
104+
// Update
105+
const updateRes = await request(app)
106+
.put(`/api/announcements/${id}`)
107+
.set('Authorization', `Bearer ${token}`)
108+
.send({ title: 'Updated Title' });
109+
110+
expect(updateRes.status).toBe(200);
111+
expect(updateRes.body.title).toBe('Updated Title');
112+
113+
// Delete
114+
const deleteRes = await request(app)
115+
.delete(`/api/announcements/${id}`)
116+
.set('Authorization', `Bearer ${token}`);
117+
118+
expect(deleteRes.status).toBe(200);
119+
expect(deleteRes.body.success).toBe(true);
120+
121+
// Verify gone
122+
const afterDelete = await request(app)
123+
.get('/api/announcements')
124+
.set('Authorization', `Bearer ${token}`);
125+
126+
expect(afterDelete.body.announcements).toHaveLength(0);
127+
});
128+
129+
it('POST with empty title → 400', async () => {
130+
const res = await request(app)
131+
.post('/api/announcements')
132+
.set('Authorization', `Bearer ${token}`)
133+
.send({ title: '', content: 'Some content' });
134+
135+
expect(res.status).toBe(400);
136+
expect(res.body).toHaveProperty('error');
137+
});
138+
139+
it('POST with missing content → 400', async () => {
140+
const res = await request(app)
141+
.post('/api/announcements')
142+
.set('Authorization', `Bearer ${token}`)
143+
.send({ title: 'Title Only' });
144+
145+
expect(res.status).toBe(400);
146+
});
147+
148+
it('DELETE nonexistent announcement → 404', async () => {
149+
const res = await request(app)
150+
.delete('/api/announcements/99999')
151+
.set('Authorization', `Bearer ${token}`);
152+
153+
expect(res.status).toBe(404);
154+
});
155+
});

0 commit comments

Comments
 (0)