├── public/ # 前端静态文件
│ ├── index.html # 主网站页面
│ ├── admin.html # 简单的管理后台页面
│ ├── css/
│ │ ├── style.css # 主网站样式
│ │ └── admin.css # 管理后台样式
│ └── js/
│ ├── main.js # 主网站JS逻辑,包括WebSocket
│ └── admin.js # 管理后台JS逻辑
└── server/ # Python Flask 后端服务
├── app.py # Flask 主应用和API、WebSocket逻辑
├── db.json # 简单JSON文件作为“数据库”
└── requirements.txt # Python依赖
功能实现思路:
实时服务器状态:
后端 (server/app.py): 使用 python-mcstatus 库定时查询真实的Minecraft服务器状态(例如每5-10秒)。将最新状态存储起来,并通过 WebSocket 推送给所有连接的客户端。
前端 (public/js/main.js): 连接到后端的WebSocket,接收服务器状态更新,并实时更新页面上的玩家数量、MOTD等信息。
排行榜:
后端 (server/app.py): 提供API (/api/rankings) 读取 db.json 中的排行榜数据。
前端 (public/js/main.js): 页面加载时通过API获取排行榜数据并展示。
管理后台 (server/app.py, public/admin.html, public/js/admin.js): 实现简单的登录功能,并提供API来更新(模拟)排行榜数据。
赞助商城:
后端 (server/app.py): 提供API (/api/shop/products) 读取 db.json 中的商品数据。
前端 (public/js/main.js): 页面加载时通过API获取商品数据并展示,并提供模拟的“购买”交互(实际不会处理支付)。
管理后台: 提供API来管理商品列表。
管理后台 (演示):
一个独立的 admin.html 页面,包含简单的登录表单。
登录成功后,可以访问一个简易的仪表盘,展示一些服务器信息,并提供修改排行榜/商城数据的模拟接口。
文件内容:
1. server/requirements.txt
Flask==2.3.3
Flask-SocketIO==5.3.0
python-mcstatus==1.20.1
eventlet==0.33.3
Flask-Cors==3.0.10
simplejson==3.19.2
2. server/db.json (模拟数据库)
{
"admin_credentials": {
"username": "admin",
"password": "password"
},
"rankings": {
"wealth": [
{ "id": "p1", "name": "PlayerA", "value": 10000000 },
{ "id": "p2", "name": "PlayerB", "value": 8500000 },
{ "id": "p3", "name": "PlayerC", "value": 7200000 },
{ "id": "p4", "name": "PlayerD", "value": 6000000 },
{ "id": "p5", "name": "PlayerE", "value": 5500000 }
],
"pvp": [
{ "id": "p6", "name": "PvPKing", "value": 1200 },
{ "id": "p7", "name": "SlayerXYZ", "value": 950 },
{ "id": "p8", "name": "Warrior_N", "value": 880 },
{ "id": "p9", "name": "BladeMaster", "value": 700 },
{ "id": "p10", "name": "ShadowAssassin", "value": 650 }
],
"island_level": [
{ "id": "p11", "name": "SkyLord", "value": 500 },
{ "id": "p12", "name": "CloudDweller", "value": 480 },
{ "id": "p13", "name": "IslandMaster", "value": 450 },
{ "id": "p14", "name": "AirArchitect", "value": 400 },
{ "id": "p15", "name": "FloatingCity", "value": 380 }
]
},
"shop_products": [
{
"id": "vip_basic",
"name": "VIP 基础套餐",
"description": "享受飞行、经验加成等多项特权。",
"price": 50,
"icon": "fas fa-gem"
},
{
"id": "rare_pack",
"name": "稀有礼包",
"description": "内含珍稀道具、强力装备和独特装饰品。",
"price": 120,
"icon": "fas fa-box-open"
},
{
"id": "custom_title",
"name": "自定义称号",
"description": "选择一个属于你的独特称号,彰显个性。",
"price": 80,
"icon": "fas fa-star"
},
{
"id": "monthly_pass",
"name": "月度通行证",
"description": "每月专属奖励、特权和点券,持续享受。",
"price": 30,
"icon": "fas fa-calendar-alt"
}
]
}
3. server/app.py
import os
import json
import time
from threading import Thread
from flask import Flask, jsonify, request, send_from_directory
from flask_socketio import SocketIO, emit
from flask_cors import CORS
from mcstatus import JavaServer
import simplejson # Using simplejson for better JSON handling, especially if dealing with non-standard types
app = Flask(__name__, static_folder='../public', static_url_path='/')
app.config['SECRET_KEY'] = 'your_secret_key_here' # 生产环境请更换为强密钥
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
CORS(app) # 允许所有来源的CORS请求
# --- 配置 ---
MINECRAFT_SERVER_IP = "play.hypixel.net" # 替换为你的Minecraft服务器IP
MINECRAFT_SERVER_PORT = 25565
MC_STATUS_INTERVAL = 10 # 查询Minecraft服务器状态的间隔(秒)
# --- 简单 JSON 数据库 ---
DB_FILE = os.path.join(os.path.dirname(__file__), 'db.json')
def load_db():
if not os.path.exists(DB_FILE):
# 如果db.json不存在,创建一个空的或者默认的
default_db = {
"admin_credentials": {"username": "admin", "password": "password"},
"rankings": {
"wealth": [],
"pvp": [],
"island_level": []
},
"shop_products": []
}
save_db(default_db)
return default_db
with open(DB_FILE, 'r', encoding='utf-8') as f:
return simplejson.load(f)
def save_db(data):
with open(DB_FILE, 'w', encoding='utf-8') as f:
simplejson.dump(data, f, indent=2, ensure_ascii=False)
db = load_db()
# --- Minecraft 服务器状态查询 ---
current_mc_status = {
"online": False,
"players": {"online": 0, "max": 0, "list": []},
"version": "Unknown",
"motd": {"clean": ["服务器状态加载中..."]},
"favicon": ""
}
def get_minecraft_server_status():
global current_mc_status
try:
server = JavaServer.lookup(f"{MINECRAFT_SERVER_IP}:{MINECRAFT_SERVER_PORT}")
status = server.status()
current_mc_status = {
"online": True,
"players": {
"online": status.players.online,
"max": status.players.max,
"list": [{"name": p.name, "uuid": p.id} for p in status.players.sample] if status.players.sample else []
},
"version": status.version.name,
"motd": {"html": status.motd.html.split('\n'), "clean": status.motd.clean.split('\n')},
"favicon": status.favicon if status.favicon else ""
}
print(f"Minecraft server {MINECRAFT_SERVER_IP} is online. Players: {status.players.online}/{status.players.max}")
except Exception as e:
current_mc_status = {
"online": False,
"players": {"online": 0, "max": 0, "list": []},
"version": "Unknown",
"motd": {"clean": ["服务器离线或无法访问。"]},
"favicon": ""
}
print(f"Failed to get Minecraft server status: {e}")
# 定时更新Minecraft服务器状态并广播
def mc_status_updater():
while True:
get_minecraft_server_status()
socketio.emit('mc_status_update', current_mc_status, namespace='/')
socketio.sleep(MC_STATUS_INTERVAL)
# 启动状态更新线程
status_thread = Thread(target=mc_status_updater)
status_thread.daemon = True
status_thread.start()
# --- 路由 ---
@app.route('/')
def serve_index():
return send_from_directory(app.static_folder, 'index.html')
@app.route('/admin')
def serve_admin():
return send_from_directory(app.static_folder, 'admin.html')
# API: 获取Minecraft服务器最新状态
@app.route('/api/mc_status')
def api_mc_status():
return jsonify(current_mc_status)
# API: 获取排行榜数据
@app.route('/api/rankings')
def api_rankings():
return jsonify(db['rankings'])
# API: 获取商城产品
@app.route('/api/shop/products')
def api_shop_products():
return jsonify(db['shop_products'])
# API: 模拟购买 (这里只是简单的返回成功,不涉及真实逻辑)
@app.route('/api/shop/purchase', methods=['POST'])
def api_shop_purchase():
data = request.json
product_id = data.get('product_id')
# 可以在这里添加一些日志或更复杂的模拟逻辑
return jsonify({"message": f"成功购买产品: {product_id}", "success": True}), 200
# API: 管理后台登录
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
data = request.json
username = data.get('username')
password = data.get('password')
if username == db['admin_credentials']['username'] and password == db['admin_credentials']['password']:
# 实际应用中会生成一个JWT或Session
return jsonify({"message": "登录成功", "token": "mock_admin_token", "success": True}), 200
return jsonify({"message": "用户名或密码错误", "success": False}), 401
# API: 管理后台获取排行榜 (需要认证,这里简化为只检查token存在)
@app.route('/api/admin/rankings', methods=['GET'])
def admin_get_rankings():
# 模拟认证:检查请求头中是否有'Authorization'
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
return jsonify(db['rankings'])
# API: 管理后台更新排行榜 (需要认证)
@app.route('/api/admin/rankings', methods=['POST'])
def admin_update_rankings():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
# 实际项目中这里会有更复杂的校验
new_rankings = request.json.get('rankings')
if new_rankings:
db['rankings'] = new_rankings
save_db(db)
socketio.emit('rankings_update', new_rankings, namespace='/') # 实时通知前端更新
return jsonify({"message": "排行榜更新成功", "success": True}), 200
return jsonify({"message": "数据无效", "success": False}), 400
# API: 管理后台获取商城产品 (需要认证)
@app.route('/api/admin/shop/products', methods=['GET'])
def admin_get_shop_products():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
return jsonify(db['shop_products'])
# API: 管理后台更新商城产品 (需要认证)
@app.route('/api/admin/shop/products', methods=['POST'])
def admin_update_shop_products():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
new_products = request.json.get('products')
if new_products:
db['shop_products'] = new_products
save_db(db)
socketio.emit('shop_products_update', new_products, namespace='/') # 实时通知前端更新
return jsonify({"message": "商城产品更新成功", "success": True}), 200
return jsonify({"message": "数据无效", "success": False}), 400
# --- SocketIO 事件 ---
@socketio.on('connect')
def test_connect():
print('Client connected')
emit('mc_status_update', current_mc_status) # 新连接时立即发送当前状态
@socketio.on('disconnect')
def test_disconnect():
print('Client disconnected')
if __name__ == '__main__':
socketio.run(app, debug=True, port=5000)
4. public/index.html (主网站页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX Minecraft 服务器官网 - 体验极致方块世界</title>
<meta name="description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta name="keywords" content="Minecraft, 服务器, 多人游戏, 生存, RPG, 空岛, PVP, 赞助, 排行榜, 活动">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="http://localhost:5000/"> <!-- 请更新为你的实际域名 -->
<meta property="og:title" content="XXX Minecraft 服务器官网 - 体验极致方块世界">
<meta property="og:description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta property="og:image" content="http://localhost:5000/images/og-image.jpg"> <!-- 请更新为你的实际域名 -->
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="http://localhost:5000/"> <!-- 请更新为你的实际域名 -->
<meta property="twitter:title" content="XXX Minecraft 服务器官网 - 体验极致方块世界">
<meta property="twitter:description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta property="twitter:image" content="http://localhost:5000/images/twitter-image.jpg"> <!-- 请更新为你的实际域名 -->
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/style.css">
<!-- Socket.IO Client -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
<!-- 结构化数据 (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "XXX Minecraft 服务器官网",
"url": "http://localhost:5000/",
"description": "XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。",
"potentialAction": {
"@type": "SearchAction",
"target": "http://localhost:5000/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
</head>
<body>
<header>
<div class="container" style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<a href="#home" class="logo">XXX MC</a>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#status">服务器状态</a></li>
<li><a href="#features">特色玩法</a></li>
<li><a href="#rankings">荣耀榜</a></li>
<li><a href="#shop">赞助商城</a></li>
<li><a href="#community">社区</a></li>
</ul>
</nav>
<button id="theme-toggle" class="theme-switcher">
<i class="fas fa-sun"></i> <span>亮色主题</span>
</button>
</div>
</header>
<main>
<section id="home" class="hero">
<h1>欢迎来到 XXX Minecraft 服务器</h1>
<p>探索无限的方块世界,与志同道合的朋友一同创造、冒险、竞技!我们提供稳定、流畅、充满创意的游戏体验。</p>
<a href="javascript:void(0);" onclick="copyIP('play.xxx-mc.com')" class="btn-primary">
<i class="fas fa-copy"></i> 一键复制服务器 IP: play.xxx-mc.com
</a>
<p id="hero-online-status">
<span id="hero-status-indicator" class="status-indicator status-unknown"></span>
当前在线人数: <span id="hero-online-players">加载中...</span> / <span id="hero-max-players">加载中...</span>
</p>
</section>
<section id="status" class="container">
<h2>服务器实时状态</h2>
<div class="server-status-card">
<div class="status-header">
<img id="server-favicon" src="/images/default-favicon.png" alt="服务器图标"> <!-- 默认图标 -->
<div>
<h3>XXX Minecraft Server</h3>
<p id="server-address" style="font-size: 0.9rem; color: var(--text-color);">连接地址: play.xxx-mc.com</p>
<p style="font-weight: bold;">
<span id="server-main-indicator" class="status-indicator status-unknown"></span>
<span id="server-status-text">加载中...</span>
</p>
</div>
</div>
<p id="server-motd" style="color: var(--text-color); margin-bottom: 20px; font-style: italic;">加载服务器消息...</p>
<div class="status-details">
<div class="status-item">
<strong><span id="status-online-players">?</span></strong>
<span>在线玩家</span>
</div>
<div class="status-item">
<strong><span id="status-max-players">?</span></strong>
<span>最大容量</span>
</div>
<div class="status-item">
<strong><span id="status-version">?</span></strong>
<span>服务器版本</span>
</div>
<!-- 延迟信息通常需要客户端直接ping,后端API不直接提供,这里暂时不显示,或保持为占位符 -->
<div class="status-item">
<strong><span id="status-latency">N/A</span></strong>
<span>延迟</span>
</div>
</div>
<h4 style="margin-top: 30px; margin-bottom: 15px; color: var(--primary-color);">在线玩家列表 (最多显示 10 名):</h4>
<div id="player-list" class="player-avatars">
<!-- Player avatars will be loaded here by JavaScript -->
<span id="player-list-placeholder">加载玩家中...</span>
</div>
</div>
</section>
<section id="features" class="container">
<h2>服务器特色与玩法</h2>
<div class="features-grid">
<div class="feature-item">
<i class="fas fa-tree"></i>
<h3>生存模式</h3>
<p>原版生存体验,高版本特色,无乱七八糟的魔改,让你回归最纯粹的方块世界。</p>
</div>
<div class="feature-item">
<i class="fas fa-cloud-sun"></i>
<h3>空岛生存</h3>
<p>从一块浮空的岛屿开始,挑战资源有限的极限生存,打造你的空中帝国,与好友共建家园。</p>
</div>
<div class="feature-item">
<i class="fas fa-dragon"></i>
<h3>RPG 冒险</h3>
<p>丰富的剧情任务,独特的BOSS挑战,专属技能与装备,沉浸式角色扮演体验,等你来征服。</p>
</div>
<div class="feature-item">
<i class="fas fa-trophy"></i>
<h3>PVP 竞技</h3>
<p>设有PVP竞技场,公平公正的对决环境,考验你的操作与策略,争夺PVP王者宝座。</p>
</div>
<div class="feature-item">
<i class="fas fa-hat-wizard"></i>
<h3>独家插件</h3>
<p>多款独家定制插件,优化游戏体验,提供更多互动与便利功能,提升游戏乐趣。</p>
</div>
<div class="feature-item">
<i class="fas fa-users-cog"></i>
<h3>友好社区</h3>
<p>拥有活跃的玩家社区和专业的管理团队,为你解决游戏中遇到的任何问题,共同成长。</p>
</div>
</div>
</section>
<section id="rankings" class="container">
<h2>荣耀排行榜</h2>
<p>记录玩家们的辉煌成就,展现服务器内最强者的风采!</p>
<div id="rankings-content" class="rankings-grid">
<!-- Rankings will be loaded here by JavaScript -->
<p>加载排行榜中...</p>
</div>
<p style="margin-top: 40px; font-size: 1.1rem; color: var(--text-color);">排行榜数据每 24 小时更新一次。</p>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 30px;"><i class="fas fa-chart-bar"></i> 查看完整排行榜</a>
</section>
<section id="shop" class="container">
<h2>赞助商城</h2>
<p>感谢您的支持,赞助所得将用于服务器的持续运营和发展。获取专属VIP、特殊道具和独特头衔!</p>
<div id="shop-products-grid" class="shop-grid">
<!-- Shop products will be loaded here by JavaScript -->
<p>加载商城产品中...</p>
</div>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 50px;"><i class="fas fa-shopping-cart"></i> 前往赞助商城</a>
</section>
<section id="community" class="container">
<h2>加入我们的社区</h2>
<p>我们拥有活跃的社区,欢迎您加入官方QQ群、Discord服务器,与志同道合的玩家一同交流心得,分享乐趣。</p>
<div class="social-links" style="margin-top: 40px;">
<a href="#" target="_blank" title="QQ 群"><i class="fab fa-qq"></i></a>
<a href="#" target="_blank" title="Discord"><i class="fab fa-discord"></i></a>
<a href="#" target="_blank" title="Bilibili"><i class="fab fa-bilibili"></i></a>
<a href="#" target="_blank" title="GitHub"><i class="fab fa-github"></i></a>
</div>
<p style="margin-top: 40px;">遇到问题?您可以通过用户中心提交工单,获得管理团队的专业支持。</p>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 30px;"><i class="fas fa-user-circle"></i> 前往用户中心</a>
</section>
</main>
<footer>
<div class="container">
<p>© 2025 XXX Minecraft Server. All rights reserved.</p>
<p>联系我们: <a href="mailto:support@xxx-mc.com" style="color: var(--primary-color); text-decoration: none;">support@xxx-mc.com</a></p>
<p><a href="/admin" style="color: var(--primary-color); text-decoration: none;">管理后台 (演示)</a></p>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
5. public/css/style.css
/* CSS 变量用于主题切换 */
:root {
--background-color: #f8fafc; /* light grey */
--text-color: #2c3e50; /* dark blue */
--primary-color: #34d399; /* emerald green */
--primary-dark-color: #10b981; /* darker emerald */
--secondary-bg-color: #ffffff; /* white */
--border-color: #e2e8f0; /* light grey border */
--card-bg-color: #ffffff;
--card-shadow: rgba(0, 0, 0, 0.1);
--header-bg-color: #ffffff;
}
html.dark {
--background-color: #1a202c; /* dark grey */
--text-color: #e2e8f0; /* light grey text */
--primary-color: #059669; /* darker emerald for dark mode */
--primary-dark-color: #047857; /* even darker */
--secondary-bg-color: #2d3748; /* charcoal */
--border-color: #4a5568; /* grey border */
--card-bg-color: #2d3748;
--card-shadow: rgba(0, 0, 0, 0.5);
--header-bg-color: #2d3748;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
font-size: 16px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
header {
background-color: var(--header-bg-color);
padding: 1rem 0;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: sticky;
top: 0;
z-index: 1000;
}
header .logo {
font-size: 1.8rem;
font-weight: 800;
color: var(--primary-color);
text-decoration: none;
margin-right: 20px;
letter-spacing: -0.05em;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
nav a {
color: var(--text-color);
text-decoration: none;
font-weight: 500;
padding: 0.5rem 0.8rem;
border-radius: 6px;
transition: background-color 0.3s ease, color 0.3s ease;
}
nav a:hover {
background-color: var(--primary-color);
color: white;
}
.theme-switcher {
background: var(--secondary-bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.6rem 1rem;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
margin-left: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.theme-switcher:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.theme-switcher i {
font-size: 1.1rem;
}
.hero {
background: url('https://source.unsplash.com/random/1920x1080/?minecraft,fantasy,game') no-repeat center center/cover; /* 示例背景图 */
color: white;
text-align: center;
padding: 120px 20px;
min-height: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.65); /* 半透明遮罩 */
z-index: 0;
}
.hero * {
position: relative;
z-index: 1;
}
.hero h1 {
font-size: 3.8rem;
margin-bottom: 25px;
text-shadow: 2px 2px 6px rgba(0,0,0,0.8);
font-weight: 900;
letter-spacing: -0.05em;
}
.hero p {
font-size: 1.6rem;
margin-bottom: 40px;
max-width: 900px;
text-shadow: 1px 1px 4px rgba(0,0,0,0.7);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
padding: 16px 32px;
text-decoration: none;
border-radius: 8px;
font-size: 1.3rem;
font-weight: bold;
transition: background-color 0.3s ease, transform 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.8rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary:hover {
background-color: var(--primary-dark-color);
transform: translateY(-2px);
}
.btn-primary i {
font-size: 1.2rem;
}
#hero-online-status {
margin-top: 25px;
font-size: 1.2rem;
font-weight: 600;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 8px;
display: inline-block;
}
section {
padding: 90px 0;
text-align: center;
}
section:nth-of-type(even) {
background-color: var(--secondary-bg-color);
}
section h2 {
font-size: 3rem;
margin-bottom: 50px;
color: var(--primary-color);
font-weight: 800;
letter-spacing: -0.03em;
}
.features-grid, .rankings-grid, .activity-grid, .shop-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 40px;
}
.feature-item, .ranking-item, .activity-item, .shop-item {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 12px;
box-shadow: 0 6px 20px var(--card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
text-align: left;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.feature-item:hover, .ranking-item:hover, .activity-item:hover, .shop-item:hover {
transform: translateY(-8px);
box-shadow: 0 10px 30px var(--card-shadow);
}
.feature-item i, .ranking-item i, .activity-item i, .shop-item i {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 15px;
}
.feature-item h3, .ranking-item h3, .activity-item h3, .shop-item h3 {
font-size: 1.8rem;
margin-bottom: 15px;
color: var(--text-color);
font-weight: 700;
}
.feature-item p, .ranking-item p, .activity-item p, .shop-item p {
font-size: 1rem;
color: var(--text-color);
flex-grow: 1;
}
.ranking-item ul {
list-style: none;
padding: 0;
margin-top: 15px;
width: 100%;
}
.ranking-item li {
background-color: var(--secondary-bg-color);
padding: 8px 15px;
margin-bottom: 5px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
border: 1px solid var(--border-color);
}
.ranking-item li:nth-child(1) { background-color: rgba(255, 215, 0, 0.2); } /* Gold */
.ranking-item li:nth-child(2) { background-color: rgba(192, 192, 192, 0.2); } /* Silver */
.ranking-item li:nth-child(3) { background-color: rgba(205, 127, 50, 0.2); } /* Bronze */
.server-status-card {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 12px;
box-shadow: 0 6px 20px var(--card-shadow);
max-width: 800px;
margin: 50px auto;
text-align: left;
border: 1px solid var(--border-color);
}
.status-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.status-header img {
width: 80px;
height: 80px;
border-radius: 12px;
border: 2px solid var(--primary-color);
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-online { background-color: #22c55e; } /* green-500 */
.status-offline { background-color: #ef4444; } /* red-500 */
.status-unknown { background-color: #f59e0b; } /* amber-500 */
.status-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
}
.status-item {
font-size: 1rem;
color: var(--text-color);
text-align: center;
}
.status-item strong {
display: block;
font-size: 1.5rem;
color: var(--primary-color);
margin-bottom: 5px;
}
.player-avatars {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 25px;
justify-content: center;
border-top: 1px dashed var(--border-color);
padding-top: 20px;
}
.player-avatars img {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--primary-color);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.player-avatars img:hover {
transform: translateY(-2px) scale(1.1);
}
.player-avatars .more-players {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--secondary-bg-color);
color: var(--text-color);
font-size: 0.8rem;
border: 2px solid var(--border-color);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
footer {
background-color: var(--secondary-bg-color);
padding: 50px 0;
text-align: center;
color: var(--text-color);
border-top: 1px solid var(--border-color);
margin-top: 60px;
}
footer p {
margin-bottom: 10px;
font-size: 0.95rem;
}
.social-links {
margin-top: 20px;
margin-bottom: 30px;
}
.social-links a {
color: var(--text-color);
margin: 0 12px;
font-size: 1.8rem;
text-decoration: none;
transition: color 0.3s ease, transform 0.2s ease;
}
.social-links a:hover {
color: var(--primary-color);
transform: translateY(-3px);
}
.disclaimer {
font-size: 0.85rem;
color: #888;
margin-top: 40px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
}
@Media (max-width: 992px) {
header nav {
order: 2; /* Move nav below logo and theme switcher */
flex-basis: 100%;
margin-top: 1rem;
justify-content: center;
}
header .logo, .theme-switcher {
margin: 0 auto;
}
.hero h1 {
font-size: 3rem;
}
.hero p {
font-size: 1.3rem;
}
section h2 {
font-size: 2.5rem;
}
}
@Media (max-width: 768px) {
.container {
padding: 0 1rem;
}
header {
flex-direction: column;
align-items: center;
}
nav ul {
flex-direction: column;
gap: 0.8rem;
margin-top: 1.5rem;
}
.theme-switcher {
margin-top: 1.5rem;
margin-left: 0;
}
.hero {
padding: 80px 15px;
min-height: 500px;
}
.hero h1 {
font-size: 2.5rem;
}
.hero p {
font-size: 1.1rem;
margin-bottom: 30px;
}
.btn-primary {
padding: 12px 25px;
font-size: 1.1rem;
}
#hero-online-status {
font-size: 1rem;
}
section {
padding: 60px 0;
}
section h2 {
font-size: 2rem;
margin-bottom: 40px;
}
.features-grid, .rankings-grid, .activity-grid, .shop-grid {
grid-template-columns: 1fr;
}
.feature-item, .ranking-item, .activity-item, .shop-item {
padding: 25px;
}
.server-status-card {
margin: 30px auto;
padding: 25px;
}
.status-header {
flex-direction: column;
text-align: center;
}
.status-header img {
width: 60px;
height: 60px;
}
.player-avatars {
justify-content: center;
}
.status-details {
grid-template-columns: 1fr 1fr;
}
}
6. public/js/main.js
const BACKEND_URL = 'http://127.0.0.1:5000'; // 后端服务地址
const SOCKET_IO_URL = BACKEND_URL;
const CRAVATAR_BASE_URL = 'https://cravatar.cn/avatar/';
// --- 主题切换逻辑 ---
const themeToggleBtn = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
const themeIcon = themeToggleBtn.querySelector('i');
const themeText = themeToggleBtn.querySelector('span');
function updateThemeUI(isDark) {
if (isDark) {
htmlElement.classList.add('dark');
themeIcon.classList.remove('fa-sun');
themeIcon.classList.add('fa-moon');
themeText.textContent = '亮色主题';
} else {
htmlElement.classList.remove('dark');
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
themeText.textContent = '暗色主题';
}
}
// 初始化主题
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let currentTheme = localStorage.getItem('theme');
if (currentTheme === null) {
currentTheme = prefersDark ? 'dark' : 'light';
}
updateThemeUI(currentTheme === 'dark');
themeToggleBtn.addEventListener('click', () => {
const isDark = htmlElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'light' : 'dark');
updateThemeUI(!isDark);
});
// --- 复制服务器 IP 逻辑 ---
function copyIP(ip) {
navigator.clipboard.writeText(ip).then(() => {
alert('服务器 IP ' + ip + ' 已复制到剪贴板!');
}).catch(err => {
console.error('无法复制 IP: ', err);
prompt('复制失败,请手动复制:', ip); // Fallback for browsers without clipboard API
});
}
// 将 copyIP 暴露给全局,以便 HTML 中的 onclick 能够调用
window.copyIP = copyIP;
// --- Minecraft 服务器实时状态 (通过 WebSocket) ---
const socket = io(SOCKET_IO_URL);
socket.on('connect', () => {
console.log('Connected to WebSocket server');
});
socket.on('disconnect', () => {
console.log('Disconnected from WebSocket server');
});
socket.on('mc_status_update', (data) => {
console.log('Received MC status update:', data);
updateServerStatusUI(data);
});
function updateServerStatusUI(data) {
const isOnline = data.online;
// 更新 Hero Section 的状态
document.getElementById('hero-online-players').textContent = isOnline ? (data.players?.online || 0) : '0';
document.getElementById('hero-max-players').textContent = isOnline ? (data.players?.max || 0) : '0';
document.getElementById('hero-status-indicator').className = `status-indicator ${isOnline ? 'status-online' : 'status-offline'}`;
// 更新详细状态卡片
document.getElementById('server-favicon').src = data.favicon || '/images/default-favicon.png'; // Fallback to a local default image
document.getElementById('server-main-indicator').className = `status-indicator ${isOnline ? 'status-online' : 'status-offline'}`;
document.getElementById('server-status-text').textContent = isOnline ? '在线' : '离线';
// Clean MOTD: Remove any HTML tags that might be in the raw motd
const motdHtml = isOnline ? (data.motd?.html?.join('<br>') || data.motd?.clean?.join('<br>') || '暂无消息') : '服务器当前离线或无法访问。';
document.getElementById('server-motd').innerHTML = motdHtml.replace(/§[0-9a-fk-or]/gi, ''); // Remove Minecraft color codes
document.getElementById('status-online-players').textContent = isOnline ? (data.players?.online || 0) : '0';
document.getElementById('status-max-players').textContent = isOnline ? (data.players?.max || 0) : '0';
document.getElementById('status-version').textContent = isOnline ? (data.version || '未知') : 'N/A';
// Latency is hard to get reliably from server-side query, keep N/A or remove if not available
// document.getElementById('status-latency').textContent = isOnline ? (data.latency || '?') : '?';
// 更新玩家列表
const playerListDiv = document.getElementById('player-list');
playerListDiv.innerHTML = ''; // Clear previous players
if (isOnline && data.players?.list && data.players.list.length > 0) {
const playersToShow = data.players.list.slice(0, 10); // Show max 10 players
playersToShow.forEach(player => {
const img = document.createElement('img');
img.src = `${CRAVATAR_BASE_URL}${player.uuid}?s=36&d=identicon`;
img.alt = player.name;
img.title = player.name; // Tooltip on hover
playerListDiv.appendChild(img);
});
if (data.players.list.length > 10) {
const morePlayersSpan = document.createElement('span');
morePlayersSpan.className = 'more-players';
morePlayersSpan.textContent = `+${data.players.list.length - 10}`;
morePlayersSpan.title = `${data.players.list.length - 10} 更多玩家`;
playerListDiv.appendChild(morePlayersSpan);
}
} else {
playerListDiv.innerHTML = '<span id="player-list-placeholder">当前无在线玩家。</span>';
}
}
// --- 获取和显示排行榜数据 ---
async function fetchRankings() {
try {
const response = await fetch(`${BACKEND_URL}/api/rankings`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const rankings = await response.json();
renderRankings(rankings);
} catch (error) {
console.error('获取排行榜数据失败:', error);
document.getElementById('rankings-content').innerHTML = '<p>无法加载排行榜数据。</p>';
}
}
function renderRankings(rankings) {
const rankingsContent = document.getElementById('rankings-content');
rankingsContent.innerHTML = ''; // Clear previous content
const rankingTypes = {
'wealth': { icon: 'fas fa-money-bill-wave', title: '财富榜', unit: '
' },
'pvp': { icon: 'fas fa-fist-raised', title: 'PVP 榜', unit: '
' },
'island_level': { icon: 'fas fa-building', title: '空岛等级', unit: '
LVL' }
};
for (const type in rankings) {
if (rankings.hasOwnProperty(type) && rankingTypes[type]) {
const item = rankingTypes[type];
const rankingList = rankings[type];
const rankingItemDiv = document.createElement('div');
rankingItemDiv.className = 'ranking-item';
rankingItemDiv.innerHTML = `
<i class="${item.icon}"></i>
<h3>${item.title}</h3>
<p>${item.title === '财富榜' ? '服务器内经济巨头排名,展示你的财富实力,谁是方块世界的首富?' :
item.title === 'PVP 榜' ? '竞技场王者,击杀数与胜率的巅峰对决,每一次战斗都为了荣耀!' :
'空岛规模与发展程度的象征,谁是天空霸主?你的空岛由你定义。'}</p>
<ul>
${rankingList.map((player, index) => `
<li>
<i class="fas fa-award" style="color: ${index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? '#CD7F32' : 'var(--text-color)'};"></i>
${index + 1}. ${player.name} - <span style="color: var(--primary-color);">${item.unit} ${player.value}</span>
</li>
`).join('')}
</ul>
`;
rankingsContent.appendChild(rankingItemDiv);
}
}
}
// 监听排行榜数据更新的WebSocket事件
socket.on('rankings_update', (data) => {
console.log('Received rankings update:', data);
renderRankings(data);
});
// --- 获取和显示赞助商城产品数据 ---
async function fetchShopProducts() {
try {
const response = await fetch(`${BACKEND_URL}/api/shop/products`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const products = await response.json();
renderShopProducts(products);
} catch (error) {
console.error('获取商城产品数据失败:', error);
document.getElementById('shop-products-grid').innerHTML = '<p>无法加载商城产品。</p>';
}
}
function renderShopProducts(products) {
const shopProductsGrid = document.getElementById('shop-products-grid');
shopProductsGrid.innerHTML = ''; // Clear previous content
if (products.length === 0) {
shopProductsGrid.innerHTML = '<p>暂无产品上架。</p>';
return;
}
products.forEach(product => {
const shopItemDiv = document.createElement('div');
shopItemDiv.className = 'shop-item';
shopItemDiv.innerHTML = `
<i class="${product.icon}"></i>
<h3>${product.name}</h3>
<p>${product.description}</p>
<a href="javascript:void(0);" onclick="simulatePurchase('${product.id}', '${product.name}')" class="btn-primary" style="margin-top: auto; padding: 10px 20px; font-size: 1rem;">
购买 - ¥${product.price}
</a>
`;
shopProductsGrid.appendChild(shopItemDiv);
});
}
// 模拟购买功能 (仅客户端提示)
async function simulatePurchase(productId, productName) {
alert(`你点击了购买 ${productName} (ID: ${productId}),实际支付功能需要后端集成支付网关。`);
// 模拟向后端发送购买请求
try {
const response = await fetch(`${BACKEND_URL}/api/shop/purchase`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ product_id: productId })
});
const result = await response.json();
if (result.success) {
console.log(result.message);
// 可以在这里更新UI,例如显示“购买成功”的通知
} else {
console.error('模拟购买失败:', result.message);
}
} catch (error) {
console.error('模拟购买请求发送失败:', error);
}
}
window.simulatePurchase = simulatePurchase; // Expose to global scope for onclick
// 监听商城产品数据更新的WebSocket事件
socket.on('shop_products_update', (data) => {
console.log('Received shop products update:', data);
renderShopProducts(data);
});
// --- 页面加载时执行 ---
document.addEventListener('DOMContentLoaded', () => {
fetchRankings(); // 加载排行榜
fetchShopProducts(); // 加载商城产品
});
7. public/admin.html (管理后台页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX Minecraft 服务器管理后台 (演示)</title>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/css/admin.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
</head>
<body>
<div class="container">
<header>
<h1 class="logo">XXX MC 管理后台</h1>
<button id="logout-button" class="btn btn-secondary" style="display: none;">注销</button>
</header>
<main id="admin-main">
<!-- 登录表单 -->
<section id="login-section" class="card">
<h2>管理员登录</h2>
<form id="login-form">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">登录</button>
<p id="login-message" class="message error-message"></p>
</form>
</section>
<!-- 仪表盘 -->
<section id="dashboard-section" class="dashboard" style="display: none;">
<h2>仪表盘</h2>
<div class="dashboard-grid">
<div class="card status-card">
<h3>服务器状态</h3>
<p>状态: <span id="admin-server-status">加载中...</span></p>
<p>在线玩家: <span id="admin-online-players">0</span> / <span id="admin-max-players">0</span></p>
<p>版本: <span id="admin-server-version">N/A</span></p>
</div>
<div class="card data-card">
<h3>排行榜管理</h3>
<p>在此处查看和编辑排行榜数据。</p>
<button class="btn btn-primary" onclick="showSection('edit-rankings-section')">编辑排行榜</button>
</div>
<div class="card data-card">
<h3>商城产品管理</h3>
<p>在此处查看和编辑商城产品。</p>
<button class="btn btn-primary" onclick="showSection('edit-shop-section')">编辑商城产品</button>
</div>
</div>
</section>
<!-- 编辑排行榜 -->
<section id="edit-rankings-section" class="card edit-section" style="display: none;">
<h2>编辑排行榜</h2>
<div class="tabs">
<button class="tab-button active" data-tab="wealth">财富榜</button>
<button class="tab-button" data-tab="pvp">PVP 榜</button>
<button class="tab-button" data-tab="island_level">空岛等级</button>
</div>
<div id="rankings-editor">
<!-- Dynamic ranking editor will be loaded here -->
</div>
<button class="btn btn-primary" id="save-rankings-btn">保存排行榜</button>
<button class="btn btn-secondary" onclick="showSection('dashboard-section')">返回仪表盘</button>
<p id="rankings-message" class="message"></p>
</section>
<!-- 编辑商城产品 -->
<section id="edit-shop-section" class="card edit-section" style="display: none;">
<h2>编辑商城产品</h2>
<div id="shop-editor">
<!-- Dynamic shop editor will be loaded here -->
</div>
<button class="btn btn-primary" id="save-shop-btn">保存商城产品</button>
<button class="btn btn-secondary" onclick="showSection('dashboard-section')">返回仪表盘</button>
<p id="shop-message" class="message"></p>
</section>
</main>
</div>
<script src="/js/admin.js"></script>
</body>
</html>
8. public/css/admin.css
:root {
--primary-color: #34d399;
--primary-dark-color: #10b981;
--secondary-color: #607d8b;
--background-color: #f0f2f5;
--card-bg-color: #ffffff;
--text-color: #333;
--border-color: #e0e0e0;
--shadow-color: rgba(0, 0, 0, 0.1);
--error-color: #ef4444;
--success-color: #22c55e;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 40px auto;
padding: 20px;
background-color: var(--background-color);
border-radius: 8px;
box-shadow: 0 4px 20px var(--shadow-color);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
margin-bottom: 30px;
border-bottom: 1px solid var(--border-color);
}
.logo {
font-size: 2rem;
color: var(--primary-color);
font-weight: bold;
margin: 0;
}
h2 {
color: var(--primary-color);
margin-bottom: 25px;
font-size: 1.8rem;
text-align: center;
}
.card {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
margin-bottom: 30px;
border: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-color);
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="number"],
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: var(--primary-color);
outline: none;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark-color);
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
margin-left: 10px;
}
.btn-secondary:hover {
background-color: #546a79;
transform: translateY(-1px);
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 5px;
font-weight: 500;
}
.error-message {
background-color: rgba(239, 68, 68, 0.1);
color: var(--error-color);
}
.success-message {
background-color: rgba(34, 197, 94, 0.1);
color: var(--success-color);
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.dashboard-grid .card {
margin-bottom: 0; /* Remove extra margin for grid items */
}
.dashboard-grid .status-card h3,
.dashboard-grid .data-card h3 {
color: var(--text-color);
margin-bottom: 15px;
text-align: left;
}
.dashboard-grid .status-card p {
text-align: left;
margin-bottom: 8px;
}
.dashboard-grid .status-card span {
font-weight: bold;
color: var(--primary-dark-color);
}
/* Tabs for ranking/shop editing */
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 1rem;
color: var(--text-color);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.tab-button.active {
color: var(--primary-color);
border-color: var(--primary-color);
font-weight: bold;
}
.tab-button:hover:not(.active) {
color: var(--primary-dark-color);
}
.ranking-entry, .shop-entry {
background-color: var(--background-color);
padding: 15px;
border-radius: 6px;
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
border: 1px solid var(--border-color);
}
.ranking-entry div, .shop-entry div {
flex: 1 1 auto;
min-width: 120px;
}
.ranking-entry input, .shop-entry input, .shop-entry textarea {
width: 100%;
}
.ranking-entry .btn-danger, .shop-entry .btn-danger {
flex: 0 0 auto;
align-self: center;
}
/* Specific styling for shop form */
.shop-entry .form-group {
margin-bottom: 0;
}
.shop-entry .form-group input, .shop-entry .form-group textarea {
min-width: unset; /* Override min-width if needed */
}
@Media (max-width: 768px) {
.container {
margin: 20px auto;
padding: 15px;
}
header {
flex-direction: column;
align-items: flex-start;
padding-bottom: 15px;
margin-bottom: 20px;
}
.logo {
margin-bottom: 10px;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.btn-secondary {
margin-left: 0;
margin-top: 10px;
}
.ranking-entry, .shop-entry {
flex-direction: column;
align-items: stretch;
}
}
9. public/js/admin.js
const ADMIN_BACKEND_URL = 'http://127.0.0.1:5000'; // 后端服务地址
const ADMIN_SOCKET_IO_URL = ADMIN_BACKEND_URL;
const ADMIN_TOKEN_KEY = 'admin_auth_token';
// --- DOM 元素 ---
const loginSection = document.getElementById('login-section');
const dashboardSection = document.getElementById('dashboard-section');
const editRankingsSection = document.getElementById('edit-rankings-section');
const editShopSection = document.getElementById('edit-shop-section');
const loginForm = document.getElementById('login-form');
const loginMessage = document.getElementById('login-message');
const logoutButton = document.getElementById('logout-button');
const rankingsEditor = document.getElementById('rankings-editor');
const shopEditor = document.getElementById('shop-editor');
const saveRankingsBtn = document.getElementById('save-rankings-btn');
const saveShopBtn = document.getElementById('save-shop-btn');
const rankingsMessage = document.getElementById('rankings-message');
const shopMessage = document.getElementById('shop-message');
const socket = io(ADMIN_SOCKET_IO_URL);
let currentRankingsData = {};
let currentShopProductsData = [];
// --- 辅助函数:显示/隐藏区域 ---
function showSection(sectionId) {
loginSection.style.display = 'none';
dashboardSection.style.display = 'none';
editRankingsSection.style.display = 'none';
editShopSection.style.display = 'none';
document.getElementById(sectionId).style.display = 'block';
// 如果是编辑排行榜或商城,需要重新加载数据
if (sectionId === 'edit-rankings-section') {
fetchAdminRankings();
} else if (sectionId === 'edit-shop-section') {
fetchAdminShopProducts();
}
}
// --- 认证相关 ---
function saveAuthToken(token) {
localStorage.setItem(ADMIN_TOKEN_KEY, token);
logoutButton.style.display = 'block';
}
function getAuthToken() {
return localStorage.getItem(ADMIN_TOKEN_KEY);
}
function removeAuthToken() {
localStorage.removeItem(ADMIN_TOKEN_KEY);
logoutButton.style.display = 'none';
}
async function checkAuth() {
const token = getAuthToken();
if (token) {
// 实际应用中会验证token的有效性
showSection('dashboard-section');
} else {
showSection('login-section');
}
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
loginMessage.textContent = '';
const username = e.target.username.value;
const password = e.target.password.value;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
saveAuthToken(data.token);
showSection('dashboard-section');
updateAdminServerStatus(window.currentMcStatus); // 登录成功后更新状态
} else {
loginMessage.textContent = data.message;
}
} catch (error) {
console.error('登录请求失败:', error);
loginMessage.textContent = '登录失败,请稍后再试。';
}
});
logoutButton.addEventListener('click', () => {
removeAuthToken();
showSection('login-section');
});
// --- 管理后台服务器状态 (通过 WebSocket) ---
window.currentMcStatus = null; // 存储最新MC状态
socket.on('connect', () => {
console.log('Admin connected to WebSocket server');
});
socket.on('disconnect', () => {
console.log('Admin disconnected from WebSocket server');
});
socket.on('mc_status_update', (data) => {
window.currentMcStatus = data; // 保存最新状态
if (getAuthToken()) { // 只有登录后才更新仪表盘上的状态
updateAdminServerStatus(data);
}
});
function updateAdminServerStatus(data) {
if (!data) {
document.getElementById('admin-server-status').textContent = '无法获取';
document.getElementById('admin-online-players').textContent = 'N/A';
document.getElementById('admin-max-players').textContent = 'N/A';
document.getElementById('admin-server-version').textContent = 'N/A';
return;
}
document.getElementById('admin-server-status').textContent = data.online ? '在线' : '离线';
document.getElementById('admin-server-status').style.color = data.online ? 'var(--success-color)' : 'var(--error-color)';
document.getElementById('admin-online-players').textContent = data.players?.online || 0;
document.getElementById('admin-max-players').textContent = data.players?.max || 0;
document.getElementById('admin-server-version').textContent = data.version || '未知';
}
// --- 排行榜管理 ---
async function fetchAdminRankings() {
rankingsMessage.textContent = '';
const token = getAuthToken();
if (!token) {
showSection('login-section');
return;
}
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/rankings`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 403) {
removeAuthToken();
showSection('login-section');
rankingsMessage.textContent = '会话过期,请重新登录。';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
currentRankingsData = await response.json();
renderRankingsEditor(currentRankingsData);
} catch (error) {
console.error('获取排行榜数据失败:', error);
rankingsMessage.textContent = '获取排行榜数据失败。';
}
}
function renderRankingsEditor(rankings) {
rankingsEditor.innerHTML = ''; // Clear previous content
const rankingTypes = {
'wealth': '财富榜',
'pvp': 'PVP 榜',
'island_level': '空岛等级'
};
for (const type in rankings) {
if (rankings.hasOwnProperty(type)) {
const tabContent = document.createElement('div');
tabContent.id = `tab-${type}`;
tabContent.className = 'tab-content';
if (type !== 'wealth') tabContent.style.display = 'none';
const typeLabel = document.createElement('h3');
typeLabel.textContent = rankingTypes[type] || type;
tabContent.appendChild(typeLabel);
rankings[type].forEach((entry, index) => {
const entryDiv = document.createElement('div');
entryDiv.className = 'ranking-entry';
entryDiv.innerHTML = `
<div>
<label>玩家ID:</label>
<input type="text" data-type="${type}" data-field="id" value="${entry.id}" data-index="${index}">
</div>
<div>
<label>名称:</label>
<input type="text" data-type="${type}" data-field="name" value="${entry.name}" data-index="${index}">
</div>
<div>
<label>值:</label>
<input type="number" data-type="${type}" data-field="value" value="${entry.value}" data-index="${index}">
</div>
<button class="btn btn-danger remove-ranking-entry" data-type="${type}" data-index="${index}">删除</button>
`;
tabContent.appendChild(entryDiv);
});
const addEntryBtn = document.createElement('button');
addEntryBtn.className = 'btn btn-primary';
addEntryBtn.textContent = `添加新的 ${rankingTypes[type]} 记录`;
addEntryBtn.onclick = () => addRankingEntry(type);
tabContent.appendChild(addEntryBtn);
rankingsEditor.appendChild(tabContent);
}
}
// Add event listeners for input changes
rankingsEditor.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (e) => {
const { type, field, index } = e.target.dataset;
currentRankingsData[type][index][field] = field === 'value' ? parseInt(e.target.value) : e.target.value;
});
});
// Add event listeners for remove buttons
rankingsEditor.querySelectorAll('.remove-ranking-entry').forEach(button => {
button.addEventListener('click', (e) => {
const { type, index } = e.target.dataset;
currentRankingsData[type].splice(parseInt(index), 1);
renderRankingsEditor(currentRankingsData); // Re-render to update indices
});
});
// Set up tabs functionality
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.style.display = 'none');
document.getElementById(`tab-${button.dataset.tab}`).style.display = 'block';
});
});
}
function addRankingEntry(type) {
currentRankingsData[type].push({ id: '', name: '', value: 0 });
renderRankingsEditor(currentRankingsData);
}
saveRankingsBtn.addEventListener('click', async () => {
rankingsMessage.textContent = '正在保存...';
rankingsMessage.className = 'message';
const token = getAuthToken();
if (!token) return;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/rankings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ rankings: currentRankingsData })
});
const data = await response.json();
if (data.success) {
rankingsMessage.textContent = '排行榜保存成功!';
rankingsMessage.className = 'message success-message';
} else {
rankingsMessage.textContent = data.message;
rankingsMessage.className = 'message error-message';
}
} catch (error) {
console.error('保存排行榜失败:', error);
rankingsMessage.textContent = '保存失败,请稍后再试。';
rankingsMessage.className = 'message error-message';
}
});
// --- 商城产品管理 ---
async function fetchAdminShopProducts() {
shopMessage.textContent = '';
const token = getAuthToken();
if (!token) {
showSection('login-section');
return;
}
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/shop/products`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 403) {
removeAuthToken();
showSection('login-section');
shopMessage.textContent = '会话过期,请重新登录。';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
currentShopProductsData = await response.json();
renderShopProductsEditor(currentShopProductsData);
} catch (error) {
console.error('获取商城产品失败:', error);
shopMessage.textContent = '获取商城产品失败。';
}
}
function renderShopProductsEditor(products) {
shopEditor.innerHTML = '';
products.forEach((product, index) => {
const entryDiv = document.createElement('div');
entryDiv.className = 'shop-entry card';
entryDiv.innerHTML = `
<div>
<label>产品ID:</label>
<input type="text" data-field="id" value="${product.id}" data-index="${index}">
</div>
<div>
<label>名称:</label>
<input type="text" data-field="name" value="${product.name}" data-index="${index}">
</div>
<div>
<label>描述:</label>
<textarea data-field="description" data-index="${index}">${product.description}</textarea>
</div>
<div>
<label>价格:</label>
<input type="number" data-field="price" value="${product.price}" data-index="${index}">
</div>
<div>
<label>图标 (Font Awesome class):</label>
<input type="text" data-field="icon" value="${product.icon}" data-index="${index}">
</div>
<button class="btn btn-danger remove-shop-entry" data-index="${index}">删除</button>
`;
shopEditor.appendChild(entryDiv);
});
const addProductBtn = document.createElement('button');
addProductBtn.className = 'btn btn-primary';
addProductBtn.textContent = '添加新产品';
addProductBtn.onclick = addShopProduct;
shopEditor.appendChild(addProductBtn);
shopEditor.querySelectorAll('input, textarea').forEach(input => {
input.addEventListener('change', (e) => {
const { field, index } = e.target.dataset;
currentShopProductsData[index][field] = field === 'price' ? parseInt(e.target.value) : e.target.value;
});
});
shopEditor.querySelectorAll('.remove-shop-entry').forEach(button => {
button.addEventListener('click', (e) => {
const { index } = e.target.dataset;
currentShopProductsData.splice(parseInt(index), 1);
renderShopProductsEditor(currentShopProductsData);
});
});
}
function addShopProduct() {
currentShopProductsData.push({ id: '', name: '', description: '', price: 0, icon: 'fas fa-box' });
renderShopProductsEditor(currentShopProductsData);
}
saveShopBtn.addEventListener('click', async () => {
shopMessage.textContent = '正在保存...';
shopMessage.className = 'message';
const token = getAuthToken();
if (!token) return;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/shop/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ products: currentShopProductsData })
});
const data = await response.json();
if (data.success) {
shopMessage.textContent = '商城产品保存成功!';
shopMessage.className = 'message success-message';
} else {
shopMessage.textContent = data.message;
shopMessage.className = 'message error-message';
}
} catch (error) {
console.error('保存商城产品失败:', error);
shopMessage.textContent = '保存失败,请稍后再试。';
shopMessage.className = 'message error-message';
}
});
// --- 页面初始化 ---
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
// Initially render dashboard status if already logged in (token exists)
if (getAuthToken()) {
updateAdminServerStatus(window.currentMcStatus);
}
// Expose showSection to global scope for onclick events
window.showSection = showSection;
});
10. public/images/default-favicon.png (示例图标)
为了避免服务器状态图片加载失败时显示空白,你可以创建一个简单的默认图标(例如一个Minecraft方块的图片),并将其命名为 default-favicon.png 放在 public/images/ 目录下。
│ ├── index.html # 主网站页面
│ ├── admin.html # 简单的管理后台页面
│ ├── css/
│ │ ├── style.css # 主网站样式
│ │ └── admin.css # 管理后台样式
│ └── js/
│ ├── main.js # 主网站JS逻辑,包括WebSocket
│ └── admin.js # 管理后台JS逻辑
└── server/ # Python Flask 后端服务
├── app.py # Flask 主应用和API、WebSocket逻辑
├── db.json # 简单JSON文件作为“数据库”
└── requirements.txt # Python依赖
功能实现思路:
实时服务器状态:
后端 (server/app.py): 使用 python-mcstatus 库定时查询真实的Minecraft服务器状态(例如每5-10秒)。将最新状态存储起来,并通过 WebSocket 推送给所有连接的客户端。
前端 (public/js/main.js): 连接到后端的WebSocket,接收服务器状态更新,并实时更新页面上的玩家数量、MOTD等信息。
排行榜:
后端 (server/app.py): 提供API (/api/rankings) 读取 db.json 中的排行榜数据。
前端 (public/js/main.js): 页面加载时通过API获取排行榜数据并展示。
管理后台 (server/app.py, public/admin.html, public/js/admin.js): 实现简单的登录功能,并提供API来更新(模拟)排行榜数据。
赞助商城:
后端 (server/app.py): 提供API (/api/shop/products) 读取 db.json 中的商品数据。
前端 (public/js/main.js): 页面加载时通过API获取商品数据并展示,并提供模拟的“购买”交互(实际不会处理支付)。
管理后台: 提供API来管理商品列表。
管理后台 (演示):
一个独立的 admin.html 页面,包含简单的登录表单。
登录成功后,可以访问一个简易的仪表盘,展示一些服务器信息,并提供修改排行榜/商城数据的模拟接口。
文件内容:
1. server/requirements.txt
Flask==2.3.3
Flask-SocketIO==5.3.0
python-mcstatus==1.20.1
eventlet==0.33.3
Flask-Cors==3.0.10
simplejson==3.19.2
2. server/db.json (模拟数据库)
{
"admin_credentials": {
"username": "admin",
"password": "password"
},
"rankings": {
"wealth": [
{ "id": "p1", "name": "PlayerA", "value": 10000000 },
{ "id": "p2", "name": "PlayerB", "value": 8500000 },
{ "id": "p3", "name": "PlayerC", "value": 7200000 },
{ "id": "p4", "name": "PlayerD", "value": 6000000 },
{ "id": "p5", "name": "PlayerE", "value": 5500000 }
],
"pvp": [
{ "id": "p6", "name": "PvPKing", "value": 1200 },
{ "id": "p7", "name": "SlayerXYZ", "value": 950 },
{ "id": "p8", "name": "Warrior_N", "value": 880 },
{ "id": "p9", "name": "BladeMaster", "value": 700 },
{ "id": "p10", "name": "ShadowAssassin", "value": 650 }
],
"island_level": [
{ "id": "p11", "name": "SkyLord", "value": 500 },
{ "id": "p12", "name": "CloudDweller", "value": 480 },
{ "id": "p13", "name": "IslandMaster", "value": 450 },
{ "id": "p14", "name": "AirArchitect", "value": 400 },
{ "id": "p15", "name": "FloatingCity", "value": 380 }
]
},
"shop_products": [
{
"id": "vip_basic",
"name": "VIP 基础套餐",
"description": "享受飞行、经验加成等多项特权。",
"price": 50,
"icon": "fas fa-gem"
},
{
"id": "rare_pack",
"name": "稀有礼包",
"description": "内含珍稀道具、强力装备和独特装饰品。",
"price": 120,
"icon": "fas fa-box-open"
},
{
"id": "custom_title",
"name": "自定义称号",
"description": "选择一个属于你的独特称号,彰显个性。",
"price": 80,
"icon": "fas fa-star"
},
{
"id": "monthly_pass",
"name": "月度通行证",
"description": "每月专属奖励、特权和点券,持续享受。",
"price": 30,
"icon": "fas fa-calendar-alt"
}
]
}
3. server/app.py
import os
import json
import time
from threading import Thread
from flask import Flask, jsonify, request, send_from_directory
from flask_socketio import SocketIO, emit
from flask_cors import CORS
from mcstatus import JavaServer
import simplejson # Using simplejson for better JSON handling, especially if dealing with non-standard types
app = Flask(__name__, static_folder='../public', static_url_path='/')
app.config['SECRET_KEY'] = 'your_secret_key_here' # 生产环境请更换为强密钥
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
CORS(app) # 允许所有来源的CORS请求
# --- 配置 ---
MINECRAFT_SERVER_IP = "play.hypixel.net" # 替换为你的Minecraft服务器IP
MINECRAFT_SERVER_PORT = 25565
MC_STATUS_INTERVAL = 10 # 查询Minecraft服务器状态的间隔(秒)
# --- 简单 JSON 数据库 ---
DB_FILE = os.path.join(os.path.dirname(__file__), 'db.json')
def load_db():
if not os.path.exists(DB_FILE):
# 如果db.json不存在,创建一个空的或者默认的
default_db = {
"admin_credentials": {"username": "admin", "password": "password"},
"rankings": {
"wealth": [],
"pvp": [],
"island_level": []
},
"shop_products": []
}
save_db(default_db)
return default_db
with open(DB_FILE, 'r', encoding='utf-8') as f:
return simplejson.load(f)
def save_db(data):
with open(DB_FILE, 'w', encoding='utf-8') as f:
simplejson.dump(data, f, indent=2, ensure_ascii=False)
db = load_db()
# --- Minecraft 服务器状态查询 ---
current_mc_status = {
"online": False,
"players": {"online": 0, "max": 0, "list": []},
"version": "Unknown",
"motd": {"clean": ["服务器状态加载中..."]},
"favicon": ""
}
def get_minecraft_server_status():
global current_mc_status
try:
server = JavaServer.lookup(f"{MINECRAFT_SERVER_IP}:{MINECRAFT_SERVER_PORT}")
status = server.status()
current_mc_status = {
"online": True,
"players": {
"online": status.players.online,
"max": status.players.max,
"list": [{"name": p.name, "uuid": p.id} for p in status.players.sample] if status.players.sample else []
},
"version": status.version.name,
"motd": {"html": status.motd.html.split('\n'), "clean": status.motd.clean.split('\n')},
"favicon": status.favicon if status.favicon else ""
}
print(f"Minecraft server {MINECRAFT_SERVER_IP} is online. Players: {status.players.online}/{status.players.max}")
except Exception as e:
current_mc_status = {
"online": False,
"players": {"online": 0, "max": 0, "list": []},
"version": "Unknown",
"motd": {"clean": ["服务器离线或无法访问。"]},
"favicon": ""
}
print(f"Failed to get Minecraft server status: {e}")
# 定时更新Minecraft服务器状态并广播
def mc_status_updater():
while True:
get_minecraft_server_status()
socketio.emit('mc_status_update', current_mc_status, namespace='/')
socketio.sleep(MC_STATUS_INTERVAL)
# 启动状态更新线程
status_thread = Thread(target=mc_status_updater)
status_thread.daemon = True
status_thread.start()
# --- 路由 ---
@app.route('/')
def serve_index():
return send_from_directory(app.static_folder, 'index.html')
@app.route('/admin')
def serve_admin():
return send_from_directory(app.static_folder, 'admin.html')
# API: 获取Minecraft服务器最新状态
@app.route('/api/mc_status')
def api_mc_status():
return jsonify(current_mc_status)
# API: 获取排行榜数据
@app.route('/api/rankings')
def api_rankings():
return jsonify(db['rankings'])
# API: 获取商城产品
@app.route('/api/shop/products')
def api_shop_products():
return jsonify(db['shop_products'])
# API: 模拟购买 (这里只是简单的返回成功,不涉及真实逻辑)
@app.route('/api/shop/purchase', methods=['POST'])
def api_shop_purchase():
data = request.json
product_id = data.get('product_id')
# 可以在这里添加一些日志或更复杂的模拟逻辑
return jsonify({"message": f"成功购买产品: {product_id}", "success": True}), 200
# API: 管理后台登录
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
data = request.json
username = data.get('username')
password = data.get('password')
if username == db['admin_credentials']['username'] and password == db['admin_credentials']['password']:
# 实际应用中会生成一个JWT或Session
return jsonify({"message": "登录成功", "token": "mock_admin_token", "success": True}), 200
return jsonify({"message": "用户名或密码错误", "success": False}), 401
# API: 管理后台获取排行榜 (需要认证,这里简化为只检查token存在)
@app.route('/api/admin/rankings', methods=['GET'])
def admin_get_rankings():
# 模拟认证:检查请求头中是否有'Authorization'
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
return jsonify(db['rankings'])
# API: 管理后台更新排行榜 (需要认证)
@app.route('/api/admin/rankings', methods=['POST'])
def admin_update_rankings():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
# 实际项目中这里会有更复杂的校验
new_rankings = request.json.get('rankings')
if new_rankings:
db['rankings'] = new_rankings
save_db(db)
socketio.emit('rankings_update', new_rankings, namespace='/') # 实时通知前端更新
return jsonify({"message": "排行榜更新成功", "success": True}), 200
return jsonify({"message": "数据无效", "success": False}), 400
# API: 管理后台获取商城产品 (需要认证)
@app.route('/api/admin/shop/products', methods=['GET'])
def admin_get_shop_products():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
return jsonify(db['shop_products'])
# API: 管理后台更新商城产品 (需要认证)
@app.route('/api/admin/shop/products', methods=['POST'])
def admin_update_shop_products():
if 'Authorization' not in request.headers:
return jsonify({"message": "未经授权", "success": False}), 403
new_products = request.json.get('products')
if new_products:
db['shop_products'] = new_products
save_db(db)
socketio.emit('shop_products_update', new_products, namespace='/') # 实时通知前端更新
return jsonify({"message": "商城产品更新成功", "success": True}), 200
return jsonify({"message": "数据无效", "success": False}), 400
# --- SocketIO 事件 ---
@socketio.on('connect')
def test_connect():
print('Client connected')
emit('mc_status_update', current_mc_status) # 新连接时立即发送当前状态
@socketio.on('disconnect')
def test_disconnect():
print('Client disconnected')
if __name__ == '__main__':
socketio.run(app, debug=True, port=5000)
4. public/index.html (主网站页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX Minecraft 服务器官网 - 体验极致方块世界</title>
<meta name="description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta name="keywords" content="Minecraft, 服务器, 多人游戏, 生存, RPG, 空岛, PVP, 赞助, 排行榜, 活动">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="http://localhost:5000/"> <!-- 请更新为你的实际域名 -->
<meta property="og:title" content="XXX Minecraft 服务器官网 - 体验极致方块世界">
<meta property="og:description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta property="og:image" content="http://localhost:5000/images/og-image.jpg"> <!-- 请更新为你的实际域名 -->
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="http://localhost:5000/"> <!-- 请更新为你的实际域名 -->
<meta property="twitter:title" content="XXX Minecraft 服务器官网 - 体验极致方块世界">
<meta property="twitter:description" content="XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。加入我们,探索无限可能!">
<meta property="twitter:image" content="http://localhost:5000/images/twitter-image.jpg"> <!-- 请更新为你的实际域名 -->
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/style.css">
<!-- Socket.IO Client -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
<!-- 结构化数据 (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "XXX Minecraft 服务器官网",
"url": "http://localhost:5000/",
"description": "XXX Minecraft 服务器官方网站,提供服务器特色、玩法、活动、排行榜、赞助商城等一站式服务。",
"potentialAction": {
"@type": "SearchAction",
"target": "http://localhost:5000/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
</head>
<body>
<header>
<div class="container" style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<a href="#home" class="logo">XXX MC</a>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#status">服务器状态</a></li>
<li><a href="#features">特色玩法</a></li>
<li><a href="#rankings">荣耀榜</a></li>
<li><a href="#shop">赞助商城</a></li>
<li><a href="#community">社区</a></li>
</ul>
</nav>
<button id="theme-toggle" class="theme-switcher">
<i class="fas fa-sun"></i> <span>亮色主题</span>
</button>
</div>
</header>
<main>
<section id="home" class="hero">
<h1>欢迎来到 XXX Minecraft 服务器</h1>
<p>探索无限的方块世界,与志同道合的朋友一同创造、冒险、竞技!我们提供稳定、流畅、充满创意的游戏体验。</p>
<a href="javascript:void(0);" onclick="copyIP('play.xxx-mc.com')" class="btn-primary">
<i class="fas fa-copy"></i> 一键复制服务器 IP: play.xxx-mc.com
</a>
<p id="hero-online-status">
<span id="hero-status-indicator" class="status-indicator status-unknown"></span>
当前在线人数: <span id="hero-online-players">加载中...</span> / <span id="hero-max-players">加载中...</span>
</p>
</section>
<section id="status" class="container">
<h2>服务器实时状态</h2>
<div class="server-status-card">
<div class="status-header">
<img id="server-favicon" src="/images/default-favicon.png" alt="服务器图标"> <!-- 默认图标 -->
<div>
<h3>XXX Minecraft Server</h3>
<p id="server-address" style="font-size: 0.9rem; color: var(--text-color);">连接地址: play.xxx-mc.com</p>
<p style="font-weight: bold;">
<span id="server-main-indicator" class="status-indicator status-unknown"></span>
<span id="server-status-text">加载中...</span>
</p>
</div>
</div>
<p id="server-motd" style="color: var(--text-color); margin-bottom: 20px; font-style: italic;">加载服务器消息...</p>
<div class="status-details">
<div class="status-item">
<strong><span id="status-online-players">?</span></strong>
<span>在线玩家</span>
</div>
<div class="status-item">
<strong><span id="status-max-players">?</span></strong>
<span>最大容量</span>
</div>
<div class="status-item">
<strong><span id="status-version">?</span></strong>
<span>服务器版本</span>
</div>
<!-- 延迟信息通常需要客户端直接ping,后端API不直接提供,这里暂时不显示,或保持为占位符 -->
<div class="status-item">
<strong><span id="status-latency">N/A</span></strong>
<span>延迟</span>
</div>
</div>
<h4 style="margin-top: 30px; margin-bottom: 15px; color: var(--primary-color);">在线玩家列表 (最多显示 10 名):</h4>
<div id="player-list" class="player-avatars">
<!-- Player avatars will be loaded here by JavaScript -->
<span id="player-list-placeholder">加载玩家中...</span>
</div>
</div>
</section>
<section id="features" class="container">
<h2>服务器特色与玩法</h2>
<div class="features-grid">
<div class="feature-item">
<i class="fas fa-tree"></i>
<h3>生存模式</h3>
<p>原版生存体验,高版本特色,无乱七八糟的魔改,让你回归最纯粹的方块世界。</p>
</div>
<div class="feature-item">
<i class="fas fa-cloud-sun"></i>
<h3>空岛生存</h3>
<p>从一块浮空的岛屿开始,挑战资源有限的极限生存,打造你的空中帝国,与好友共建家园。</p>
</div>
<div class="feature-item">
<i class="fas fa-dragon"></i>
<h3>RPG 冒险</h3>
<p>丰富的剧情任务,独特的BOSS挑战,专属技能与装备,沉浸式角色扮演体验,等你来征服。</p>
</div>
<div class="feature-item">
<i class="fas fa-trophy"></i>
<h3>PVP 竞技</h3>
<p>设有PVP竞技场,公平公正的对决环境,考验你的操作与策略,争夺PVP王者宝座。</p>
</div>
<div class="feature-item">
<i class="fas fa-hat-wizard"></i>
<h3>独家插件</h3>
<p>多款独家定制插件,优化游戏体验,提供更多互动与便利功能,提升游戏乐趣。</p>
</div>
<div class="feature-item">
<i class="fas fa-users-cog"></i>
<h3>友好社区</h3>
<p>拥有活跃的玩家社区和专业的管理团队,为你解决游戏中遇到的任何问题,共同成长。</p>
</div>
</div>
</section>
<section id="rankings" class="container">
<h2>荣耀排行榜</h2>
<p>记录玩家们的辉煌成就,展现服务器内最强者的风采!</p>
<div id="rankings-content" class="rankings-grid">
<!-- Rankings will be loaded here by JavaScript -->
<p>加载排行榜中...</p>
</div>
<p style="margin-top: 40px; font-size: 1.1rem; color: var(--text-color);">排行榜数据每 24 小时更新一次。</p>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 30px;"><i class="fas fa-chart-bar"></i> 查看完整排行榜</a>
</section>
<section id="shop" class="container">
<h2>赞助商城</h2>
<p>感谢您的支持,赞助所得将用于服务器的持续运营和发展。获取专属VIP、特殊道具和独特头衔!</p>
<div id="shop-products-grid" class="shop-grid">
<!-- Shop products will be loaded here by JavaScript -->
<p>加载商城产品中...</p>
</div>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 50px;"><i class="fas fa-shopping-cart"></i> 前往赞助商城</a>
</section>
<section id="community" class="container">
<h2>加入我们的社区</h2>
<p>我们拥有活跃的社区,欢迎您加入官方QQ群、Discord服务器,与志同道合的玩家一同交流心得,分享乐趣。</p>
<div class="social-links" style="margin-top: 40px;">
<a href="#" target="_blank" title="QQ 群"><i class="fab fa-qq"></i></a>
<a href="#" target="_blank" title="Discord"><i class="fab fa-discord"></i></a>
<a href="#" target="_blank" title="Bilibili"><i class="fab fa-bilibili"></i></a>
<a href="#" target="_blank" title="GitHub"><i class="fab fa-github"></i></a>
</div>
<p style="margin-top: 40px;">遇到问题?您可以通过用户中心提交工单,获得管理团队的专业支持。</p>
<a href="javascript:void(0);" class="btn-primary" style="margin-top: 30px;"><i class="fas fa-user-circle"></i> 前往用户中心</a>
</section>
</main>
<footer>
<div class="container">
<p>© 2025 XXX Minecraft Server. All rights reserved.</p>
<p>联系我们: <a href="mailto:support@xxx-mc.com" style="color: var(--primary-color); text-decoration: none;">support@xxx-mc.com</a></p>
<p><a href="/admin" style="color: var(--primary-color); text-decoration: none;">管理后台 (演示)</a></p>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
5. public/css/style.css
/* CSS 变量用于主题切换 */
:root {
--background-color: #f8fafc; /* light grey */
--text-color: #2c3e50; /* dark blue */
--primary-color: #34d399; /* emerald green */
--primary-dark-color: #10b981; /* darker emerald */
--secondary-bg-color: #ffffff; /* white */
--border-color: #e2e8f0; /* light grey border */
--card-bg-color: #ffffff;
--card-shadow: rgba(0, 0, 0, 0.1);
--header-bg-color: #ffffff;
}
html.dark {
--background-color: #1a202c; /* dark grey */
--text-color: #e2e8f0; /* light grey text */
--primary-color: #059669; /* darker emerald for dark mode */
--primary-dark-color: #047857; /* even darker */
--secondary-bg-color: #2d3748; /* charcoal */
--border-color: #4a5568; /* grey border */
--card-bg-color: #2d3748;
--card-shadow: rgba(0, 0, 0, 0.5);
--header-bg-color: #2d3748;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
font-size: 16px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
header {
background-color: var(--header-bg-color);
padding: 1rem 0;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: sticky;
top: 0;
z-index: 1000;
}
header .logo {
font-size: 1.8rem;
font-weight: 800;
color: var(--primary-color);
text-decoration: none;
margin-right: 20px;
letter-spacing: -0.05em;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
nav a {
color: var(--text-color);
text-decoration: none;
font-weight: 500;
padding: 0.5rem 0.8rem;
border-radius: 6px;
transition: background-color 0.3s ease, color 0.3s ease;
}
nav a:hover {
background-color: var(--primary-color);
color: white;
}
.theme-switcher {
background: var(--secondary-bg-color);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 0.6rem 1rem;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
margin-left: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.theme-switcher:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.theme-switcher i {
font-size: 1.1rem;
}
.hero {
background: url('https://source.unsplash.com/random/1920x1080/?minecraft,fantasy,game') no-repeat center center/cover; /* 示例背景图 */
color: white;
text-align: center;
padding: 120px 20px;
min-height: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.65); /* 半透明遮罩 */
z-index: 0;
}
.hero * {
position: relative;
z-index: 1;
}
.hero h1 {
font-size: 3.8rem;
margin-bottom: 25px;
text-shadow: 2px 2px 6px rgba(0,0,0,0.8);
font-weight: 900;
letter-spacing: -0.05em;
}
.hero p {
font-size: 1.6rem;
margin-bottom: 40px;
max-width: 900px;
text-shadow: 1px 1px 4px rgba(0,0,0,0.7);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
padding: 16px 32px;
text-decoration: none;
border-radius: 8px;
font-size: 1.3rem;
font-weight: bold;
transition: background-color 0.3s ease, transform 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.8rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary:hover {
background-color: var(--primary-dark-color);
transform: translateY(-2px);
}
.btn-primary i {
font-size: 1.2rem;
}
#hero-online-status {
margin-top: 25px;
font-size: 1.2rem;
font-weight: 600;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 8px;
display: inline-block;
}
section {
padding: 90px 0;
text-align: center;
}
section:nth-of-type(even) {
background-color: var(--secondary-bg-color);
}
section h2 {
font-size: 3rem;
margin-bottom: 50px;
color: var(--primary-color);
font-weight: 800;
letter-spacing: -0.03em;
}
.features-grid, .rankings-grid, .activity-grid, .shop-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 40px;
}
.feature-item, .ranking-item, .activity-item, .shop-item {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 12px;
box-shadow: 0 6px 20px var(--card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
text-align: left;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.feature-item:hover, .ranking-item:hover, .activity-item:hover, .shop-item:hover {
transform: translateY(-8px);
box-shadow: 0 10px 30px var(--card-shadow);
}
.feature-item i, .ranking-item i, .activity-item i, .shop-item i {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 15px;
}
.feature-item h3, .ranking-item h3, .activity-item h3, .shop-item h3 {
font-size: 1.8rem;
margin-bottom: 15px;
color: var(--text-color);
font-weight: 700;
}
.feature-item p, .ranking-item p, .activity-item p, .shop-item p {
font-size: 1rem;
color: var(--text-color);
flex-grow: 1;
}
.ranking-item ul {
list-style: none;
padding: 0;
margin-top: 15px;
width: 100%;
}
.ranking-item li {
background-color: var(--secondary-bg-color);
padding: 8px 15px;
margin-bottom: 5px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
border: 1px solid var(--border-color);
}
.ranking-item li:nth-child(1) { background-color: rgba(255, 215, 0, 0.2); } /* Gold */
.ranking-item li:nth-child(2) { background-color: rgba(192, 192, 192, 0.2); } /* Silver */
.ranking-item li:nth-child(3) { background-color: rgba(205, 127, 50, 0.2); } /* Bronze */
.server-status-card {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 12px;
box-shadow: 0 6px 20px var(--card-shadow);
max-width: 800px;
margin: 50px auto;
text-align: left;
border: 1px solid var(--border-color);
}
.status-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.status-header img {
width: 80px;
height: 80px;
border-radius: 12px;
border: 2px solid var(--primary-color);
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-online { background-color: #22c55e; } /* green-500 */
.status-offline { background-color: #ef4444; } /* red-500 */
.status-unknown { background-color: #f59e0b; } /* amber-500 */
.status-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
}
.status-item {
font-size: 1rem;
color: var(--text-color);
text-align: center;
}
.status-item strong {
display: block;
font-size: 1.5rem;
color: var(--primary-color);
margin-bottom: 5px;
}
.player-avatars {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 25px;
justify-content: center;
border-top: 1px dashed var(--border-color);
padding-top: 20px;
}
.player-avatars img {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--primary-color);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.player-avatars img:hover {
transform: translateY(-2px) scale(1.1);
}
.player-avatars .more-players {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--secondary-bg-color);
color: var(--text-color);
font-size: 0.8rem;
border: 2px solid var(--border-color);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
footer {
background-color: var(--secondary-bg-color);
padding: 50px 0;
text-align: center;
color: var(--text-color);
border-top: 1px solid var(--border-color);
margin-top: 60px;
}
footer p {
margin-bottom: 10px;
font-size: 0.95rem;
}
.social-links {
margin-top: 20px;
margin-bottom: 30px;
}
.social-links a {
color: var(--text-color);
margin: 0 12px;
font-size: 1.8rem;
text-decoration: none;
transition: color 0.3s ease, transform 0.2s ease;
}
.social-links a:hover {
color: var(--primary-color);
transform: translateY(-3px);
}
.disclaimer {
font-size: 0.85rem;
color: #888;
margin-top: 40px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
}
@Media (max-width: 992px) {
header nav {
order: 2; /* Move nav below logo and theme switcher */
flex-basis: 100%;
margin-top: 1rem;
justify-content: center;
}
header .logo, .theme-switcher {
margin: 0 auto;
}
.hero h1 {
font-size: 3rem;
}
.hero p {
font-size: 1.3rem;
}
section h2 {
font-size: 2.5rem;
}
}
@Media (max-width: 768px) {
.container {
padding: 0 1rem;
}
header {
flex-direction: column;
align-items: center;
}
nav ul {
flex-direction: column;
gap: 0.8rem;
margin-top: 1.5rem;
}
.theme-switcher {
margin-top: 1.5rem;
margin-left: 0;
}
.hero {
padding: 80px 15px;
min-height: 500px;
}
.hero h1 {
font-size: 2.5rem;
}
.hero p {
font-size: 1.1rem;
margin-bottom: 30px;
}
.btn-primary {
padding: 12px 25px;
font-size: 1.1rem;
}
#hero-online-status {
font-size: 1rem;
}
section {
padding: 60px 0;
}
section h2 {
font-size: 2rem;
margin-bottom: 40px;
}
.features-grid, .rankings-grid, .activity-grid, .shop-grid {
grid-template-columns: 1fr;
}
.feature-item, .ranking-item, .activity-item, .shop-item {
padding: 25px;
}
.server-status-card {
margin: 30px auto;
padding: 25px;
}
.status-header {
flex-direction: column;
text-align: center;
}
.status-header img {
width: 60px;
height: 60px;
}
.player-avatars {
justify-content: center;
}
.status-details {
grid-template-columns: 1fr 1fr;
}
}
6. public/js/main.js
const BACKEND_URL = 'http://127.0.0.1:5000'; // 后端服务地址
const SOCKET_IO_URL = BACKEND_URL;
const CRAVATAR_BASE_URL = 'https://cravatar.cn/avatar/';
// --- 主题切换逻辑 ---
const themeToggleBtn = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
const themeIcon = themeToggleBtn.querySelector('i');
const themeText = themeToggleBtn.querySelector('span');
function updateThemeUI(isDark) {
if (isDark) {
htmlElement.classList.add('dark');
themeIcon.classList.remove('fa-sun');
themeIcon.classList.add('fa-moon');
themeText.textContent = '亮色主题';
} else {
htmlElement.classList.remove('dark');
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
themeText.textContent = '暗色主题';
}
}
// 初始化主题
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let currentTheme = localStorage.getItem('theme');
if (currentTheme === null) {
currentTheme = prefersDark ? 'dark' : 'light';
}
updateThemeUI(currentTheme === 'dark');
themeToggleBtn.addEventListener('click', () => {
const isDark = htmlElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'light' : 'dark');
updateThemeUI(!isDark);
});
// --- 复制服务器 IP 逻辑 ---
function copyIP(ip) {
navigator.clipboard.writeText(ip).then(() => {
alert('服务器 IP ' + ip + ' 已复制到剪贴板!');
}).catch(err => {
console.error('无法复制 IP: ', err);
prompt('复制失败,请手动复制:', ip); // Fallback for browsers without clipboard API
});
}
// 将 copyIP 暴露给全局,以便 HTML 中的 onclick 能够调用
window.copyIP = copyIP;
// --- Minecraft 服务器实时状态 (通过 WebSocket) ---
const socket = io(SOCKET_IO_URL);
socket.on('connect', () => {
console.log('Connected to WebSocket server');
});
socket.on('disconnect', () => {
console.log('Disconnected from WebSocket server');
});
socket.on('mc_status_update', (data) => {
console.log('Received MC status update:', data);
updateServerStatusUI(data);
});
function updateServerStatusUI(data) {
const isOnline = data.online;
// 更新 Hero Section 的状态
document.getElementById('hero-online-players').textContent = isOnline ? (data.players?.online || 0) : '0';
document.getElementById('hero-max-players').textContent = isOnline ? (data.players?.max || 0) : '0';
document.getElementById('hero-status-indicator').className = `status-indicator ${isOnline ? 'status-online' : 'status-offline'}`;
// 更新详细状态卡片
document.getElementById('server-favicon').src = data.favicon || '/images/default-favicon.png'; // Fallback to a local default image
document.getElementById('server-main-indicator').className = `status-indicator ${isOnline ? 'status-online' : 'status-offline'}`;
document.getElementById('server-status-text').textContent = isOnline ? '在线' : '离线';
// Clean MOTD: Remove any HTML tags that might be in the raw motd
const motdHtml = isOnline ? (data.motd?.html?.join('<br>') || data.motd?.clean?.join('<br>') || '暂无消息') : '服务器当前离线或无法访问。';
document.getElementById('server-motd').innerHTML = motdHtml.replace(/§[0-9a-fk-or]/gi, ''); // Remove Minecraft color codes
document.getElementById('status-online-players').textContent = isOnline ? (data.players?.online || 0) : '0';
document.getElementById('status-max-players').textContent = isOnline ? (data.players?.max || 0) : '0';
document.getElementById('status-version').textContent = isOnline ? (data.version || '未知') : 'N/A';
// Latency is hard to get reliably from server-side query, keep N/A or remove if not available
// document.getElementById('status-latency').textContent = isOnline ? (data.latency || '?') : '?';
// 更新玩家列表
const playerListDiv = document.getElementById('player-list');
playerListDiv.innerHTML = ''; // Clear previous players
if (isOnline && data.players?.list && data.players.list.length > 0) {
const playersToShow = data.players.list.slice(0, 10); // Show max 10 players
playersToShow.forEach(player => {
const img = document.createElement('img');
img.src = `${CRAVATAR_BASE_URL}${player.uuid}?s=36&d=identicon`;
img.alt = player.name;
img.title = player.name; // Tooltip on hover
playerListDiv.appendChild(img);
});
if (data.players.list.length > 10) {
const morePlayersSpan = document.createElement('span');
morePlayersSpan.className = 'more-players';
morePlayersSpan.textContent = `+${data.players.list.length - 10}`;
morePlayersSpan.title = `${data.players.list.length - 10} 更多玩家`;
playerListDiv.appendChild(morePlayersSpan);
}
} else {
playerListDiv.innerHTML = '<span id="player-list-placeholder">当前无在线玩家。</span>';
}
}
// --- 获取和显示排行榜数据 ---
async function fetchRankings() {
try {
const response = await fetch(`${BACKEND_URL}/api/rankings`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const rankings = await response.json();
renderRankings(rankings);
} catch (error) {
console.error('获取排行榜数据失败:', error);
document.getElementById('rankings-content').innerHTML = '<p>无法加载排行榜数据。</p>';
}
}
function renderRankings(rankings) {
const rankingsContent = document.getElementById('rankings-content');
rankingsContent.innerHTML = ''; // Clear previous content
const rankingTypes = {
'wealth': { icon: 'fas fa-money-bill-wave', title: '财富榜', unit: '
'pvp': { icon: 'fas fa-fist-raised', title: 'PVP 榜', unit: '
'island_level': { icon: 'fas fa-building', title: '空岛等级', unit: '
};
for (const type in rankings) {
if (rankings.hasOwnProperty(type) && rankingTypes[type]) {
const item = rankingTypes[type];
const rankingList = rankings[type];
const rankingItemDiv = document.createElement('div');
rankingItemDiv.className = 'ranking-item';
rankingItemDiv.innerHTML = `
<i class="${item.icon}"></i>
<h3>${item.title}</h3>
<p>${item.title === '财富榜' ? '服务器内经济巨头排名,展示你的财富实力,谁是方块世界的首富?' :
item.title === 'PVP 榜' ? '竞技场王者,击杀数与胜率的巅峰对决,每一次战斗都为了荣耀!' :
'空岛规模与发展程度的象征,谁是天空霸主?你的空岛由你定义。'}</p>
<ul>
${rankingList.map((player, index) => `
<li>
<i class="fas fa-award" style="color: ${index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? '#CD7F32' : 'var(--text-color)'};"></i>
${index + 1}. ${player.name} - <span style="color: var(--primary-color);">${item.unit} ${player.value}</span>
</li>
`).join('')}
</ul>
`;
rankingsContent.appendChild(rankingItemDiv);
}
}
}
// 监听排行榜数据更新的WebSocket事件
socket.on('rankings_update', (data) => {
console.log('Received rankings update:', data);
renderRankings(data);
});
// --- 获取和显示赞助商城产品数据 ---
async function fetchShopProducts() {
try {
const response = await fetch(`${BACKEND_URL}/api/shop/products`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const products = await response.json();
renderShopProducts(products);
} catch (error) {
console.error('获取商城产品数据失败:', error);
document.getElementById('shop-products-grid').innerHTML = '<p>无法加载商城产品。</p>';
}
}
function renderShopProducts(products) {
const shopProductsGrid = document.getElementById('shop-products-grid');
shopProductsGrid.innerHTML = ''; // Clear previous content
if (products.length === 0) {
shopProductsGrid.innerHTML = '<p>暂无产品上架。</p>';
return;
}
products.forEach(product => {
const shopItemDiv = document.createElement('div');
shopItemDiv.className = 'shop-item';
shopItemDiv.innerHTML = `
<i class="${product.icon}"></i>
<h3>${product.name}</h3>
<p>${product.description}</p>
<a href="javascript:void(0);" onclick="simulatePurchase('${product.id}', '${product.name}')" class="btn-primary" style="margin-top: auto; padding: 10px 20px; font-size: 1rem;">
购买 - ¥${product.price}
</a>
`;
shopProductsGrid.appendChild(shopItemDiv);
});
}
// 模拟购买功能 (仅客户端提示)
async function simulatePurchase(productId, productName) {
alert(`你点击了购买 ${productName} (ID: ${productId}),实际支付功能需要后端集成支付网关。`);
// 模拟向后端发送购买请求
try {
const response = await fetch(`${BACKEND_URL}/api/shop/purchase`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ product_id: productId })
});
const result = await response.json();
if (result.success) {
console.log(result.message);
// 可以在这里更新UI,例如显示“购买成功”的通知
} else {
console.error('模拟购买失败:', result.message);
}
} catch (error) {
console.error('模拟购买请求发送失败:', error);
}
}
window.simulatePurchase = simulatePurchase; // Expose to global scope for onclick
// 监听商城产品数据更新的WebSocket事件
socket.on('shop_products_update', (data) => {
console.log('Received shop products update:', data);
renderShopProducts(data);
});
// --- 页面加载时执行 ---
document.addEventListener('DOMContentLoaded', () => {
fetchRankings(); // 加载排行榜
fetchShopProducts(); // 加载商城产品
});
7. public/admin.html (管理后台页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX Minecraft 服务器管理后台 (演示)</title>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/css/admin.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
</head>
<body>
<div class="container">
<header>
<h1 class="logo">XXX MC 管理后台</h1>
<button id="logout-button" class="btn btn-secondary" style="display: none;">注销</button>
</header>
<main id="admin-main">
<!-- 登录表单 -->
<section id="login-section" class="card">
<h2>管理员登录</h2>
<form id="login-form">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">登录</button>
<p id="login-message" class="message error-message"></p>
</form>
</section>
<!-- 仪表盘 -->
<section id="dashboard-section" class="dashboard" style="display: none;">
<h2>仪表盘</h2>
<div class="dashboard-grid">
<div class="card status-card">
<h3>服务器状态</h3>
<p>状态: <span id="admin-server-status">加载中...</span></p>
<p>在线玩家: <span id="admin-online-players">0</span> / <span id="admin-max-players">0</span></p>
<p>版本: <span id="admin-server-version">N/A</span></p>
</div>
<div class="card data-card">
<h3>排行榜管理</h3>
<p>在此处查看和编辑排行榜数据。</p>
<button class="btn btn-primary" onclick="showSection('edit-rankings-section')">编辑排行榜</button>
</div>
<div class="card data-card">
<h3>商城产品管理</h3>
<p>在此处查看和编辑商城产品。</p>
<button class="btn btn-primary" onclick="showSection('edit-shop-section')">编辑商城产品</button>
</div>
</div>
</section>
<!-- 编辑排行榜 -->
<section id="edit-rankings-section" class="card edit-section" style="display: none;">
<h2>编辑排行榜</h2>
<div class="tabs">
<button class="tab-button active" data-tab="wealth">财富榜</button>
<button class="tab-button" data-tab="pvp">PVP 榜</button>
<button class="tab-button" data-tab="island_level">空岛等级</button>
</div>
<div id="rankings-editor">
<!-- Dynamic ranking editor will be loaded here -->
</div>
<button class="btn btn-primary" id="save-rankings-btn">保存排行榜</button>
<button class="btn btn-secondary" onclick="showSection('dashboard-section')">返回仪表盘</button>
<p id="rankings-message" class="message"></p>
</section>
<!-- 编辑商城产品 -->
<section id="edit-shop-section" class="card edit-section" style="display: none;">
<h2>编辑商城产品</h2>
<div id="shop-editor">
<!-- Dynamic shop editor will be loaded here -->
</div>
<button class="btn btn-primary" id="save-shop-btn">保存商城产品</button>
<button class="btn btn-secondary" onclick="showSection('dashboard-section')">返回仪表盘</button>
<p id="shop-message" class="message"></p>
</section>
</main>
</div>
<script src="/js/admin.js"></script>
</body>
</html>
8. public/css/admin.css
:root {
--primary-color: #34d399;
--primary-dark-color: #10b981;
--secondary-color: #607d8b;
--background-color: #f0f2f5;
--card-bg-color: #ffffff;
--text-color: #333;
--border-color: #e0e0e0;
--shadow-color: rgba(0, 0, 0, 0.1);
--error-color: #ef4444;
--success-color: #22c55e;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 40px auto;
padding: 20px;
background-color: var(--background-color);
border-radius: 8px;
box-shadow: 0 4px 20px var(--shadow-color);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
margin-bottom: 30px;
border-bottom: 1px solid var(--border-color);
}
.logo {
font-size: 2rem;
color: var(--primary-color);
font-weight: bold;
margin: 0;
}
h2 {
color: var(--primary-color);
margin-bottom: 25px;
font-size: 1.8rem;
text-align: center;
}
.card {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
margin-bottom: 30px;
border: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-color);
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="number"],
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: var(--primary-color);
outline: none;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark-color);
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
margin-left: 10px;
}
.btn-secondary:hover {
background-color: #546a79;
transform: translateY(-1px);
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 5px;
font-weight: 500;
}
.error-message {
background-color: rgba(239, 68, 68, 0.1);
color: var(--error-color);
}
.success-message {
background-color: rgba(34, 197, 94, 0.1);
color: var(--success-color);
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.dashboard-grid .card {
margin-bottom: 0; /* Remove extra margin for grid items */
}
.dashboard-grid .status-card h3,
.dashboard-grid .data-card h3 {
color: var(--text-color);
margin-bottom: 15px;
text-align: left;
}
.dashboard-grid .status-card p {
text-align: left;
margin-bottom: 8px;
}
.dashboard-grid .status-card span {
font-weight: bold;
color: var(--primary-dark-color);
}
/* Tabs for ranking/shop editing */
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 1rem;
color: var(--text-color);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.tab-button.active {
color: var(--primary-color);
border-color: var(--primary-color);
font-weight: bold;
}
.tab-button:hover:not(.active) {
color: var(--primary-dark-color);
}
.ranking-entry, .shop-entry {
background-color: var(--background-color);
padding: 15px;
border-radius: 6px;
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
border: 1px solid var(--border-color);
}
.ranking-entry div, .shop-entry div {
flex: 1 1 auto;
min-width: 120px;
}
.ranking-entry input, .shop-entry input, .shop-entry textarea {
width: 100%;
}
.ranking-entry .btn-danger, .shop-entry .btn-danger {
flex: 0 0 auto;
align-self: center;
}
/* Specific styling for shop form */
.shop-entry .form-group {
margin-bottom: 0;
}
.shop-entry .form-group input, .shop-entry .form-group textarea {
min-width: unset; /* Override min-width if needed */
}
@Media (max-width: 768px) {
.container {
margin: 20px auto;
padding: 15px;
}
header {
flex-direction: column;
align-items: flex-start;
padding-bottom: 15px;
margin-bottom: 20px;
}
.logo {
margin-bottom: 10px;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.btn-secondary {
margin-left: 0;
margin-top: 10px;
}
.ranking-entry, .shop-entry {
flex-direction: column;
align-items: stretch;
}
}
9. public/js/admin.js
const ADMIN_BACKEND_URL = 'http://127.0.0.1:5000'; // 后端服务地址
const ADMIN_SOCKET_IO_URL = ADMIN_BACKEND_URL;
const ADMIN_TOKEN_KEY = 'admin_auth_token';
// --- DOM 元素 ---
const loginSection = document.getElementById('login-section');
const dashboardSection = document.getElementById('dashboard-section');
const editRankingsSection = document.getElementById('edit-rankings-section');
const editShopSection = document.getElementById('edit-shop-section');
const loginForm = document.getElementById('login-form');
const loginMessage = document.getElementById('login-message');
const logoutButton = document.getElementById('logout-button');
const rankingsEditor = document.getElementById('rankings-editor');
const shopEditor = document.getElementById('shop-editor');
const saveRankingsBtn = document.getElementById('save-rankings-btn');
const saveShopBtn = document.getElementById('save-shop-btn');
const rankingsMessage = document.getElementById('rankings-message');
const shopMessage = document.getElementById('shop-message');
const socket = io(ADMIN_SOCKET_IO_URL);
let currentRankingsData = {};
let currentShopProductsData = [];
// --- 辅助函数:显示/隐藏区域 ---
function showSection(sectionId) {
loginSection.style.display = 'none';
dashboardSection.style.display = 'none';
editRankingsSection.style.display = 'none';
editShopSection.style.display = 'none';
document.getElementById(sectionId).style.display = 'block';
// 如果是编辑排行榜或商城,需要重新加载数据
if (sectionId === 'edit-rankings-section') {
fetchAdminRankings();
} else if (sectionId === 'edit-shop-section') {
fetchAdminShopProducts();
}
}
// --- 认证相关 ---
function saveAuthToken(token) {
localStorage.setItem(ADMIN_TOKEN_KEY, token);
logoutButton.style.display = 'block';
}
function getAuthToken() {
return localStorage.getItem(ADMIN_TOKEN_KEY);
}
function removeAuthToken() {
localStorage.removeItem(ADMIN_TOKEN_KEY);
logoutButton.style.display = 'none';
}
async function checkAuth() {
const token = getAuthToken();
if (token) {
// 实际应用中会验证token的有效性
showSection('dashboard-section');
} else {
showSection('login-section');
}
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
loginMessage.textContent = '';
const username = e.target.username.value;
const password = e.target.password.value;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
saveAuthToken(data.token);
showSection('dashboard-section');
updateAdminServerStatus(window.currentMcStatus); // 登录成功后更新状态
} else {
loginMessage.textContent = data.message;
}
} catch (error) {
console.error('登录请求失败:', error);
loginMessage.textContent = '登录失败,请稍后再试。';
}
});
logoutButton.addEventListener('click', () => {
removeAuthToken();
showSection('login-section');
});
// --- 管理后台服务器状态 (通过 WebSocket) ---
window.currentMcStatus = null; // 存储最新MC状态
socket.on('connect', () => {
console.log('Admin connected to WebSocket server');
});
socket.on('disconnect', () => {
console.log('Admin disconnected from WebSocket server');
});
socket.on('mc_status_update', (data) => {
window.currentMcStatus = data; // 保存最新状态
if (getAuthToken()) { // 只有登录后才更新仪表盘上的状态
updateAdminServerStatus(data);
}
});
function updateAdminServerStatus(data) {
if (!data) {
document.getElementById('admin-server-status').textContent = '无法获取';
document.getElementById('admin-online-players').textContent = 'N/A';
document.getElementById('admin-max-players').textContent = 'N/A';
document.getElementById('admin-server-version').textContent = 'N/A';
return;
}
document.getElementById('admin-server-status').textContent = data.online ? '在线' : '离线';
document.getElementById('admin-server-status').style.color = data.online ? 'var(--success-color)' : 'var(--error-color)';
document.getElementById('admin-online-players').textContent = data.players?.online || 0;
document.getElementById('admin-max-players').textContent = data.players?.max || 0;
document.getElementById('admin-server-version').textContent = data.version || '未知';
}
// --- 排行榜管理 ---
async function fetchAdminRankings() {
rankingsMessage.textContent = '';
const token = getAuthToken();
if (!token) {
showSection('login-section');
return;
}
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/rankings`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 403) {
removeAuthToken();
showSection('login-section');
rankingsMessage.textContent = '会话过期,请重新登录。';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
currentRankingsData = await response.json();
renderRankingsEditor(currentRankingsData);
} catch (error) {
console.error('获取排行榜数据失败:', error);
rankingsMessage.textContent = '获取排行榜数据失败。';
}
}
function renderRankingsEditor(rankings) {
rankingsEditor.innerHTML = ''; // Clear previous content
const rankingTypes = {
'wealth': '财富榜',
'pvp': 'PVP 榜',
'island_level': '空岛等级'
};
for (const type in rankings) {
if (rankings.hasOwnProperty(type)) {
const tabContent = document.createElement('div');
tabContent.id = `tab-${type}`;
tabContent.className = 'tab-content';
if (type !== 'wealth') tabContent.style.display = 'none';
const typeLabel = document.createElement('h3');
typeLabel.textContent = rankingTypes[type] || type;
tabContent.appendChild(typeLabel);
rankings[type].forEach((entry, index) => {
const entryDiv = document.createElement('div');
entryDiv.className = 'ranking-entry';
entryDiv.innerHTML = `
<div>
<label>玩家ID:</label>
<input type="text" data-type="${type}" data-field="id" value="${entry.id}" data-index="${index}">
</div>
<div>
<label>名称:</label>
<input type="text" data-type="${type}" data-field="name" value="${entry.name}" data-index="${index}">
</div>
<div>
<label>值:</label>
<input type="number" data-type="${type}" data-field="value" value="${entry.value}" data-index="${index}">
</div>
<button class="btn btn-danger remove-ranking-entry" data-type="${type}" data-index="${index}">删除</button>
`;
tabContent.appendChild(entryDiv);
});
const addEntryBtn = document.createElement('button');
addEntryBtn.className = 'btn btn-primary';
addEntryBtn.textContent = `添加新的 ${rankingTypes[type]} 记录`;
addEntryBtn.onclick = () => addRankingEntry(type);
tabContent.appendChild(addEntryBtn);
rankingsEditor.appendChild(tabContent);
}
}
// Add event listeners for input changes
rankingsEditor.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (e) => {
const { type, field, index } = e.target.dataset;
currentRankingsData[type][index][field] = field === 'value' ? parseInt(e.target.value) : e.target.value;
});
});
// Add event listeners for remove buttons
rankingsEditor.querySelectorAll('.remove-ranking-entry').forEach(button => {
button.addEventListener('click', (e) => {
const { type, index } = e.target.dataset;
currentRankingsData[type].splice(parseInt(index), 1);
renderRankingsEditor(currentRankingsData); // Re-render to update indices
});
});
// Set up tabs functionality
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.style.display = 'none');
document.getElementById(`tab-${button.dataset.tab}`).style.display = 'block';
});
});
}
function addRankingEntry(type) {
currentRankingsData[type].push({ id: '', name: '', value: 0 });
renderRankingsEditor(currentRankingsData);
}
saveRankingsBtn.addEventListener('click', async () => {
rankingsMessage.textContent = '正在保存...';
rankingsMessage.className = 'message';
const token = getAuthToken();
if (!token) return;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/rankings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ rankings: currentRankingsData })
});
const data = await response.json();
if (data.success) {
rankingsMessage.textContent = '排行榜保存成功!';
rankingsMessage.className = 'message success-message';
} else {
rankingsMessage.textContent = data.message;
rankingsMessage.className = 'message error-message';
}
} catch (error) {
console.error('保存排行榜失败:', error);
rankingsMessage.textContent = '保存失败,请稍后再试。';
rankingsMessage.className = 'message error-message';
}
});
// --- 商城产品管理 ---
async function fetchAdminShopProducts() {
shopMessage.textContent = '';
const token = getAuthToken();
if (!token) {
showSection('login-section');
return;
}
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/shop/products`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 403) {
removeAuthToken();
showSection('login-section');
shopMessage.textContent = '会话过期,请重新登录。';
}
throw new Error(`HTTP error! status: ${response.status}`);
}
currentShopProductsData = await response.json();
renderShopProductsEditor(currentShopProductsData);
} catch (error) {
console.error('获取商城产品失败:', error);
shopMessage.textContent = '获取商城产品失败。';
}
}
function renderShopProductsEditor(products) {
shopEditor.innerHTML = '';
products.forEach((product, index) => {
const entryDiv = document.createElement('div');
entryDiv.className = 'shop-entry card';
entryDiv.innerHTML = `
<div>
<label>产品ID:</label>
<input type="text" data-field="id" value="${product.id}" data-index="${index}">
</div>
<div>
<label>名称:</label>
<input type="text" data-field="name" value="${product.name}" data-index="${index}">
</div>
<div>
<label>描述:</label>
<textarea data-field="description" data-index="${index}">${product.description}</textarea>
</div>
<div>
<label>价格:</label>
<input type="number" data-field="price" value="${product.price}" data-index="${index}">
</div>
<div>
<label>图标 (Font Awesome class):</label>
<input type="text" data-field="icon" value="${product.icon}" data-index="${index}">
</div>
<button class="btn btn-danger remove-shop-entry" data-index="${index}">删除</button>
`;
shopEditor.appendChild(entryDiv);
});
const addProductBtn = document.createElement('button');
addProductBtn.className = 'btn btn-primary';
addProductBtn.textContent = '添加新产品';
addProductBtn.onclick = addShopProduct;
shopEditor.appendChild(addProductBtn);
shopEditor.querySelectorAll('input, textarea').forEach(input => {
input.addEventListener('change', (e) => {
const { field, index } = e.target.dataset;
currentShopProductsData[index][field] = field === 'price' ? parseInt(e.target.value) : e.target.value;
});
});
shopEditor.querySelectorAll('.remove-shop-entry').forEach(button => {
button.addEventListener('click', (e) => {
const { index } = e.target.dataset;
currentShopProductsData.splice(parseInt(index), 1);
renderShopProductsEditor(currentShopProductsData);
});
});
}
function addShopProduct() {
currentShopProductsData.push({ id: '', name: '', description: '', price: 0, icon: 'fas fa-box' });
renderShopProductsEditor(currentShopProductsData);
}
saveShopBtn.addEventListener('click', async () => {
shopMessage.textContent = '正在保存...';
shopMessage.className = 'message';
const token = getAuthToken();
if (!token) return;
try {
const response = await fetch(`${ADMIN_BACKEND_URL}/api/admin/shop/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ products: currentShopProductsData })
});
const data = await response.json();
if (data.success) {
shopMessage.textContent = '商城产品保存成功!';
shopMessage.className = 'message success-message';
} else {
shopMessage.textContent = data.message;
shopMessage.className = 'message error-message';
}
} catch (error) {
console.error('保存商城产品失败:', error);
shopMessage.textContent = '保存失败,请稍后再试。';
shopMessage.className = 'message error-message';
}
});
// --- 页面初始化 ---
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
// Initially render dashboard status if already logged in (token exists)
if (getAuthToken()) {
updateAdminServerStatus(window.currentMcStatus);
}
// Expose showSection to global scope for onclick events
window.showSection = showSection;
});
10. public/images/default-favicon.png (示例图标)
为了避免服务器状态图片加载失败时显示空白,你可以创建一个简单的默认图标(例如一个Minecraft方块的图片),并将其命名为 default-favicon.png 放在 public/images/ 目录下。