game/login.html
2026-07-28 21:46:38 +08:00

247 lines
11 KiB
HTML
Raw 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, viewport-fit=cover" />
<title>宇森 · 登录</title>
<link rel="icon" type="image/png" sizes="48x48" href="assets/ui/logo_planet_48.png?v=20260723">
<link rel="icon" type="image/png" sizes="256x256" href="assets/ui/logo_planet_256.png?v=20260723">
<style>
:root{
--bg-0:#080c1a; --bg-1:#0e1730; --bg-2:#15213f;
--violet:#8b5cf6; --magenta:#c77dff; --cyan:#4cc9f0; --gold:#e7b85c;
--text:#e9edf6; --text-dim:#9aa6c4;
--card:rgba(255,255,255,.05); --card-bd:rgba(255,255,255,.12);
--glow:0 0 22px rgba(139,92,246,.30);
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;}
body{
font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",system-ui,-apple-system,sans-serif;color:var(--text);
background:#000;
display:flex;align-items:center;justify-content:center;min-height:100dvh;overflow:hidden;position:relative;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
/* 关闭装饰光斑,避免遮挡背景轮播 */
body::before,body::after{display:none;}
/* 登录页背景(固定单图 login_bg原图直出不加特效 */
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.webp') center/cover no-repeat;}
.bg-slides{position:fixed;inset:0;z-index:0;background:url('assets/slides/login_bg.png') center/cover no-repeat;}
.wrap{position:relative;z-index:2;width:min(420px,92vw);}
.brand{text-align:center;margin-bottom:26px;}
.brand .cn{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;font-weight:900;font-size:42px;letter-spacing:8px;color:#fff;text-shadow:0 0 18px rgba(139,92,246,.6);}
.brand .en{font-size:16px;color:var(--magenta);letter-spacing:5px;margin-top:8px;font-weight:600;text-shadow:0 1px 6px rgba(0,0,0,.7);}
.brand .tag{font-size:13px;color:var(--text-dim);margin-top:10px;}
.card{background:var(--card);border:1px solid var(--card-bd);border-radius:18px;padding:26px 26px 30px;backdrop-filter:blur(10px);box-shadow:var(--glow);}
.tabs{display:flex;gap:6px;margin-bottom:20px;background:rgba(8,12,26,.5);border-radius:12px;padding:5px;}
.tabs button{flex:1;font-family:inherit;font-size:14px;font-weight:500;color:var(--text-dim);background:transparent;border:none;padding:9px;border-radius:9px;cursor:pointer;transition:.2s;}
.tabs button.active{color:#fff;background:rgba(139,92,246,.28);box-shadow:var(--glow);}
.field{margin-bottom:14px;}
.field label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:6px;letter-spacing:1px;}
.field input{width:100%;background:rgba(8,12,26,.6);border:1px solid var(--card-bd);border-radius:10px;color:var(--text);padding:11px 13px;font-family:inherit;font-size:14px;outline:none;transition:.2s;}
.field input:focus{border-color:var(--violet);box-shadow:0 0 0 3px rgba(139,92,246,.18);}
.pw-wrap{position:relative;}
.pw-wrap input{padding-right:40px;}
.pw-toggle,.random-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:transparent;border:none;color:var(--text-dim);cursor:pointer;font-size:16px;line-height:1;padding:4px 6px;border-radius:6px;transition:.2s;}
.pw-toggle:hover,.random-btn:hover{color:#fff;background:rgba(255,255,255,.08);}
.random-btn{font-size:15px;}
.submit{width:100%;margin-top:6px;font-size:15px;font-weight:700;color:#0b1020;background:linear-gradient(120deg,var(--gold),#f0d49a);padding:12px;border-radius:11px;cursor:pointer;border:none;letter-spacing:2px;transition:.22s;box-shadow:0 0 18px rgba(231,184,92,.35);}
.submit:hover{filter:brightness(1.08);}
.submit:disabled{opacity:.6;cursor:default;}
.msg{margin-top:14px;font-size:13px;text-align:center;min-height:18px;}
.msg.err{color:#ff8a8a;}
.msg.ok{color:#8be39a;}
.back{text-align:center;margin-top:18px;font-size:13px;color:var(--text-dim);}
.back a{color:var(--cyan);text-decoration:none;}
.back a:hover{text-decoration:underline;}
.hidden{display:none;}
@media (max-width:420px){
.brand .cn{font-size:34px;letter-spacing:5px;}
.card{padding:22px 18px 26px;}
.field input{padding:12px 13px;font-size:15px;}
.submit{padding:13px;font-size:15px;}
}
</style>
</head>
<body>
<div class="bg-slides"></div>
<div class="wrap">
<div class="brand">
<div class="cn">宇森</div>
<div class="en">YUSEN · GARDEN OF CIVILIZATION</div>
</div>
<div class="card">
<div class="tabs">
<button id="tabLogin" class="active">登录</button>
<button id="tabReg">注册</button>
</div>
<!-- 登录 -->
<form id="loginForm" autocomplete="off">
<div class="field">
<label>账号</label>
<input id="lLogin" type="text" placeholder="用户名" />
</div>
<div class="field">
<label>密码</label>
<div class="pw-wrap">
<input id="lPwd" type="password" placeholder="密码" />
<button type="button" class="pw-toggle" data-target="lPwd" aria-label="显示密码">👁</button>
</div>
</div>
<button class="submit" type="submit">进入世界</button>
</form>
<!-- 注册 -->
<form id="regForm" class="hidden" autocomplete="off">
<div class="field">
<label>账号</label>
<input id="rLogin" type="text" placeholder="登录用账号(字母/数字/._-" />
</div>
<div class="field">
<label>昵称</label>
<div class="pw-wrap">
<input id="rName" type="text" placeholder="可随机生成,也可自定义" />
<button type="button" class="random-btn" id="randomNameBtn" aria-label="随机昵称">🎲</button>
</div>
</div>
<div class="field">
<label>密码</label>
<div class="pw-wrap">
<input id="rPwd" type="password" placeholder="至少 8 位,字母+数字" />
<button type="button" class="pw-toggle" data-target="rPwd" aria-label="显示密码">👁</button>
</div>
</div>
<div class="field">
<label>确认密码</label>
<div class="pw-wrap">
<input id="rPwd2" type="password" placeholder="再次输入密码" />
<button type="button" class="pw-toggle" data-target="rPwd2" aria-label="显示密码">👁</button>
</div>
</div>
<button class="submit" type="submit">创建账号</button>
</form>
<div class="msg" id="msg"></div>
</div>
<div class="back"><a href="index.html">← 返回官网</a></div>
</div>
<script src="assets/game-api.js"></script>
<script>
const tabLogin = document.getElementById('tabLogin');
const tabReg = document.getElementById('tabReg');
const loginForm = document.getElementById('loginForm');
const regForm = document.getElementById('regForm');
const msg = document.getElementById('msg');
// 回跳目标(进入游戏流程带 ?next=world.html
const params = new URLSearchParams(location.search);
const nextUrl = params.get('next') || 'world.html';
// 已登录则直接跳走
(async () => {
try { const me = await API.me(); if (me && me.uid) location.href = nextUrl; } catch (e) {}
})();
function showMsg(text, kind){ msg.textContent = text || ''; msg.className = 'msg' + (kind ? ' ' + kind : ''); }
function switchTab(which){
if (which === 'login'){
tabLogin.classList.add('active'); tabReg.classList.remove('active');
loginForm.classList.remove('hidden'); regForm.classList.add('hidden');
} else {
tabReg.classList.add('active'); tabLogin.classList.remove('active');
regForm.classList.remove('hidden'); loginForm.classList.add('hidden');
}
showMsg('');
}
tabLogin.onclick = () => switchTab('login');
tabReg.onclick = () => switchTab('reg');
// 密码显示/隐藏切换(眼睛按钮)
document.querySelectorAll('.pw-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const input = document.getElementById(btn.dataset.target);
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
btn.textContent = '🙈';
btn.setAttribute('aria-label', '隐藏密码');
} else {
input.type = 'password';
btn.textContent = '👁';
btn.setAttribute('aria-label', '显示密码');
}
input.focus();
});
});
// 随机昵称生成
const rName = document.getElementById('rName');
const ADJ = ['云游','星尘','微光','风吟','夜语','深海','荒原','天穹','逐风','灵叶','虚境','晨星','暮雨','浮世','苍穹'];
const NOUN = ['旅者','观察者','行者','拾光人','园丁','筑梦师','游侠','学者','守夜人','引路人','浪人','渡鸦','白鹿'];
function randomName() {
const a = ADJ[Math.floor(Math.random() * ADJ.length)];
const n = NOUN[Math.floor(Math.random() * NOUN.length)];
const num = String(Math.floor(Math.random() * 9000) + 1000);
return a + n + num;
}
document.getElementById('randomNameBtn').addEventListener('click', () => {
rName.value = randomName();
});
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
showMsg('');
const login = document.getElementById('lLogin').value.trim();
const password = document.getElementById('lPwd').value;
if (!login || !password){ showMsg('请填写账号和密码', 'err'); return; }
const btn = loginForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '登录中…';
try {
const r = await API.login(login, password);
if (r.ok){ location.href = nextUrl; }
else { showMsg(r.error || '登录失败', 'err'); }
} catch (err) {
showMsg('网络错误,请稍后重试', 'err');
} finally {
btn.disabled = false; btn.textContent = '进入世界';
}
});
regForm.addEventListener('submit', async (e) => {
e.preventDefault();
showMsg('');
const login = document.getElementById('rLogin').value.trim();
const name = document.getElementById('rName').value.trim();
const password = document.getElementById('rPwd').value;
const pwd2 = document.getElementById('rPwd2').value;
if (!login) { showMsg('请填写账号', 'err'); return; }
if (password.length < 8) { showMsg('密码至少 8 位', 'err'); return; }
if (!/[a-zA-Z]/.test(password) || !/\d/.test(password)) {
showMsg('密码必须同时包含字母和数字', 'err'); return;
}
if (password !== pwd2) { showMsg('两次密码输入不一致', 'err'); return; }
const btn = regForm.querySelector('.submit'); btn.disabled = true; btn.textContent = '注册中…';
try {
const r = await API.register({ name, login, password });
if (r.ok){ location.href = nextUrl; }
else { showMsg(r.error || '注册失败', 'err'); }
} catch (err) {
showMsg('网络错误,请稍后重试', 'err');
} finally {
btn.disabled = false; btn.textContent = '创建账号';
}
});
</script>
</body>
</html>