设计思路与技术栈选择
一个现代化的开服表网站需要考虑以下几个方面:
- 前端展示:需要清晰地展示游戏列表,并提供良好的搜索、筛选和分页体验。
- 后端管理:需要一个管理后台来方便地添加、修改、删除游戏和开服信息。
- 数据存储:需要一个数据库来持久化存储所有数据。
- 性能与扩展性:代码结构要清晰,易于维护和扩展。
技术栈推荐:
- 前端:
- 框架: Vue.js 3 或 React,这里我们以 Vue.js 3 为例,因为它在构建这类 UI 界面时非常高效。
- UI 库: Element Plus (配合 Vue 3) 或 Ant Design Vue,它们提供了丰富的组件,能快速搭建出美观的界面。
- 构建工具: Vite。
- 后端:
- 框架: Node.js + Express 或 Koa,轻量、灵活,非常适合做 API 服务。
- 数据库: MySQL 或 PostgreSQL,关系型数据库,结构化数据存储非常合适。
- ORM (对象关系映射): Prisma 或 Sequelize,ORM 可以让你用 JavaScript/TypeScript 对象来操作数据库,避免手写复杂的 SQL,提高开发效率和代码可读性,这里我们选择 Prisma。
- 部署:
- 前端: Nginx。
- 后端: PM2 (Node.js 进程管理器)。
数据库设计
这是整个应用的基石,我们需要至少两张表:games (游戏表) 和 servers (开服表)。
games (游戏表)
存储游戏的基本信息。
| 字段名 | 类型 | 描述 |
|---|---|---|
id |
INT (PK, Auto-Inc) |
游戏ID,主键 |
name |
VARCHAR(255) |
游戏名称 (e.g., "原神", "梦幻西游") |
icon_url |
VARCHAR(512) |
游戏图标URL |
status |
INT |
游戏状态 (1: 运营中, 0: 已停运) |
created_at |
DATETIME |
创建时间 |
updated_at |
DATETIME |
更新时间 |
servers (开服表)
存储具体的开服信息,并与 games 表关联。
| 字段名 | 类型 | 描述 |
|---|---|---|
id |
INT (PK, Auto-Inc) |
开服ID,主键 |
game_id |
INT (FK) |
关联的游戏ID |
server_name |
VARCHAR(255) |
服务器名称 (e.g., "iOS-1区", "安卓互通区") |
open_type |
VARCHAR(50) |
开服类型 (e.g., "全新开服", "资料片", "合服") |
open_time |
DATETIME |
开服时间,这是核心字段 |
screenshot_url |
VARCHAR(512) |
游戏截图URL |
download_url |
VARCHAR(512) |
下载地址 |
official_website |
VARCHAR(512) |
官方网站 |
created_at |
DATETIME |
创建时间 |
updated_at |
DATETIME |
更新时间 |
核心功能实现
我们将分为后端 API 和前端页面两部分来讲解。
后端 API 实现 (Node.js + Express + Prisma)
项目结构:
/server
/src
/controllers // 控制器,处理业务逻辑
/routes // 路由定义
/prisma // Prisma 相关文件
prisma/schema.prisma // 数据库模型定义
package.json
步骤 1: 初始化项目
mkdir game-server && cd game-server npm init -y npm install express prisma @prisma/client cors dotenv npx prisma init
步骤 2: 定义 Prisma Schema (prisma/schema.prisma)
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql" // 或 "postgresql"
url = env("DATABASE_URL")
}
model Game {
id Int @id @default(autoincrement())
name String
icon_url String?
status Int @default(1)
servers Server[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Server {
id Int @id @default(autoincrement())
game_id Int
game Game @relation(fields: [game_id], references: [id], onDelete: Cascade)
server_name String
open_type String
open_time DateTime
screenshot_url String?
download_url String?
official_website String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
然后运行 npx prisma db push 来同步数据库结构。
步骤 3: 创建 API 路由和控制器
/src/controllers/serverController.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// 获取开服列表,支持分页、搜索、筛选
exports.getServers = async (req, res) => {
const { page = 1, pageSize = 10, gameId, openType, keyword } = req.query;
const skip = (page - 1) * pageSize;
const where = {};
// 关联游戏搜索
if (gameId) {
where.game_id = parseInt(gameId);
}
// 开服类型筛选
if (openType) {
where.open_type = { contains: openType, mode: 'insensitive' };
}
// 关键词搜索 (游戏名或服务器名)
if (keyword) {
where.OR = [
{ game: { name: { contains: keyword, mode: 'insensitive' } } },
{ server_name: { contains: keyword, mode: 'insensitive' } }
];
}
// 按开服时间倒序排列
const orderBy = { open_time: 'desc' };
try {
const [servers, total] = await Promise.all([
prisma.server.findMany({
where,
include: { // 关联查询游戏信息
game: {
select: { id: true, name: true, icon_url: true }
}
},
skip: parseInt(skip),
take: parseInt(pageSize),
orderBy,
}),
prisma.server.count({ where })
]);
res.json({
data: servers,
pagination: {
total,
page: parseInt(page),
pageSize: parseInt(pageSize)
}
});
} catch (error) {
res.status(500).json({ error: '获取服务器列表失败' });
}
};
// 获取即将开服的游戏列表 (用于首页推荐)
exports.getUpcomingGames = async (req, res) => {
const now = new Date();
try {
const servers = await prisma.server.findMany({
where: {
open_time: { gt: now } // gt: greater than
},
include: {
game: {
select: { id: true, name: true, icon_url: true }
}
},
orderBy: { open_time: 'asc' },
take: 10 // 只取前10个
});
res.json(servers);
} catch (error) {
res.status(500).json({ error: '获取即将开服列表失败' });
}
};
// ... 其他增删改查方法
/src/routes/serverRoutes.js
const express = require('express');
const router = express.Router();
const serverController = require('../controllers/serverController');
// 获取开服列表
router.get('/', serverController.getServers);
// 获取即将开服列表
router.get('/upcoming', serverController.getUpcomingGames);
// ... 其他路由 POST, PUT, DELETE
module.exports = router;
/src/index.js (主入口文件)
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const serverRoutes = require('./routes/serverRoutes');
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors()); // 允许跨域
app.use(express.json()); // 解析 JSON 请求体
// 路由
app.use('/api/servers', serverRoutes);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
前端实现 (Vue.js 3 + Element Plus)
项目结构:
/client
/src
/components // 可复用组件
/views // 页面
/router // 路由
/api // API 请求封装
App.vue
main.js
步骤 1: 初始化项目
npm create vue@latest my-game-client cd my-game-client npm install npm install element-plus axios vue-router
步骤 2: 封装 API 请求 (/src/api/index.js)
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:3000/api', // 后端服务地址
timeout: 10000,
});
// 请求拦截器 (可以在这里添加 token)
api.interceptors.request.use(config => {
return config;
});
// 响应拦截器 (统一处理错误)
api.interceptors.response.use(
response => response.data,
error => {
console.error('API Error:', error);
return Promise.reject(error);
}
);
export default {
// 获取开服列表
getServers(params) {
return api.get('/servers', { params });
},
// 获取即将开服列表
getUpcomingGames() {
return api.get('/servers/upcoming');
}
// ... 其他 API
};
步骤 3: 创建视图 (/src/views/ServersList.vue)
<template>
<div class="server-list-container">
<h1>游戏开服表</h1>
<!-- 搜索和筛选区 -->
<el-card class="filter-card">
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="关键词">
<el-input v-model="filterForm.keyword" placeholder="搜索游戏或服务器" clearable />
</el-form-item>
<el-form-item label="游戏">
<el-select v-model="filterForm.gameId" placeholder="选择游戏" clearable filterable>
<el-option
v-for="game in gameOptions"
:key="game.id"
:label="game.name"
:value="game.id"
/>
</el-select>
</el-form-item>
<el-form-item label="开服类型">
<el-select v-model="filterForm.openType" placeholder="选择类型" clearable>
<el-option label="全新开服" value="全新开服" />
<el-option label="资料片" value="资料片" />
<el-option label="合服" value="合服" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchServers">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 开服列表 -->
<el-card v-for="server in serverList" :key="server.id" class="server-card">
<div class="server-info">
<img :src="server.game.icon_url || '/default-icon.png'" class="game-icon" alt="游戏图标">
<div class="server-details">
<h3>{{ server.game.name }}</h3>
<p class="server-name">{{ server.server_name }}</p>
<p class="open-type">{{ server.open_type }}</p>
</div>
<div class="server-time">
<el-tag type="danger" size="large">{{ formatOpenTime(server.open_time) }}</el-tag>
</div>
</div>
<div class="server-actions">
<el-button type="primary" size="small" :href="server.download_url" target="_blank">立即下载</el-button>
<el-button size="small" :href="server.official_website" target="_blank">官网</el-button>
</div>
</el-card>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import api from '../api';
// 响应式数据
const serverList = ref([]);
const gameOptions = ref([]); // 游戏下拉框选项
const loading = ref(false);
// 筛选表单
const filterForm = reactive({
keyword: '',
gameId: '',
openType: ''
});
// 分页信息
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0
});
// 获取开服列表
const fetchServers = async () => {
loading.value = true;
try {
const params = {
page: pagination.page,
pageSize: pagination.pageSize,
...filterForm
};
const response = await api.getServers(params);
serverList.value = response.data;
pagination.total = response.pagination.total;
} catch (error) {
ElMessage.error('获取开服列表失败');
} finally {
loading.value = false;
}
};
// 获取游戏列表 (用于下拉框)
const fetchGames = async () => {
// 这里需要另一个后端 API 来获取游戏列表
// 假设 API 是 /api/games
// try {
// const response = await api.getGames();
// gameOptions.value = response.data;
// } catch (error) {
// ElMessage.error('获取游戏列表失败');
// }
};
// 格式化开服时间
const formatOpenTime = (timeStr) => {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// 分页事件处理
const handleSizeChange = (val) => {
pagination.pageSize = val;
fetchServers();
};
const handleCurrentChange = (val) => {
pagination.page = val;
fetchServers();
};
// 组件挂载时获取数据
onMounted(() => {
fetchServers();
fetchGames();
});
</script>
<style scoped>
.server-list-container { padding: 20px; }
.filter-card { margin-bottom: 20px; }
.server-card { margin-bottom: 15px; }
.server-info { display: flex; align-items: center; margin-bottom: 10px; }
.game-icon { width: 50px; height: 50px; margin-right: 15px; object-fit: cover; border-radius: 5px; }
.server-details { flex-grow: 1; }
.server-name { font-size: 18px; font-weight: bold; margin: 5px 0; }
.open-type { color: #666; }
.server-time { margin-left: 20px; }
.server-actions { text-align: right; }
.pagination-container { margin-top: 20px; text-align: center; }
</style>
完整源码示例
由于篇幅限制,这里无法提供完整可运行的带数据库的代码包,但以上代码已经涵盖了核心逻辑,您可以基于此进行扩展。
一个更简单的、不依赖数据库的静态版本示例
如果您想快速看到一个效果,可以使用一个静态的 JSON 文件来模拟数据。
public/data.json
{
"servers": [
{
"id": 1,
"game_id": 1,
"game": { "id": 1, "name": "原神", "icon_url": "https://example.com/genshin-icon.png" },
"server_name": "iOS-1区",
"open_type": "全新开服",
"open_time": "2025-10-27T10:00:00.000Z",
"screenshot_url": "https://example.com/screenshot1.jpg",
"download_url": "https://appstore.com/genshin",
"official_website": "https://genshin.hoyoverse.com/"
},
{
"id": 2,
"game_id": 2,
"game": { "id": 2, "name": "梦幻西游", "icon_url": "https://example.com/mhxy-icon.png" },
"server_name": "安卓互通-王者归来",
"open_type": "资料片",
"open_time": "2025-10-28T14:30:00.000Z",
"screenshot_url": "https://example.com/screenshot2.jpg",
"download_url": "https://androidapp.com/mhxy",
"official_website": "https://xyq.163.com/"
}
],
"games": [
{ "id": 1, "name": "原神", "icon_url": "https://example.com/genshin-icon.png" },
{ "id": 2, "name": "梦幻西游", "icon_url": "https://example.com/mhxy-icon.png" }
]
}
然后修改前端的 API 调用,改为从本地 JSON 文件读取,这种方式适合原型开发或数据量极小的场景。
部署说明
-
后端部署:
- 安装 Node.js 和 PM2。
- 将后端代码上传到服务器。
- 运行
npm install安装依赖。 - 配置
DATABASE_URL等环境变量。 - 使用 PM2 启动服务,实现后台运行和自动重启:
npm install -g pm2 pm2 start src/index.js --name "game-server-api" pm2 startup # 设置开机自启 pm2 save # 保存当前进程列表
-
前端部署:
-
在前端项目根目录运行
npm run build,生成dist文件夹。 -
将
dist文件夹下的所有文件上传到服务器的 Web 根目录(如/var/www/html)。 -
配置 Nginx,将所有请求指向
index.html,并代理 API 请求到后端服务。 -
Nginx 配置示例 (
/etc/nginx/sites-available/game-website):server { listen 80; server_name your-domain.com; # 替换成你的域名 # 前端静态文件 root /var/www/html; index index.html; # 处理 Vue Router 的 history 模式 location / { try_files $uri $uri/ /index.html; } # 代理 API 请求到后端 location /api { proxy_pass http://localhost:3000; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } -
启用配置并重启 Nginx:
sudo ln -s /etc/nginx/sites-available/game-website /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx
-
功能扩展建议
- 管理后台:使用 Element Plus Plus 或 Ant Design Pro 快速构建一个管理界面,用于管理游戏和开服信息。
- 用户系统:增加用户注册、登录、收藏游戏/服务器、开服提醒等功能。
- 缓存:使用 Redis 缓存热门游戏列表或即将开服列表,减轻数据库压力。
- 定时任务:使用
node-cron等库,定时抓取其他游戏官网的开服信息,实现数据自动更新。 - RSS/订阅:为用户提供 RSS 订阅或邮件订阅服务,当有新开服信息时自动通知用户。
- API 接口开放:将你的开服数据 API 开放给其他网站使用,可以增加网站的影响力。
这份指南为您提供了一个坚实的基础,您可以根据自己的需求进行修改和扩展,祝您开发顺利!
