增加用户管理

This commit is contained in:
wtz
2026-02-20 23:39:49 +08:00
parent a272dad5f1
commit af3b805dbf
18 changed files with 1493 additions and 53 deletions

View File

@@ -7,6 +7,17 @@ body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#1a1a2e,
h1{text-align:center;font-size:2rem;color:#ffd700;margin-bottom:20px}
h2{text-align:center;color:#ffd700;margin-bottom:15px}
h3{color:#ffd700;margin-bottom:10px}
.auth-tabs{display:flex;justify-content:center;gap:10px;margin-bottom:15px}
.auth-tabs .tab{padding:10px 30px;border:none;border-radius:6px 6px 0 0;cursor:pointer;font-size:15px;background:rgba(255,255,255,.1);color:#aaa;transition:.2s}
.auth-tabs .tab.active{background:rgba(255,255,255,.2);color:#ffd700}
.auth-form{background:rgba(255,255,255,.1);padding:25px;border-radius:0 0 12px 12px;max-width:350px;margin:0 auto 20px}
.auth-form input{width:100%;padding:10px;margin-bottom:10px;border:none;border-radius:6px;background:rgba(255,255,255,.15);color:#fff;font-size:15px}
.auth-form input::placeholder{color:#aaa}
.captcha-row{display:flex;gap:8px;margin-bottom:10px}
.captcha-row input{flex:1}
.captcha-img{height:38px;border-radius:4px;cursor:pointer}
.user-info{display:flex;justify-content:center;align-items:center;gap:15px;margin-bottom:15px}
.user-info span{color:#ffd700}
.form{background:rgba(255,255,255,.1);padding:25px;border-radius:12px;max-width:350px;margin:0 auto 20px}
.form input,.form select{width:100%;padding:10px;margin-bottom:10px;border:none;border-radius:6px;background:rgba(255,255,255,.15);color:#fff;font-size:15px}
.form input::placeholder{color:#aaa}

View File

@@ -8,10 +8,49 @@
</head>
<body>
<div id="app">
<div id="lobby" class="screen">
<div id="auth" class="screen">
<h1>斗地主残局版</h1>
<div class="auth-tabs">
<button id="loginTab" class="tab active">登录</button>
<button id="registerTab" class="tab">注册</button>
</div>
<div id="loginForm" class="auth-form">
<input type="text" id="loginUsername" placeholder="用户名" maxlength="20">
<input type="password" id="loginPassword" placeholder="密码">
<div class="captcha-row">
<input type="text" id="loginCaptcha" placeholder="验证码" maxlength="4">
<img id="loginCaptchaImg" src="" alt="验证码" class="captcha-img">
</div>
<button id="loginBtn" class="btn primary">登录</button>
</div>
<div id="registerForm" class="auth-form hidden">
<input type="text" id="regUsername" placeholder="用户名 (3-20字符)" maxlength="20">
<input type="password" id="regPassword" placeholder="密码 (至少6位)">
<input type="text" id="regNickname" placeholder="昵称" maxlength="10">
<div class="captcha-row">
<input type="text" id="regCaptcha" placeholder="验证码" maxlength="4">
<img id="regCaptchaImg" src="" alt="验证码" class="captcha-img">
</div>
<button id="registerBtn" class="btn primary">注册</button>
</div>
<div class="rules">
<h3>规则</h3>
<ul>
<li>新增"超人强"为最大单牌</li>
<li>每人初始5张牌</li>
<li>三张可成顺子/炸弹,两对可成连对</li>
</ul>
</div>
</div>
<div id="lobby" class="screen hidden">
<h1>斗地主残局版</h1>
<div class="user-info">
<span id="welcomeUser">欢迎</span>
<button id="logoutBtn" class="btn sm">退出登录</button>
</div>
<div class="form">
<input type="text" id="playerName" placeholder="昵称" maxlength="10">
<input type="text" id="playerName" placeholder="游戏昵称" maxlength="10">
<select id="maxPlayers">
<option value="2">2人</option>
<option value="3">3人</option>
@@ -25,14 +64,6 @@
<button id="joinBtn" class="btn">加入</button>
</div>
</div>
<div class="rules">
<h3>规则</h3>
<ul>
<li>新增"超人强"为最大单牌</li>
<li>每人初始5张牌</li>
<li>三张可成顺子/炸弹,两对可成连对</li>
</ul>
</div>
</div>
<div id="waiting" class="screen hidden">
@@ -41,7 +72,7 @@
<div id="playerList"></div>
<div class="actions">
<button id="readyBtn" class="btn primary">准备</button>
<button id="leaveBtn" class="btn danger">离开</button>
<button id="leaveBtn" class="btn danger">离开房间</button>
</div>
</div>

View File

@@ -1,4 +1,6 @@
let ws = null, playerId = '', roomId = '', state = null, selected = [];
let token = '', userId = '', username = '', nickname = '';
let loginCaptchaId = '', regCaptchaId = '';
const $ = id => document.getElementById(id);
function show(id) {
@@ -13,13 +15,191 @@ function chat(name, msg, sys) {
$('chatMsgs').scrollTop = 1e6;
}
function authHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
};
}
async function getCaptcha(type) {
try {
const res = await fetch('/api/auth/captcha');
const d = await res.json();
if (d.code === 0) {
if (type === 'login') {
loginCaptchaId = d.data.captchaId;
$('loginCaptchaImg').src = d.data.image;
} else {
regCaptchaId = d.data.captchaId;
$('regCaptchaImg').src = d.data.image;
}
}
} catch (e) {
console.error('Failed to get captcha:', e);
}
}
async function login() {
const usernameVal = $('loginUsername').value.trim();
const passwordVal = $('loginPassword').value;
const captchaVal = $('loginCaptcha').value.trim().toUpperCase();
if (!usernameVal || !passwordVal || !captchaVal) {
alert('请填写完整信息');
return;
}
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: usernameVal,
password: passwordVal,
captcha: captchaVal,
captchaId: loginCaptchaId
})
});
const d = await res.json();
if (d.code === 0) {
token = d.data.token;
userId = d.data.userId;
username = d.data.username;
nickname = d.data.nickname;
localStorage.setItem('token', token);
localStorage.setItem('userId', userId);
localStorage.setItem('username', username);
localStorage.setItem('nickname', nickname);
showLobby();
} else {
alert(d.message);
getCaptcha('login');
}
} catch (e) {
alert('登录失败: ' + e.message);
getCaptcha('login');
}
}
async function register() {
const usernameVal = $('regUsername').value.trim();
const passwordVal = $('regPassword').value;
const nicknameVal = $('regNickname').value.trim();
const captchaVal = $('regCaptcha').value.trim().toUpperCase();
if (!usernameVal || !passwordVal || !nicknameVal || !captchaVal) {
alert('请填写完整信息');
return;
}
if (usernameVal.length < 3 || usernameVal.length > 20) {
alert('用户名需要3-20个字符');
return;
}
if (passwordVal.length < 6) {
alert('密码至少6位');
return;
}
try {
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: usernameVal,
password: passwordVal,
nickname: nicknameVal,
captcha: captchaVal,
captchaId: regCaptchaId
})
});
const d = await res.json();
if (d.code === 0) {
token = d.data.token;
userId = d.data.userId;
username = d.data.username;
nickname = d.data.nickname;
localStorage.setItem('token', token);
localStorage.setItem('userId', userId);
localStorage.setItem('username', username);
localStorage.setItem('nickname', nickname);
showLobby();
} else {
alert(d.message);
getCaptcha('register');
}
} catch (e) {
alert('注册失败: ' + e.message);
getCaptcha('register');
}
}
async function logout() {
try {
await fetch('/api/auth/logout', {
method: 'POST',
headers: authHeaders()
});
} catch (e) {}
token = '';
userId = '';
username = '';
nickname = '';
localStorage.removeItem('token');
localStorage.removeItem('userId');
localStorage.removeItem('username');
localStorage.removeItem('nickname');
show('auth');
getCaptcha('login');
}
async function validateToken() {
if (!token) return false;
try {
const res = await fetch('/api/auth/validate', {
headers: authHeaders()
});
const d = await res.json();
if (d.code === 0) {
userId = d.data.userId;
username = d.data.username;
nickname = d.data.nickname;
return true;
}
} catch (e) {}
return false;
}
async function checkCurrentRoom() {
try {
const res = await fetch('/api/rooms/current', {
headers: authHeaders()
});
const d = await res.json();
if (d.code === 0 && d.data && d.data.roomId) {
roomId = d.data.roomId;
playerId = d.data.playerId;
return true;
}
} catch (e) {}
return false;
}
async function showLobby() {
$('welcomeUser').textContent = '欢迎, ' + nickname;
$('playerName').value = nickname;
show('lobby');
}
async function create() {
const name = $('playerName').value.trim();
if (!name) { alert('请输入昵称'); return; }
try {
const res = await fetch('/api/rooms', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
headers: authHeaders(),
body: JSON.stringify({playerName: name, maxPlayers: +$('maxPlayers').value})
});
const d = await res.json();
@@ -43,7 +223,7 @@ async function join() {
try {
const res = await fetch('/api/rooms/' + rid + '/join', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
headers: authHeaders(),
body: JSON.stringify({playerName: name})
});
const d = await res.json();
@@ -59,6 +239,21 @@ async function join() {
}
}
async function leaveRoom() {
try {
await fetch('/api/rooms/leave', {
method: 'POST',
headers: authHeaders()
});
} catch (e) {}
if (ws) ws.close();
show('lobby');
roomId = '';
playerId = '';
state = null;
selected = [];
}
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(proto + '//' + location.host + '/api/ws?roomId=' + roomId + '&playerId=' + playerId);
@@ -68,6 +263,7 @@ function connect() {
if (msg.type === 'state') render(msg.data);
else if (msg.type === 'gameOver') showGameOver(msg.data);
else if (msg.type === 'chat') chat(msg.data.playerName, msg.data.message);
else if (msg.type === 'leave') chat('', '有玩家离开房间', true);
else if (msg.type === 'error') chat('', msg.data, true);
};
ws.onerror = function() { chat('', '连接错误', true); };
@@ -308,9 +504,7 @@ function toggleReady() {
}
function leave() {
if (ws) ws.close();
show('lobby');
roomId = ''; playerId = ''; state = null; selected = [];
leaveRoom();
}
function showGameOver(d) {
@@ -329,11 +523,31 @@ function again() {
send('ready', {ready: false});
}
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', async function() {
$('loginTab').onclick = function() {
$('loginTab').classList.add('active');
$('registerTab').classList.remove('active');
$('loginForm').classList.remove('hidden');
$('registerForm').classList.add('hidden');
getCaptcha('login');
};
$('registerTab').onclick = function() {
$('registerTab').classList.add('active');
$('loginTab').classList.remove('active');
$('registerForm').classList.remove('hidden');
$('loginForm').classList.add('hidden');
getCaptcha('register');
};
$('loginCaptchaImg').onclick = function() { getCaptcha('login'); };
$('regCaptchaImg').onclick = function() { getCaptcha('register'); };
$('loginBtn').onclick = login;
$('registerBtn').onclick = register;
$('logoutBtn').onclick = logout;
$('createBtn').onclick = create;
$('joinBtn').onclick = join;
$('readyBtn').onclick = toggleReady;
$('leaveBtn').onclick = leave;
$('leaveBtn').onclick = leaveRoom;
$('playBtn').onclick = play;
$('passBtn').onclick = pass;
$('againBtn').onclick = again;
@@ -343,4 +557,29 @@ document.addEventListener('DOMContentLoaded', function() {
};
$('chatInput').onkeypress = function(e) { if (e.key === 'Enter') $('chatBtn').click(); };
$('roomIdInput').onkeypress = function(e) { if (e.key === 'Enter') join(); };
token = localStorage.getItem('token') || '';
userId = localStorage.getItem('userId') || '';
username = localStorage.getItem('username') || '';
nickname = localStorage.getItem('nickname') || '';
if (token) {
var valid = await validateToken();
if (valid) {
var inRoom = await checkCurrentRoom();
if (inRoom) {
connect();
return;
}
showLobby();
return;
}
localStorage.removeItem('token');
localStorage.removeItem('userId');
localStorage.removeItem('username');
localStorage.removeItem('nickname');
}
show('auth');
getCaptcha('login');
});