增加用户管理
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user