Files
whale-town-front-v2/web/progressive_shell.html

229 lines
12 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, 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>