Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

939 lines
33 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>🎮 Net-Games 游戏大厅</title>
<script>
// 强制刷新缓存
if (!window.location.search.includes('v=2')) {
window.location.replace(window.location.pathname + '?v=2');
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #fff;
overflow-x: hidden;
}
/* 头部 */
.header {
text-align: center;
padding: 40px 20px 30px;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
text-shadow: 0 0 30px rgba(102, 126, 234, 0.3);
}
.header p {
color: #888;
font-size: 1rem;
}
/* 游戏网格容器 */
.games-container {
max-width: 1600px;
margin: 0 auto;
padding: 30px 20px 60px;
}
.games-grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 640px) {
.games-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 900px) {
.games-grid { grid-template-columns: repeat(4, 1fr); }
}
@media (min-width: 1200px) {
.games-grid { grid-template-columns: repeat(5, 1fr); }
}
/* 游戏卡片 */
.game-card {
position: relative;
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
border-radius: 16px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
}
.game-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.5);
}
.game-card:active {
transform: translateY(-4px) scale(0.98);
}
.game-icon {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
position: relative;
overflow: hidden;
}
.game-icon::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.game-info {
padding: 15px;
background: rgba(0, 0, 0, 0.3);
}
.game-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 5px;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.game-desc {
font-size: 0.75rem;
color: #888;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.game-tag {
display: inline-block;
padding: 3px 8px;
background: rgba(102, 126, 234, 0.3);
border-radius: 12px;
font-size: 0.7rem;
color: #a5b4fc;
margin-top: 8px;
}
.game-score {
display: inline-block;
padding: 3px 10px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border-radius: 12px;
font-size: 0.75rem;
color: #fff;
margin-top: 8px;
margin-left: 6px;
font-weight: 600;
}
.new-badge {
position: absolute;
top: 10px;
right: 10px;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
color: #fff;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
z-index: 10;
box-shadow: 0 4px 10px rgba(238, 90, 90, 0.4);
}
.hot-badge {
position: absolute;
top: 10px;
left: 10px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: #fff;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
z-index: 10;
box-shadow: 0 4px 10px rgba(217, 119, 6, 0.4);
}
/* 页脚 */
.footer {
text-align: center;
padding: 30px 20px;
color: #666;
font-size: 0.85rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 分类标题 */
.category-title {
font-size: 1.3rem;
margin: 30px 0 20px;
padding-left: 10px;
border-left: 4px solid #667eea;
color: #fff;
}
/* ========== 登录弹窗 ========== */
.login-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.85);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
}
.login-overlay.hidden {
display: none !important;
}
.login-input.hidden {
display: none !important;
}
.login-box {
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 360px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.login-title {
text-align: center;
font-size: 22px;
font-weight: 600;
margin-bottom: 8px;
color: #fff;
}
.login-subtitle {
text-align: center;
font-size: 14px;
color: #888;
margin-bottom: 24px;
}
.login-input {
width: 100%;
padding: 14px 16px;
margin-bottom: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
font-size: 16px;
background: rgba(0, 0, 0, 0.3);
color: #fff;
transition: border-color 0.2s;
}
.login-input:focus {
outline: none;
border-color: #667eea;
}
.login-input::placeholder {
color: #666;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
margin-top: 8px;
}
.login-btn:hover { opacity: 0.9; }
.login-btn:disabled {
background: #444;
cursor: not-allowed;
}
.login-error {
color: #ff6b6b;
font-size: 14px;
margin-top: 12px;
text-align: center;
display: none;
}
.login-error.show { display: block; }
.login-link {
text-align: center;
margin-top: 16px;
font-size: 14px;
color: #888;
}
.login-link a {
color: #667eea;
cursor: pointer;
text-decoration: none;
}
.login-link a:hover {
text-decoration: underline;
}
/* ========== 用户信息栏 ========== */
.user-bar {
position: fixed;
top: 10px;
right: 10px;
display: none;
align-items: center;
gap: 10px;
background: rgba(0, 0, 0, 0.5);
padding: 8px 16px;
border-radius: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 1000;
}
.user-bar.visible { display: flex; }
.user-info { font-size: 13px; color: #ccc; }
.user-name { color: #fff; font-weight: 600; }
.logout-btn {
padding: 4px 12px;
background: rgba(255, 59, 48, 0.8);
color: white;
border: none;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
}
.logout-btn:hover { background: rgba(255, 59, 48, 1); }
.settings-btn {
padding: 4px 12px;
background: rgba(102, 126, 234, 0.8);
color: white;
border: none;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
}
.settings-btn:hover { background: rgba(102, 126, 234, 1); }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
</style>
</head>
<body>
<!-- 登录/注册弹窗 - 默认隐藏,由JS控制显示 -->
<div class="login-overlay hidden" id="loginOverlay" style="display: none;">
<div class="login-box">
<div class="login-title" id="authTitle">🔒 Net-Games</div>
<div class="login-subtitle" id="authSubtitle">请先登录后进入游戏大厅</div>
<input type="text" class="login-input" id="username" placeholder="用户名" autocomplete="username">
<input type="password" class="login-input" id="password" placeholder="密码" autocomplete="current-password">
<input type="password" class="login-input hidden" id="password2" placeholder="确认密码" autocomplete="new-password">
<button class="login-btn" id="authBtn" onclick="doAuth()">登录</button>
<div class="login-error" id="loginError"></div>
<div class="login-link">
<span id="authSwitchText">还没有账号?</span>
<a id="authSwitchLink" onclick="toggleAuthMode()">立即注册</a>
</div>
</div>
</div>
<!-- 用户信息栏 -->
<div class="user-bar" id="userBar">
<div class="user-info">
👤 <span class="user-name" id="userName"></span>
<span id="adminBadge" style="display: none; background: #ff6b6b; color: white; padding: 2px 6px; border-radius: 4px; font-size: 11px; margin-left: 6px;">管理员</span>
</div>
<button class="settings-btn" id="adminBtn" style="display: none; background: #ff6b6b;" onclick="showAdminPanel()">管理</button>
<button class="settings-btn" onclick="showSettingsModal()">设置</button>
<button class="logout-btn" onclick="doLogout()">退出</button>
</div>
<!-- 个人设置弹窗 -->
<div class="login-overlay hidden" id="settingsOverlay" style="display: none;">
<div class="login-box">
<div class="login-title">⚙️ 个人设置</div>
<div class="login-subtitle">修改昵称和密码</div>
<div style="margin-bottom: 20px; padding: 12px; background: rgba(102, 126, 234, 0.1); border-radius: 8px;">
<div style="font-size: 13px; color: #888;">用户名(不可修改)</div>
<div id="settingsUsername" style="font-size: 16px; color: #fff; font-weight: 600;"></div>
</div>
<input type="text" class="login-input" id="settingNickname" placeholder="昵称" maxlength="20">
<div style="margin: 20px 0; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 20px;">
<div style="font-size: 14px; color: #888; margin-bottom: 12px;">🔒 修改密码(可选)</div>
<input type="password" class="login-input" id="settingOldPassword" placeholder="当前密码">
<input type="password" class="login-input" id="settingNewPassword" placeholder="新密码(至少6位)">
</div>
<button class="login-btn" onclick="saveSettings()">保存设置</button>
<div class="login-error" id="settingsError"></div>
<div class="login-link">
<a onclick="hideSettingsModal()">取消</a>
</div>
</div>
</div>
<header class="header">
<h1>🎮 Net-Games 游戏大厅</h1>
<p>Socket.IO 实时对战游戏平台</p>
<p style="font-size: 0.8rem; margin-top: 5px; opacity: 0.7;">支持人机对战、AI对战、多人联机</p>
</header>
<main class="games-container">
<h2 class="category-title">🔥 热门对战</h2>
<div class="games-grid" id="gamesGrid">
<!-- 游戏卡片动态生成 -->
</div>
<h2 class="category-title">🆕 即将上线</h2>
<div class="games-grid" id="comingSoonGrid">
<!-- 即将上线 -->
</div>
</main>
<footer class="footer">
<p>🎮 Net-Games 游戏大厅 | 实时对战,随时开局</p>
</footer>
<script>
// 游戏配置 - 添加新游戏只需修改这里
const games = [
{
id: 'doudizhu',
title: '🃏 斗地主',
desc: '经典三人扑克,支持人类 vs AI、AI vs AI 多种对战模式',
icon: '🃏',
url: '/doudizhu/',
tag: '扑克棋牌',
isNew: true,
isHot: true,
category: 'hot'
}
// 后续添加更多游戏...
// {
// id: 'chinese-chess',
// title: '♟️ 中国象棋',
// desc: '人机对弈,Minimax + DeepSeek AI',
// icon: '♟️',
// url: '/chinese-chess/',
// tag: '棋牌策略',
// isNew: false,
// isHot: true
// }
];
const comingSoonGames = [
{
id: 'chinese-chess',
title: '♟️ 中国象棋',
desc: '人机对弈,Minimax + DeepSeek AI(开发中)',
icon: '♟️',
url: '#',
tag: '即将上线',
disabled: true
},
{
id: 'guandan',
title: '🃏 掼蛋',
desc: '4人2v2对家组队(开发中)',
icon: '🃏',
url: '#',
tag: '即将上线',
disabled: true
}
];
let isAuthenticated = false;
// 用户各游戏积分
let userGameScores = {};
// 创建游戏卡片
function createGameCard(game) {
const badges = [];
if (game.isHot) badges.push('<span class="hot-badge">🔥 HOT</span>');
if (game.isNew) badges.push('<span class="new-badge">NEW</span>');
// 显示该游戏积分(如果已获取)
const score = userGameScores[game.id];
const scoreHtml = score !== undefined && !game.disabled
? `<div class="game-score">🏆 积分: ${score}</div>`
: '';
return `
<div class="game-card" data-id="${game.id}" data-url="${game.url}"
${game.disabled ? '' : 'onclick="handleGameClick(this)"'}
style="${game.disabled ? 'opacity: 0.6; cursor: not-allowed;' : ''}">
${badges.join('')}
<div class="game-icon">${game.icon}</div>
<div class="game-info">
<div class="game-title">${game.title}</div>
<div class="game-desc">${game.desc}</div>
<span class="game-tag">${game.tag}</span>
${scoreHtml}
</div>
</div>
`;
}
// 获取用户各游戏积分
async function fetchUserScores() {
if (!isAuthenticated) return;
try {
const response = await fetch('/api/user/scores', {
credentials: 'include'
});
const data = await response.json();
if (data.success) {
userGameScores = data.scores || {};
renderGames(); // 重新渲染以显示积分
}
} catch (error) {
console.error('获取积分失败:', error);
}
}
// 处理游戏点击
function handleGameClick(element) {
if (!isAuthenticated) {
element.style.animation = 'shake 0.3s';
setTimeout(() => element.style.animation = '', 300);
showLoginOverlay();
return;
}
const url = element.dataset.url;
window.location.href = url;
}
// 渲染游戏列表
function renderGames() {
const hotGames = games.filter(g => g.category === 'hot' || g.isHot);
document.getElementById('gamesGrid').innerHTML = hotGames.map(createGameCard).join('');
document.getElementById('comingSoonGrid').innerHTML = comingSoonGames.map(createGameCard).join('');
}
// ========== 登录相关 ==========
function showLoginOverlay() {
const overlay = document.getElementById('loginOverlay');
overlay.style.display = '';
overlay.classList.remove('hidden');
}
function hideLoginOverlay() {
const overlay = document.getElementById('loginOverlay');
overlay.classList.add('hidden');
// 保留内联样式确保隐藏
overlay.style.display = 'none';
}
function showUserBar(user) {
currentUser = user;
document.getElementById('userName').textContent = user.nickname || user.username;
document.getElementById('userBar').classList.add('visible');
// 显示管理员标记和按钮
if (user.is_admin || user.role === 'admin') {
document.getElementById('adminBadge').style.display = 'inline-block';
document.getElementById('adminBtn').style.display = 'inline-block';
} else {
document.getElementById('adminBadge').style.display = 'none';
document.getElementById('adminBtn').style.display = 'none';
}
}
function hideUserBar() {
document.getElementById('userBar').classList.remove('visible');
}
// ========== 管理员面板 ==========
function showAdminPanel() {
alert('🛠️ 管理员功能开发中...\n\n计划功能:\n• 查看在线统计\n• 管理房间列表\n• 强制关闭房间\n• 用户管理');
}
// ========== 个人设置 ==========
let currentUser = null;
function showSettingsModal() {
if (!currentUser) return;
document.getElementById('settingsUsername').textContent = currentUser.username;
document.getElementById('settingNickname').value = currentUser.nickname || currentUser.username;
document.getElementById('settingOldPassword').value = '';
document.getElementById('settingNewPassword').value = '';
document.getElementById('settingsError').classList.remove('show');
const overlay = document.getElementById('settingsOverlay');
overlay.style.display = '';
overlay.classList.remove('hidden');
}
function hideSettingsModal() {
const overlay = document.getElementById('settingsOverlay');
overlay.classList.add('hidden');
overlay.style.display = 'none';
}
async function saveSettings() {
const nickname = document.getElementById('settingNickname').value.trim();
const oldPassword = document.getElementById('settingOldPassword').value;
const newPassword = document.getElementById('settingNewPassword').value;
const errorDiv = document.getElementById('settingsError');
const btn = document.querySelector('#settingsOverlay .login-btn');
// 验证昵称
if (!nickname || nickname.length < 2 || nickname.length > 20) {
errorDiv.textContent = '昵称长度需在2-20个字符之间';
errorDiv.classList.add('show');
return;
}
// 验证密码(如果填写了)
if (newPassword && newPassword.length < 6) {
errorDiv.textContent = '新密码长度至少6位';
errorDiv.classList.add('show');
return;
}
if (newPassword && !oldPassword) {
errorDiv.textContent = '修改密码需要填写当前密码';
errorDiv.classList.add('show');
return;
}
btn.disabled = true;
btn.textContent = '保存中...';
try {
// 更新昵称
const profileRes = await fetch('/api/user/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ nickname })
});
const profileData = await profileRes.json();
if (!profileData.success) {
errorDiv.textContent = profileData.error || '更新昵称失败';
errorDiv.classList.add('show');
return;
}
// 更新密码(如果填写了)
if (newPassword) {
const pwdRes = await fetch('/api/user/password', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
});
const pwdData = await pwdRes.json();
if (!pwdData.success) {
errorDiv.textContent = pwdData.error || '修改密码失败';
errorDiv.classList.add('show');
return;
}
// 密码修改成功,需要重新登录
alert('密码修改成功,请重新登录');
doLogout();
hideSettingsModal();
return;
}
// 更新本地用户信息
currentUser = profileData.user;
document.getElementById('userName').textContent = currentUser.nickname || currentUser.username;
hideSettingsModal();
} catch (error) {
errorDiv.textContent = '网络错误,请重试';
errorDiv.classList.add('show');
} finally {
btn.disabled = false;
btn.textContent = '保存设置';
}
}
async function checkAuth() {
try {
const response = await fetch('/api/auth/status', {
credentials: 'include',
cache: 'no-cache'
});
const data = await response.json();
isAuthenticated = data.authenticated;
console.log('登录状态:', isAuthenticated);
if (isAuthenticated) {
hideLoginOverlay();
showUserBar(data.user);
// 获取用户各游戏积分
await fetchUserScores();
} else {
// 未登录也可以浏览,但点击游戏需要登录
hideLoginOverlay();
}
} catch (error) {
console.error('检查登录状态失败:', error);
hideLoginOverlay();
}
}
let isRegisterMode = false;
function toggleAuthMode() {
isRegisterMode = !isRegisterMode;
const title = document.getElementById('authTitle');
const subtitle = document.getElementById('authSubtitle');
const btn = document.getElementById('authBtn');
const switchText = document.getElementById('authSwitchText');
const switchLink = document.getElementById('authSwitchLink');
const password2 = document.getElementById('password2');
const errorDiv = document.getElementById('loginError');
errorDiv.classList.remove('show');
if (isRegisterMode) {
title.textContent = '📝 注册账号';
subtitle.textContent = '创建新账号开始游戏';
btn.textContent = '注册';
switchText.textContent = '已有账号?';
switchLink.textContent = '立即登录';
password2.classList.remove('hidden');
} else {
title.textContent = '🔒 Net-Games';
subtitle.textContent = '请先登录后进入游戏大厅';
btn.textContent = '登录';
switchText.textContent = '还没有账号?';
switchLink.textContent = '立即注册';
password2.classList.add('hidden');
}
}
async function doAuth() {
if (isRegisterMode) {
await doRegister();
} else {
await doLogin();
}
}
async function doLogin() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const btn = document.getElementById('authBtn');
const errorDiv = document.getElementById('loginError');
if (!username || !password) {
errorDiv.textContent = '请输入用户名和密码';
errorDiv.classList.add('show');
return;
}
btn.disabled = true;
btn.textContent = '登录中...';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
isAuthenticated = true;
hideLoginOverlay();
showUserBar(data.user);
document.getElementById('username').value = '';
document.getElementById('password').value = '';
// 获取用户各游戏积分
await fetchUserScores();
} else {
errorDiv.textContent = data.error || '登录失败';
errorDiv.classList.add('show');
}
} catch (error) {
errorDiv.textContent = '网络错误,请重试';
errorDiv.classList.add('show');
} finally {
btn.disabled = false;
btn.textContent = '登录';
}
}
async function doRegister() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const password2 = document.getElementById('password2').value.trim();
const btn = document.getElementById('authBtn');
const errorDiv = document.getElementById('loginError');
if (!username || !password) {
errorDiv.textContent = '请输入用户名和密码';
errorDiv.classList.add('show');
return;
}
if (username.length < 3) {
errorDiv.textContent = '用户名至少3个字符';
errorDiv.classList.add('show');
return;
}
if (password.length < 6) {
errorDiv.textContent = '密码至少6位';
errorDiv.classList.add('show');
return;
}
if (password !== password2) {
errorDiv.textContent = '两次输入的密码不一致';
errorDiv.classList.add('show');
return;
}
btn.disabled = true;
btn.textContent = '注册中...';
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
isAuthenticated = true;
hideLoginOverlay();
showUserBar(data.user);
document.getElementById('username').value = '';
document.getElementById('password').value = '';
document.getElementById('password2').value = '';
// 切换回登录模式
toggleAuthMode();
alert('注册成功!欢迎加入 Net-Games!');
} else {
errorDiv.textContent = data.error || '注册失败';
errorDiv.classList.add('show');
}
} catch (error) {
errorDiv.textContent = '网络错误,请重试';
errorDiv.classList.add('show');
} finally {
btn.disabled = false;
btn.textContent = '注册';
}
}
async function doLogout() {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
isAuthenticated = false;
hideUserBar();
showLoginOverlay();
} catch (error) {
console.error('登出失败:', error);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
await checkAuth();
renderGames();
// 回车登录
document.getElementById('password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') doLogin();
});
});
</script>
</body>
</html>