first commit

This commit is contained in:
浪子
2026-03-19 11:07:49 +08:00
commit 7e927accac
20 changed files with 6322 additions and 0 deletions
+1092
View File
File diff suppressed because it is too large Load Diff
+1403
View File
File diff suppressed because it is too large Load Diff
+282
View File
@@ -0,0 +1,282 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>在线聊天室</title>
<script>
(() => {
try {
const saved = localStorage.getItem("theme");
const theme =
saved === "dark" || saved === "light"
? saved
: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.dataset.theme = theme;
} catch {}
})();
</script>
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<div class="scene-container">
<main class="chat-room" id="chatRoom">
<div class="chat-interface">
<header class="topbar">
<div class="brand">聊天室</div>
<div class="topbarRight">
<div class="me" id="meBox"></div>
<button class="btn iconBtn themeBtn" id="themeBtn" type="button" aria-label="切换黑夜模式" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z" />
</svg>
</button>
<button class="btn iconBtn menuBtn" id="menuBtn" type="button" aria-label="账户与管理">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21a8 8 0 0 0-16 0" />
<circle cx="12" cy="8" r="4" />
</svg>
</button>
</div>
</header>
<div class="menuDropdown" id="menuDropdown" hidden>
<div class="menuDropdownCard card" id="menuDropdownCard">
<div class="menuDropdownHeader">
<div class="menuDropdownTitle">菜单</div>
<button class="btn iconBtn" id="menuCloseBtn" type="button" aria-label="关闭菜单">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18" />
<path d="M6 6l12 12" />
</svg>
</button>
</div>
<div id="menuHost">
<details class="card fold" id="accountFold">
<summary class="foldSummary">账户</summary>
<div class="foldBody">
<div id="authBox"></div>
</div>
</details>
<details class="card fold" id="adminCard" style="display:none">
<summary class="foldSummary">管理</summary>
<div class="foldBody">
<div class="row">
<button id="loadUsersBtn" class="btn">刷新用户</button>
<button id="lobbyPwdBtn" class="btn">大厅密码</button>
</div>
<div id="adminToolsBox" class="stack" style="margin-top:10px" hidden></div>
<div class="hint" style="margin-top:10px">用户列表</div>
<div id="adminUsersBox" class="admin"></div>
</div>
</details>
</div>
</div>
</div>
<main class="layout">
<aside class="sidebar">
<div class="card" id="onlineCard">
<div class="row" style="justify-content:space-between; align-items:center">
<div style="font-weight:900; color:var(--text-main); font-size:13px">在线用户</div>
<div class="hint" id="onlineCount"></div>
</div>
<div class="onlineList" id="onlineList"></div>
</div>
</aside>
<section class="chat">
<div class="chatHeader">
<div>
<div class="chatTitle" id="roomTitle">未进入聊天室</div>
<div class="chatSubtitle" id="roomSubtitle"></div>
</div>
<div class="row">
<button id="leaveRoomBtn" class="btn" disabled>离开</button>
</div>
</div>
<div class="messages" id="messages"></div>
<div class="composer">
<div class="composerRow">
<button class="btn iconBtn toolBtn" id="emojiBtn" type="button" aria-label="表情" disabled>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
<path d="M9 9h.01" />
<path d="M15 9h.01" />
</svg>
</button>
<button class="btn iconBtn toolBtn" id="imageBtn" type="button" aria-label="图片" disabled hidden>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14Z" />
<path d="M8.5 10.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path d="m21 15-5-5L5 21" />
</svg>
</button>
<div class="msgInputWrap" id="msgInputWrap">
<img id="inlineThumb" class="inlineThumb" alt="待发送图片" hidden />
<input id="msgInput" class="input" placeholder="输入消息…" disabled />
</div>
<button id="sendBtn" class="btn btnPrimary" disabled>发送</button>
</div>
<input id="fileInput" type="file" accept="image/*" hidden />
<div class="composerAttach" id="composerAttach" hidden></div>
<div class="emojiPanel" id="emojiPanel" hidden></div>
</div>
</section>
</main>
</div>
</main>
<div class="doors-container" id="doorsContainer" aria-hidden="true">
<div class="door left">
<div class="door-handle"></div>
</div>
<div class="door right">
<div class="door-handle"></div>
</div>
</div>
<div class="login-wrapper" id="loginWrapper">
<div class="login-card">
<h1>进入聊天室</h1>
<p>门已锁好,请验证身份进入</p>
<div class="gateTabs">
<button class="btn btnPrimary" id="gateTabLogin" type="button">登录</button>
<button class="btn" id="gateTabRegister" type="button">注册</button>
<button class="btn" id="gateTabGuest" type="button">游客登录</button>
</div>
</div>
</div>
<!-- 登录/注册/游客 模态框 -->
<div class="confirm" id="gateModal" hidden>
<div class="confirmCard gateModalCard">
<div class="gateModalHeader">
<div class="confirmTitle" id="gateModalTitle"></div>
<button class="btn iconBtn" id="gateModalClose" type="button" aria-label="关闭">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18" /><path d="M6 6l12 12" />
</svg>
</button>
</div>
<div class="error-msg" id="gateError"></div>
<form id="gateLoginForm" class="gatePanel" hidden>
<div class="input-group">
<label for="gateLoginUser">用户名</label>
<input id="gateLoginUser" autocomplete="username" />
</div>
<div class="input-group">
<label for="gateLoginPass">密码</label>
<input id="gateLoginPass" type="password" autocomplete="current-password" />
</div>
<button class="login-btn" type="submit">登录并进入</button>
<button class="link" type="button" id="forgotPasswordBtn">找回密码</button>
</form>
<form id="gateRegisterForm" class="gatePanel" hidden>
<div class="input-group">
<label for="gateRegUser">用户名</label>
<input id="gateRegUser" autocomplete="username" />
</div>
<div class="input-group">
<label for="gateRegPass">密码</label>
<input id="gateRegPass" type="password" autocomplete="new-password" />
</div>
<div class="input-group">
<label for="gateRegEmail">邮箱(选填)</label>
<input id="gateRegEmail" autocomplete="email" />
</div>
<div class="input-group">
<label for="gateRegQq">QQ(选填)</label>
<input id="gateRegQq" autocomplete="off" />
</div>
<div class="input-group">
<label for="gateRegPhone">联系电话(选填)</label>
<input id="gateRegPhone" autocomplete="tel" />
</div>
<button class="login-btn" type="submit">注册并进入</button>
</form>
<form id="gateGuestForm" class="gatePanel" hidden>
<input id="gateGuestUser" class="srOnly" name="username" autocomplete="username" tabindex="-1" />
<div class="hint" id="gateGuestNameHint"></div>
<div class="input-group">
<label for="gateGuestPass">聊天室密码(若已设置)</label>
<input id="gateGuestPass" type="password" autocomplete="current-password" />
<div class="hint" id="gateGuestHint"></div>
</div>
<button class="login-btn" type="submit">游客进入</button>
</form>
</div>
</div>
<!-- 找回密码模态框 -->
<div class="confirm" id="forgotModal" hidden>
<div class="confirmCard" style="width:min(480px,92vw)">
<div class="confirmTitle" id="forgotModalTitle">找回密码</div>
<!-- 步骤1 -->
<div id="forgotStep1" class="stack" style="margin-top:12px">
<div class="hint">请输入你的用户名,我们将向注册邮箱发送 6 位数字验证码。</div>
<div class="input-group">
<label for="forgotUsername">用户名</label>
<input id="forgotUsername" autocomplete="username" />
</div>
<div class="hint" id="forgotStep1Msg"></div>
<div class="row" style="justify-content:flex-end;margin-top:4px">
<button class="btn" id="forgotCancelBtn" type="button">取消</button>
<button class="btn btnPrimary" id="forgotSendBtn" type="button">发送验证码</button>
</div>
</div>
<!-- 步骤2 -->
<div id="forgotStep2" class="stack" style="margin-top:12px;display:none">
<div class="hint">请输入邮件中收到的 6 位数字验证码和新密码。验证码有效期 30 分钟。</div>
<div class="input-group">
<label for="forgotToken">验证码</label>
<input id="forgotToken" maxlength="6" pattern="[0-9]*" inputmode="numeric" autocomplete="one-time-code" />
</div>
<div class="input-group">
<label for="forgotNewPass">新密码</label>
<input id="forgotNewPass" type="password" autocomplete="new-password" />
</div>
<div class="hint" id="forgotStep2Msg"></div>
<div class="row" style="justify-content:flex-end;margin-top:4px">
<button class="btn" id="forgotBackBtn" type="button">返回</button>
<button class="btn btnPrimary" id="forgotResetBtn" type="button">重置密码</button>
</div>
</div>
</div>
</div>
<div class="imgViewer" id="imgViewer" hidden>
<div class="imgViewerBackdrop" id="imgViewerBackdrop"></div>
<img class="imgViewerImg" id="imgViewerImg" alt="图片预览" />
<button class="imgViewerClose" id="imgViewerClose" type="button">关闭</button>
</div>
<div class="toast" id="toast" hidden></div>
<div class="confirm" id="confirm" hidden>
<div class="confirmCard">
<div class="confirmTitle" id="confirmTitle"></div>
<div class="confirmMsg" id="confirmMsg"></div>
<div class="row" style="justify-content:flex-end">
<button class="btn" id="confirmCancel" type="button">取消</button>
<button class="btn btnPrimary" id="confirmOk" type="button">确定</button>
</div>
</div>
</div>
</div>
<script src="/app.js"></script>
</body>
</html>
+153
View File
@@ -0,0 +1,153 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>找回密码</title>
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<div class="center">
<div class="card wide">
<div class="cardTitle">找回密码</div>
<!-- 步骤1: 请求发送重置邮件 -->
<div id="requestStep" class="stack">
<div class="hint">请输入你的用户名,我们将向你注册时填写的邮箱发送 6 位数字验证码。</div>
<input id="requestUsername" class="input" placeholder="用户名" autocomplete="username" />
<button id="requestBtn" class="btn btnPrimary">发送验证码</button>
<div id="requestMsg" class="hint"></div>
<a class="link" href="/">返回聊天室</a>
</div>
<!-- 步骤2: 输入 token 重置密码 -->
<div id="resetStep" class="stack" style="display: none;">
<div class="hint">请输入邮件中收到的 6 位数字验证码和新密码。验证码有效期 30 分钟。</div>
<input id="token" class="input" placeholder="6 位数字验证码" maxlength="6" pattern="[0-9]*" inputmode="numeric" />
<input id="newPassword" class="input" placeholder="新密码(至少6位)" type="password" autocomplete="new-password" />
<button id="resetBtn" class="btn btnPrimary">重置密码</button>
<div id="resetMsg" class="hint"></div>
<button id="backToRequestBtn" class="btn" style="margin-top: 10px;">返回重新发送</button>
<a class="link" href="/">返回聊天室</a>
</div>
</div>
</div>
<script type="module">
const $ = (id) => document.getElementById(id);
const params = new URLSearchParams(location.search);
// 如果 URL 中有 token,直接显示重置步骤
const urlToken = params.get("token");
if (urlToken) {
$("token").value = urlToken;
$("requestStep").style.display = "none";
$("resetStep").style.display = "block";
}
// 请求发送重置邮件
$("requestBtn").addEventListener("click", async (e) => {
e.preventDefault();
const username = $("requestUsername").value.trim();
if (!username) {
$("requestMsg").textContent = "请输入用户名";
$("requestMsg").style.color = "red";
return;
}
$("requestBtn").disabled = true;
$("requestMsg").textContent = "发送中...";
$("requestMsg").style.color = "";
try {
const resp = await fetch("/api/auth/request-password-reset", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ username }),
});
const data = await resp.json().catch(() => ({}));
if (resp.ok) {
// 开发模式可能返回 devResetLink
if (data.devResetLink) {
$("requestMsg").innerHTML = `开发模式:<a href="${data.devResetLink}" class="link">点击这里重置密码</a>`;
$("requestMsg").style.color = "green";
} else {
$("requestMsg").textContent = data.message || "✅ 重置链接已发送到邮箱,请查收";
$("requestMsg").style.color = "green";
// 切换到重置步骤
setTimeout(() => {
$("requestStep").style.display = "none";
$("resetStep").style.display = "block";
}, 2000);
}
} else {
$("requestMsg").textContent = data.error || "请求失败,请稍后重试";
$("requestMsg").style.color = "red";
}
} catch (err) {
$("requestMsg").textContent = "网络错误,请稍后重试";
$("requestMsg").style.color = "red";
} finally {
$("requestBtn").disabled = false;
}
});
// 重置密码
$("resetBtn").addEventListener("click", async (e) => {
e.preventDefault();
const token = $("token").value.trim();
const newPassword = $("newPassword").value;
if (!token) {
$("resetMsg").textContent = "请输入验证码";
$("resetMsg").style.color = "red";
return;
}
if (!/^\d{6}$/.test(token)) {
$("resetMsg").textContent = "验证码必须是 6 位数字";
$("resetMsg").style.color = "red";
return;
}
if (newPassword.length < 6) {
$("resetMsg").textContent = "密码至少需要 6 位";
$("resetMsg").style.color = "red";
return;
}
$("resetBtn").disabled = true;
$("resetMsg").textContent = "重置中...";
$("resetMsg").style.color = "";
try {
const resp = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token, newPassword }),
});
const data = await resp.json().catch(() => ({}));
if (resp.ok) {
$("resetMsg").textContent = "✅ 密码已重置成功!请返回登录";
$("resetMsg").style.color = "green";
} else {
$("resetMsg").textContent = data.error || "重置失败,验证码可能已过期";
$("resetMsg").style.color = "red";
}
} catch (err) {
$("resetMsg").textContent = "网络错误,请稍后重试";
$("resetMsg").style.color = "red";
} finally {
$("resetBtn").disabled = false;
}
});
// 返回重新发送
$("backToRequestBtn").addEventListener("click", () => {
$("resetStep").style.display = "none";
$("requestStep").style.display = "block";
$("requestMsg").textContent = "";
$("resetMsg").textContent = "";
});
</script>
</body>
</html>