设计思路与技术栈选择

一个现代化的开服表网站需要考虑以下几个方面:

  1. 前端展示:需要清晰地展示游戏列表,并提供良好的搜索、筛选和分页体验。
  2. 后端管理:需要一个管理后台来方便地添加、修改、删除游戏和开服信息。
  3. 数据存储:需要一个数据库来持久化存储所有数据。
  4. 性能与扩展性:代码结构要清晰,易于维护和扩展。

技术栈推荐:

  • 前端:
    • 框架: Vue.js 3React,这里我们以 Vue.js 3 为例,因为它在构建这类 UI 界面时非常高效。
    • UI 库: Element Plus (配合 Vue 3) 或 Ant Design Vue,它们提供了丰富的组件,能快速搭建出美观的界面。
    • 构建工具: Vite。
  • 后端:
    • 框架: Node.js + ExpressKoa,轻量、灵活,非常适合做 API 服务。
    • 数据库: MySQLPostgreSQL,关系型数据库,结构化数据存储非常合适。
    • ORM (对象关系映射): PrismaSequelize,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 文件读取,这种方式适合原型开发或数据量极小的场景。


部署说明

  1. 后端部署:

    • 安装 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    # 保存当前进程列表
  2. 前端部署:

    • 在前端项目根目录运行 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

功能扩展建议

  1. 管理后台:使用 Element Plus Plus 或 Ant Design Pro 快速构建一个管理界面,用于管理游戏和开服信息。
  2. 用户系统:增加用户注册、登录、收藏游戏/服务器、开服提醒等功能。
  3. 缓存:使用 Redis 缓存热门游戏列表或即将开服列表,减轻数据库压力。
  4. 定时任务:使用 node-cron 等库,定时抓取其他游戏官网的开服信息,实现数据自动更新。
  5. RSS/订阅:为用户提供 RSS 订阅或邮件订阅服务,当有新开服信息时自动通知用户。
  6. API 接口开放:将你的开服数据 API 开放给其他网站使用,可以增加网站的影响力。

这份指南为您提供了一个坚实的基础,您可以根据自己的需求进行修改和扩展,祝您开发顺利!