229 lines
12 KiB
HTML
229 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>$GODOT_PROJECT_NAME</title>
|
||
$GODOT_HEAD_INCLUDE
|
||
<style>
|
||
:root { color-scheme: light; font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }
|
||
* { box-sizing: border-box; }
|
||
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b9d9df; }
|
||
button, input { font: inherit; letter-spacing: 0; }
|
||
#canvas { position: fixed; inset: 0; display: block; width: 100%; height: 100%; border: 0; background: #0a2437; }
|
||
#auth-shell { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; background: #b9d9df url("auth-background.jpg") center / cover no-repeat; }
|
||
#auth-shell::before { content: ""; position: absolute; inset: 0; background: rgba(222, 242, 244, .38); }
|
||
.auth-panel { position: relative; width: min(440px, 100%); max-height: calc(100vh - 48px); overflow: auto; padding: 30px 32px 24px; border: 2px solid rgba(38, 112, 151, .68); border-radius: 8px; background: rgba(249, 253, 252, .96); box-shadow: 0 14px 38px rgba(15, 65, 89, .22); }
|
||
.brand { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 22px; color: #174f72; }
|
||
.brand-mark { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: #4c9ac5; color: white; font-size: 27px; font-weight: 800; }
|
||
.brand h1 { margin: 0; font-size: 26px; line-height: 1.1; letter-spacing: 0; }
|
||
.tabs { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 22px; border-bottom: 1px solid #c8dbe2; }
|
||
.tab { min-height: 42px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: #647b86; cursor: pointer; }
|
||
.tab[aria-selected="true"] { border-bottom-color: #257aa8; color: #15577b; font-weight: 700; }
|
||
.form { display: grid; gap: 15px; }
|
||
.form[hidden] { display: none; }
|
||
.field { display: grid; gap: 7px; }
|
||
.field label { color: #294b5b; font-size: 14px; font-weight: 650; }
|
||
.field input { width: 100%; min-height: 44px; padding: 9px 12px; border: 1px solid #a9c4cf; border-radius: 6px; outline: none; background: #fff; color: #102e3d; }
|
||
.field input:focus { border-color: #257aa8; box-shadow: 0 0 0 3px rgba(37, 122, 168, .14); }
|
||
.code-row { display: grid; grid-template-columns: 1fr 126px; gap: 8px; }
|
||
.primary, .secondary { min-height: 44px; border-radius: 6px; cursor: pointer; font-weight: 700; }
|
||
.primary { border: 1px solid #17638e; background: #257aa8; color: #fff; }
|
||
.secondary { border: 1px solid #84adbe; background: #e7f3f5; color: #205f7c; }
|
||
button:disabled { cursor: wait; opacity: .62; }
|
||
.status { min-height: 22px; margin: 13px 0 0; color: #4b6875; font-size: 13px; line-height: 1.5; text-align: center; }
|
||
.status.error { color: #9b3340; }
|
||
.load-track { height: 5px; margin-top: 12px; overflow: hidden; border-radius: 3px; background: #d5e5e9; }
|
||
.load-bar { width: 0; height: 100%; background: #3292b9; transition: width .2s ease; }
|
||
#failure { display: none; margin-top: 12px; color: #9b3340; font-size: 13px; text-align: center; }
|
||
@media (max-width: 560px) {
|
||
#auth-shell { align-items: stretch; padding: 0; }
|
||
.auth-panel { width: 100%; max-height: 100%; margin-top: 18vh; padding: 24px 22px; border-width: 1px 0 0; border-radius: 8px 8px 0 0; }
|
||
.brand { margin-bottom: 16px; }
|
||
.brand h1 { font-size: 23px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="canvas">当前浏览器不支持游戏画布。</canvas>
|
||
<main id="auth-shell">
|
||
<section class="auth-panel" aria-label="鲸鱼小镇账户入口">
|
||
<div class="brand"><span class="brand-mark">W</span><h1>鲸鱼小镇</h1></div>
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab" id="login-tab" type="button" role="tab" aria-selected="true">登录</button>
|
||
<button class="tab" id="register-tab" type="button" role="tab" aria-selected="false">注册</button>
|
||
</div>
|
||
<form class="form" id="login-form">
|
||
<div class="field"><label for="identifier">用户名 / 邮箱 / 手机号</label><input id="identifier" autocomplete="username" maxlength="100" required></div>
|
||
<div class="field"><label for="login-password">密码</label><input id="login-password" type="password" autocomplete="current-password" maxlength="128" required></div>
|
||
<button class="primary" type="submit">进入小镇</button>
|
||
</form>
|
||
<form class="form" id="register-form" hidden>
|
||
<div class="field"><label for="username">用户名</label><input id="username" autocomplete="username" maxlength="50" pattern="[A-Za-z0-9_]+" required></div>
|
||
<div class="field"><label for="email">邮箱</label><div class="code-row"><input id="email" type="email" autocomplete="email" required><button class="secondary" id="send-code" type="button">获取验证码</button></div></div>
|
||
<div class="field"><label for="code">邮箱验证码</label><input id="code" inputmode="numeric" autocomplete="one-time-code" minlength="6" maxlength="6" pattern="[0-9]{6}" required></div>
|
||
<div class="field"><label for="register-password">密码</label><input id="register-password" type="password" autocomplete="new-password" minlength="8" maxlength="128" required></div>
|
||
<div class="field"><label for="confirm-password">确认密码</label><input id="confirm-password" type="password" autocomplete="new-password" minlength="8" maxlength="128" required></div>
|
||
<button class="primary" type="submit">注册居民身份</button>
|
||
</form>
|
||
<p class="status" id="status" aria-live="polite">主程序正在后台加载 0%</p>
|
||
<div class="load-track" aria-hidden="true"><div class="load-bar" id="load-bar"></div></div>
|
||
<div id="failure" role="alert"></div>
|
||
</section>
|
||
</main>
|
||
<noscript>当前浏览器未启用 JavaScript,无法运行鲸鱼小镇。</noscript>
|
||
<script src="$GODOT_URL"></script>
|
||
<script>
|
||
const GODOT_CONFIG = $GODOT_CONFIG;
|
||
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
|
||
const AUTH_STORAGE_KEY = 'whaletown.auth.bootstrap';
|
||
const engine = new Engine(GODOT_CONFIG);
|
||
const shell = document.getElementById('auth-shell');
|
||
const canvas = document.getElementById('canvas');
|
||
const status = document.getElementById('status');
|
||
const loadBar = document.getElementById('load-bar');
|
||
const failure = document.getElementById('failure');
|
||
let engineReady = false;
|
||
let authReady = false;
|
||
|
||
function selectTab(name) {
|
||
const login = name === 'login';
|
||
document.getElementById('login-tab').setAttribute('aria-selected', String(login));
|
||
document.getElementById('register-tab').setAttribute('aria-selected', String(!login));
|
||
document.getElementById('login-form').hidden = !login;
|
||
document.getElementById('register-form').hidden = login;
|
||
setMessage(engineReady ? '主程序已就绪' : status.textContent, false);
|
||
}
|
||
|
||
function setMessage(message, isError) {
|
||
status.textContent = message;
|
||
status.classList.toggle('error', Boolean(isError));
|
||
}
|
||
|
||
function setFormsDisabled(disabled) {
|
||
document.querySelectorAll('form input, form button').forEach((element) => { element.disabled = disabled; });
|
||
}
|
||
|
||
async function postJson(path, body) {
|
||
const response = await fetch('/api' + path, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
});
|
||
let payload = {};
|
||
try { payload = await response.json(); } catch (_) { throw new Error('服务器响应格式错误'); }
|
||
if (!response.ok || !payload.success) throw new Error(payload.message || '请求失败');
|
||
return payload;
|
||
}
|
||
|
||
function canRecoverRegistration(error) {
|
||
const message = String(error && error.message ? error.message : error);
|
||
return error instanceof TypeError || message.includes('用户名已存在') || message.includes('用户名已被注册');
|
||
}
|
||
|
||
async function registerWithRecovery(body) {
|
||
try {
|
||
return await postJson('/auth/register', body);
|
||
} catch (registerError) {
|
||
if (!canRecoverRegistration(registerError)) throw registerError;
|
||
try {
|
||
return await postJson('/auth/login', { identifier: body.username, password: body.password });
|
||
} catch (_) {
|
||
throw registerError;
|
||
}
|
||
}
|
||
}
|
||
|
||
function handOffAuth(kind, payload) {
|
||
sessionStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ kind, payload }));
|
||
authReady = true;
|
||
setFormsDisabled(true);
|
||
setMessage(engineReady ? '正在进入小镇...' : '登录成功,主程序仍在后台加载...', false);
|
||
showGameWhenReady();
|
||
}
|
||
|
||
function showGameWhenReady() {
|
||
if (!engineReady || !authReady) return;
|
||
canvas.focus();
|
||
shell.style.display = 'none';
|
||
}
|
||
|
||
document.getElementById('login-tab').addEventListener('click', () => selectTab('login'));
|
||
document.getElementById('register-tab').addEventListener('click', () => selectTab('register'));
|
||
document.getElementById('login-form').addEventListener('submit', async (event) => {
|
||
event.preventDefault();
|
||
setFormsDisabled(true);
|
||
setMessage('正在登录...', false);
|
||
try {
|
||
const payload = await postJson('/auth/login', {
|
||
identifier: document.getElementById('identifier').value.trim(),
|
||
password: document.getElementById('login-password').value,
|
||
});
|
||
handOffAuth('login', payload);
|
||
} catch (error) {
|
||
setFormsDisabled(false);
|
||
setMessage(error.message || '登录失败', true);
|
||
}
|
||
});
|
||
document.getElementById('send-code').addEventListener('click', async () => {
|
||
const email = document.getElementById('email').value.trim();
|
||
if (!email) { setMessage('请先输入邮箱', true); return; }
|
||
const button = document.getElementById('send-code');
|
||
button.disabled = true;
|
||
setMessage('正在发送验证码...', false);
|
||
try {
|
||
await postJson('/auth/send-email-verification', { email });
|
||
setMessage('验证码已发送,请查收邮件', false);
|
||
} catch (error) {
|
||
setMessage(error.message || '验证码发送失败', true);
|
||
} finally { button.disabled = false; }
|
||
});
|
||
document.getElementById('register-form').addEventListener('submit', async (event) => {
|
||
event.preventDefault();
|
||
const username = document.getElementById('username').value.trim();
|
||
const password = document.getElementById('register-password').value;
|
||
if (password !== document.getElementById('confirm-password').value) { setMessage('两次输入的密码不一致', true); return; }
|
||
if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) { setMessage('密码需要同时包含字母和数字', true); return; }
|
||
setFormsDisabled(true);
|
||
setMessage('正在注册...', false);
|
||
try {
|
||
const payload = await registerWithRecovery({
|
||
username,
|
||
nickname: username,
|
||
email: document.getElementById('email').value.trim(),
|
||
email_verification_code: document.getElementById('code').value.trim(),
|
||
password,
|
||
});
|
||
handOffAuth('register', payload);
|
||
} catch (error) {
|
||
setFormsDisabled(false);
|
||
setMessage(error.message || '注册失败', true);
|
||
}
|
||
});
|
||
|
||
const missing = Engine.getMissingFeatures({ threads: GODOT_THREADS_ENABLED });
|
||
if (missing.length) {
|
||
failure.style.display = 'block';
|
||
failure.textContent = '当前浏览器缺少运行游戏所需能力:' + missing.join('、');
|
||
} else {
|
||
engine.startGame({
|
||
onProgress(current, total) {
|
||
if (total <= 0) return;
|
||
const percent = Math.min(100, Math.round(current / total * 100));
|
||
loadBar.style.width = percent + '%';
|
||
if (!authReady) setMessage('主程序正在后台加载 ' + percent + '%', false);
|
||
},
|
||
}).then(() => {
|
||
engineReady = true;
|
||
loadBar.style.width = '100%';
|
||
if (!authReady) setMessage('主程序已就绪', false);
|
||
showGameWhenReady();
|
||
}).catch((error) => {
|
||
failure.style.display = 'block';
|
||
failure.textContent = error && error.message ? error.message : '主程序加载失败,请刷新重试';
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|