(() => {
const $ = (id) => document.getElementById(id);
const state = {
me: null,
myAnonName: null,
accessToken: null,
currentRoom: null,
lastMessageId: 0,
es: null,
pendingGuestName: null,
pendingImageFile: null,
pendingImageUrl: null,
};
const GUEST_SESSION_KEY = "guest_session_v1";
const GUEST_SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const THEME_KEY = "theme";
function clearGuestSession() {
try {
localStorage.removeItem(GUEST_SESSION_KEY);
} catch {}
}
function getCurrentTheme() {
const t = document.documentElement?.dataset?.theme;
return t === "dark" ? "dark" : "light";
}
function setTheme(theme) {
const t = theme === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = t;
try {
localStorage.setItem(THEME_KEY, t);
} catch {}
renderThemeBtn();
}
function renderThemeBtn() {
const btn = $("themeBtn");
if (!btn) return;
const isDark = getCurrentTheme() === "dark";
btn.setAttribute("aria-pressed", isDark ? "true" : "false");
btn.setAttribute("aria-label", isDark ? "切换亮色模式" : "切换黑夜模式");
btn.innerHTML = isDark
? `
`
: `
`;
}
function loadGuestSession() {
let raw = null;
try {
raw = localStorage.getItem(GUEST_SESSION_KEY);
} catch {
return { exists: false, session: null };
}
if (!raw) return { exists: false, session: null };
try {
const data = JSON.parse(raw);
const expired = typeof data?.expiresAt !== "number" || data.expiresAt <= Date.now();
if (data?.v !== 1 || expired) {
clearGuestSession();
return { exists: true, session: null };
}
const roomId = typeof data.roomId === "string" ? data.roomId : "";
const nickname = typeof data.nickname === "string" ? data.nickname.trim() : "";
const accessToken = typeof data.accessToken === "string" ? data.accessToken : null;
if (!roomId || !nickname) {
clearGuestSession();
return { exists: true, session: null };
}
return { exists: true, session: { roomId, nickname, accessToken } };
} catch {
clearGuestSession();
return { exists: true, session: null };
}
}
function saveGuestSession({ roomId, nickname, accessToken }) {
try {
localStorage.setItem(
GUEST_SESSION_KEY,
JSON.stringify({
v: 1,
roomId,
nickname,
accessToken,
expiresAt: Date.now() + GUEST_SESSION_TTL_MS,
}),
);
} catch {}
}
function randomGuestName() {
return `游客-${Math.floor(1000 + Math.random() * 9000)}`;
}
let toastTimer = null;
function toast(message, kind = "info", ms = 2200) {
const el = $("toast");
if (!el) return;
el.classList.remove("ok", "bad");
if (kind === "ok") el.classList.add("ok");
if (kind === "bad") el.classList.add("bad");
el.textContent = message;
el.hidden = false;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
el.hidden = true;
}, ms);
}
function confirmDialog({ title, message, okText = "确定", cancelText = "取消" }) {
const overlay = $("confirm");
const titleEl = $("confirmTitle");
const msgEl = $("confirmMsg");
const okBtn = $("confirmOk");
const cancelBtn = $("confirmCancel");
if (!overlay || !titleEl || !msgEl || !okBtn || !cancelBtn) return Promise.resolve(false);
titleEl.textContent = title || "确认";
msgEl.textContent = message || "";
okBtn.textContent = okText;
cancelBtn.textContent = cancelText;
overlay.hidden = false;
return new Promise((resolve) => {
const cleanup = () => {
overlay.hidden = true;
okBtn.removeEventListener("click", onOk);
cancelBtn.removeEventListener("click", onCancel);
window.removeEventListener("keydown", onKey);
};
const onOk = () => {
cleanup();
resolve(true);
};
const onCancel = () => {
cleanup();
resolve(false);
};
const onKey = (e) => {
if (e.key === "Escape") onCancel();
};
okBtn.addEventListener("click", onOk);
cancelBtn.addEventListener("click", onCancel);
window.addEventListener("keydown", onKey);
});
}
function setFoldDefaultsOnce() {
const isSmall = window.matchMedia("(max-width: 900px)").matches;
const accountFold = $("accountFold");
if (accountFold) accountFold.open = !isSmall;
const adminFold = $("adminCard");
if (adminFold && adminFold.style.display !== "none") adminFold.open = !isSmall;
}
function setupMobileMenu() {
const menuBtn = $("menuBtn");
const menu = $("menuDropdown");
const menuClose = $("menuCloseBtn");
const menuHost = $("menuHost");
const sidebar = document.querySelector(".sidebar");
const onlineCard = $("onlineCard");
if (!menuBtn || !menu || !menuClose || !menuHost || !sidebar || !onlineCard) return;
const mq = window.matchMedia("(max-width: 900px)");
const closeMenu = () => {
menu.hidden = true;
};
const openMenu = () => {
menu.hidden = false;
setFoldDefaultsOnce();
};
const syncPlacement = () => {
if (mq.matches) {
if (onlineCard.parentElement !== menuHost) menuHost.prepend(onlineCard);
closeMenu();
} else {
if (onlineCard.parentElement !== sidebar) sidebar.prepend(onlineCard);
closeMenu();
}
};
syncPlacement();
if (mq.addEventListener) mq.addEventListener("change", syncPlacement);
else mq.addListener(syncPlacement);
menuBtn.addEventListener("click", () => {
if (menu.hidden) openMenu();
else closeMenu();
});
menuClose.addEventListener("click", closeMenu);
document.addEventListener("click", (e) => {
if (menu.hidden) return;
const target = e.target;
if (target === menuBtn || menuBtn.contains(target)) return;
if (menu.contains(target)) return;
closeMenu();
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeMenu();
});
}
function renderOnlineList() {
const list = $("onlineList");
const count = $("onlineCount");
if (!list || !count) return;
const items = [];
if (state.me) {
items.push({ name: state.me.username, level: state.me.level, isMe: true });
} else if (state.myAnonName) {
items.push({ name: state.myAnonName, level: 0, isMe: true });
}
count.textContent = items.length ? String(items.length) : "";
list.innerHTML = "";
if (!items.length) {
const empty = document.createElement("div");
empty.className = "hint";
empty.textContent = "未进入聊天室";
list.appendChild(empty);
return;
}
for (const it of items) {
const row = document.createElement("div");
row.className = "onlineItem";
const nameClass = it.level === 3 ? "userNameAdmin" : it.level === 2 ? "userNameVerified" : it.level === 1 ? "userNameMember" : "";
row.innerHTML = `
${escapeHtml(it.name)}
${it.isMe ? "(我)" : ""}
`;
list.appendChild(row);
}
}
function prepareGuestName() {
const hint = $("gateGuestNameHint");
const userInput = $("gateGuestUser");
const name = randomGuestName();
state.pendingGuestName = name;
if (hint) hint.textContent = `将以随机昵称进入:${name}`;
if (userInput) userInput.value = name;
}
function closeEmojiPanel() {
const panel = $("emojiPanel");
if (panel) panel.hidden = true;
}
function buildEmojiPanelOnce() {
const panel = $("emojiPanel");
if (!panel || panel.childElementCount) return;
const emojis = [
"😀",
"😁",
"😂",
"🤣",
"😊",
"😍",
"😭",
"😡",
"👍",
"👎",
"🙏",
"🎉",
"❤️",
"🔥",
"💯",
"🤔",
"🙃",
"😅",
"😎",
"😴",
"😱",
"👀",
"🙌",
"🥳",
"🤝",
"🤖",
"🐱",
"🐶",
"🌟",
"✅",
"❌",
];
const grid = document.createElement("div");
grid.className = "emojiGrid";
for (const e of emojis) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "emojiBtn";
btn.textContent = e;
btn.setAttribute("aria-label", e);
btn.addEventListener("click", () => {
insertIntoMsgInput(e);
closeEmojiPanel();
});
grid.appendChild(btn);
}
panel.appendChild(grid);
}
function insertIntoMsgInput(text) {
const input = $("msgInput");
if (!input) return;
input.focus();
const value = input.value || "";
const start = typeof input.selectionStart === "number" ? input.selectionStart : value.length;
const end = typeof input.selectionEnd === "number" ? input.selectionEnd : value.length;
input.value = value.slice(0, start) + text + value.slice(end);
const next = start + text.length;
try {
input.setSelectionRange(next, next);
} catch {}
}
function clearPendingImage() {
if (state.pendingImageUrl) {
try {
URL.revokeObjectURL(state.pendingImageUrl);
} catch {}
}
state.pendingImageFile = null;
state.pendingImageUrl = null;
renderAttachment();
}
function setPendingImage(file) {
if (!file) return;
if (state.pendingImageUrl) {
try {
URL.revokeObjectURL(state.pendingImageUrl);
} catch {}
}
state.pendingImageFile = file;
state.pendingImageUrl = URL.createObjectURL(file);
renderAttachment();
}
function renderAttachment() {
const box = $("composerAttach");
const thumb = $("inlineThumb");
const wrap = $("msgInputWrap");
if (!box) return;
if (!state.pendingImageFile || !state.pendingImageUrl) {
box.hidden = true;
box.innerHTML = "";
if (thumb) {
thumb.hidden = true;
thumb.removeAttribute("src");
}
if (wrap) wrap.classList.remove("hasThumb");
return;
}
box.hidden = false;
box.innerHTML = `
`;
if (thumb) {
thumb.src = state.pendingImageUrl;
thumb.hidden = false;
}
if (wrap) wrap.classList.add("hasThumb");
const removeBtn = $("attachRemoveBtn");
if (removeBtn) removeBtn.addEventListener("click", () => clearPendingImage());
}
function levelName(level) {
if (level === 3) return "管理员";
if (level === 2) return "认证会员";
if (level === 1) return "注册会员";
return "匿名游客";
}
function gateError(msg) {
const el = $("gateError");
if (!el) return;
el.textContent = msg || "";
el.classList.toggle("show", Boolean(msg));
}
function showGate() {
gateError("");
const loginWrapper = $("loginWrapper");
const doors = $("doorsContainer");
const chatRoom = $("chatRoom");
if (loginWrapper) {
loginWrapper.style.display = "";
loginWrapper.classList.remove("fade-out");
}
if (doors) {
doors.style.display = "";
doors.classList.remove("open");
}
if (chatRoom) chatRoom.classList.remove("active");
refreshGateLobbyHint().catch(() => {});
prepareGuestName();
}
function hideGate() {
gateError("");
closeGateModal();
const loginWrapper = $("loginWrapper");
const doors = $("doorsContainer");
const chatRoom = $("chatRoom");
if (loginWrapper) loginWrapper.classList.add("fade-out");
if (doors) doors.classList.add("open");
if (chatRoom) chatRoom.classList.add("active");
setTimeout(() => {
const w = $("loginWrapper");
const d = $("doorsContainer");
if (w) w.style.display = "none";
if (d) d.style.display = "none";
}, 1300);
}
function setGateView(view) {
const login = $("gateLoginForm");
const reg = $("gateRegisterForm");
const guest = $("gateGuestForm");
if (!login || !reg || !guest) return;
login.hidden = view !== "login";
reg.hidden = view !== "register";
guest.hidden = view !== "guest";
$("gateTabLogin").classList.toggle("btnPrimary", view === "login");
$("gateTabRegister").classList.toggle("btnPrimary", view === "register");
$("gateTabGuest").classList.toggle("btnPrimary", view === "guest");
const titles = { login: "登录", register: "注册", guest: "游客登录" };
const titleEl = $("gateModalTitle");
if (titleEl) titleEl.textContent = titles[view] || "";
$("gateModal").hidden = false;
gateError("");
if (view === "guest") prepareGuestName();
}
function closeGateModal() {
$("gateModal").hidden = true;
gateError("");
}
async function api(path, opts = {}) {
const resp = await fetch(path, { credentials: "include", ...opts });
const data = await resp.json().catch(() => ({}));
if (!resp.ok || data?.error) throw new Error(data.error || `请求失败: ${resp.status}`);
return data;
}
async function refreshMe() {
const data = await api("/api/me");
state.me = data.user;
if (state.me) {
state.myAnonName = null;
clearGuestSession();
hideGate();
}
renderMe();
renderAuth();
renderOnlineList();
$("adminCard").style.display = state.me && state.me.level >= 3 ? "" : "none";
if (state.me && state.me.level >= 3) {
loadUsersAdmin().catch(() => {});
}
setFoldDefaultsOnce();
updateComposerState();
}
async function getLobby() {
const data = await api("/api/lobby");
return data.room;
}
async function refreshGateLobbyHint() {
const hint = $("gateGuestHint");
if (!hint) return;
const pass = $("gateGuestPass");
const passGroup = pass ? pass.closest(".input-group") : null;
try {
const lobby = await getLobby();
if (lobby.is_private) {
hint.textContent = "该聊天室已设置密码:游客进入需要输入密码。";
if (passGroup) passGroup.hidden = false;
if (pass) pass.disabled = false;
} else {
hint.textContent = "该聊天室未设置密码:游客可直接进入(开放聊天室仅可浏览,发言需注册登录)。";
if (passGroup) passGroup.hidden = true;
if (pass) {
pass.disabled = true;
pass.value = "";
}
}
} catch {
hint.textContent = "";
if (passGroup) passGroup.hidden = false;
if (pass) pass.disabled = false;
}
}
function stopStream() {
if (state.es) {
state.es.close();
state.es = null;
}
}
function resetChatView() {
state.currentRoom = null;
state.accessToken = null;
state.lastMessageId = 0;
$("roomTitle").textContent = "未进入聊天室";
$("roomSubtitle").textContent = "";
$("messages").innerHTML = "";
$("msgInput").disabled = true;
$("sendBtn").disabled = true;
$("emojiBtn").disabled = true;
$("imageBtn").disabled = true;
$("imageBtn").hidden = true;
$("leaveRoomBtn").disabled = true;
clearPendingImage();
closeEmojiPanel();
renderOnlineList();
}
function renderMe() {
const meBox = $("meBox");
if (!meBox) return;
if (state.me) {
meBox.textContent = `${state.me.username} · ${levelName(state.me.level)}`;
return;
}
if (state.myAnonName) {
meBox.textContent = `${state.myAnonName} · 匿名游客`;
return;
}
meBox.textContent = "未登录";
}
function renderAuth() {
const box = $("authBox");
if (!box) return;
if (state.me) {
box.innerHTML = `
已登录:${state.me.username}
`;
$("logoutBtn").addEventListener("click", async () => {
await api("/api/auth/logout", { method: "POST" }).catch(() => {});
state.me = null;
state.myAnonName = null;
clearGuestSession();
stopStream();
resetChatView();
await refreshMe();
showGate();
});
return;
}
if (state.myAnonName) {
box.innerHTML = `
游客:${state.myAnonName}
`;
$("guestExitBtn").addEventListener("click", () => {
state.myAnonName = null;
state.accessToken = null;
clearGuestSession();
stopStream();
resetChatView();
renderMe();
renderAuth();
renderOnlineList();
showGate();
});
return;
}
box.innerHTML = `
`;
$("openGateBtn").addEventListener("click", () => showGate());
}
function appendMessage(msg) {
const isMe =
(state.me && msg.user_id && state.me.id === msg.user_id) ||
(!state.me && state.myAnonName && msg.user_id === null && msg.sender_name === state.myAnonName);
const row = document.createElement("div");
row.className = `msgRow ${isMe ? "right" : "left"}`;
const bubble = document.createElement("div");
bubble.className = `bubble ${isMe ? "me" : ""}`;
const meta = document.createElement("div");
meta.className = "meta";
const time = new Date(msg.created_at).toLocaleString();
const lvl = typeof msg.sender_level === "number" ? msg.sender_level : 0;
const nameClass = lvl === 3 ? "userNameAdmin" : lvl === 2 ? "userNameVerified" : lvl === 1 ? "userNameMember" : "";
meta.innerHTML = `
${escapeHtml(msg.sender_name)}
·
${escapeHtml(time)}
`;
bubble.appendChild(meta);
if (msg.type === "image" && msg.r2_key) {
const img = document.createElement("img");
const access = state.currentRoom?.is_private ? `?accessToken=${encodeURIComponent(state.accessToken || "")}` : "";
const src = `/api/images/${encodeURIComponent(msg.r2_key)}${access}`;
img.src = src;
img.loading = "lazy";
img.addEventListener("click", () => openImageViewer(src));
bubble.appendChild(img);
} else {
const text = document.createElement("div");
text.textContent = msg.content || "";
bubble.appendChild(text);
}
row.appendChild(bubble);
$("messages").appendChild(row);
$("messages").scrollTop = $("messages").scrollHeight;
}
async function loadMessages() {
if (!state.currentRoom) return;
const q = new URLSearchParams();
q.set("after", String(state.lastMessageId || 0));
if (state.currentRoom.is_private) q.set("accessToken", state.accessToken || "");
const data = await api(`/api/rooms/${state.currentRoom.id}/messages?${q.toString()}`);
for (const msg of data.messages) {
state.lastMessageId = Math.max(state.lastMessageId, msg.id);
appendMessage(msg);
}
}
function startStream() {
if (!state.currentRoom) return;
const q = new URLSearchParams();
q.set("after", String(state.lastMessageId || 0));
if (state.currentRoom.is_private) q.set("accessToken", state.accessToken || "");
const es = new EventSource(`/api/rooms/${state.currentRoom.id}/stream?${q.toString()}`);
state.es = es;
es.addEventListener("message", (evt) => {
const msg = JSON.parse(evt.data);
if (typeof msg?.id === "number" && msg.id <= state.lastMessageId) return;
state.lastMessageId = Math.max(state.lastMessageId, msg.id);
appendMessage(msg);
});
es.onerror = () => {
// 浏览器会自动重连;这里避免刷屏
};
}
function updateComposerState() {
const hasRoom = Boolean(state.currentRoom);
const canText =
hasRoom &&
(state.currentRoom.is_private
? Boolean(state.me) || (state.currentRoom.allow_anonymous && Boolean(state.accessToken))
: Boolean(state.me));
$("msgInput").disabled = !canText;
$("sendBtn").disabled = !canText;
$("emojiBtn").disabled = !canText;
const canImage = hasRoom && state.me && state.me.level >= 2;
$("imageBtn").hidden = !canImage;
$("imageBtn").disabled = !canText;
if (hasRoom && !state.currentRoom.is_private && !state.me) {
$("msgInput").placeholder = "开放聊天室需要登录才能发言";
} else {
$("msgInput").placeholder = "输入消息…";
}
}
async function enterLobbyAsLoggedIn() {
const lobby = await getLobby();
const joined = await api(`/api/rooms/${lobby.id}/join`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
state.accessToken = joined.accessToken || null;
state.myAnonName = null;
clearGuestSession();
await enterLobbyView(lobby, lobby.is_private ? "私密" : "开放");
}
async function enterLobbyAsGuest({ nickname, password }) {
const lobby = await getLobby();
if (lobby.is_private) {
const joined = await api(`/api/rooms/${lobby.id}/join`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ password: password || "", nickname }),
});
state.myAnonName = joined.me?.nickname || nickname;
state.accessToken = joined.accessToken || null;
if (state.accessToken) {
saveGuestSession({ roomId: lobby.id, nickname: state.myAnonName, accessToken: state.accessToken });
}
await enterLobbyView(lobby, "私密 · 游客");
return;
}
state.myAnonName = nickname;
state.accessToken = null;
clearGuestSession();
await enterLobbyView(lobby, "开放 · 游客");
}
async function enterLobbyView(lobby, subtitle) {
stopStream();
$("messages").innerHTML = "";
state.lastMessageId = 0;
state.currentRoom = lobby;
$("roomTitle").textContent = lobby.name;
$("roomSubtitle").textContent = subtitle;
$("leaveRoomBtn").disabled = false;
await loadMessages();
startStream();
updateComposerState();
hideGate();
renderMe();
renderAuth();
renderOnlineList();
clearPendingImage();
closeEmojiPanel();
}
async function sendMessage() {
const room = state.currentRoom;
if (!room) return;
const text = ($("msgInput").value || "").trim();
const file = state.pendingImageFile;
if (!text && !file) return;
if (text) {
const payload = {
type: "text",
content: text,
accessToken: room.is_private ? state.accessToken : undefined,
};
const data = await api(`/api/rooms/${room.id}/messages`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
$("msgInput").value = "";
if (data?.message?.id && data.message.id > state.lastMessageId) {
state.lastMessageId = data.message.id;
appendMessage(data.message);
}
}
if (file) {
await uploadImageFile(file);
clearPendingImage();
}
closeEmojiPanel();
}
async function uploadImageFile(file) {
const room = state.currentRoom;
if (!room) return;
if (!state.me) throw new Error("登录后可发送图片");
if (state.me.level < 2) throw new Error("认证会员才可以发送图片");
const form = new FormData();
form.set("file", file);
if (room.is_private) form.set("accessToken", state.accessToken || "");
const data = await api(`/api/rooms/${room.id}/upload`, { method: "POST", body: form });
if (data?.message?.id && data.message.id > state.lastMessageId) {
state.lastMessageId = data.message.id;
appendMessage(data.message);
}
}
async function loadUsersAdmin() {
const box = $("adminUsersBox");
if (!box) return;
box.innerHTML = `加载中…
`;
try {
const data = await api("/api/admin/users");
const users = data.users || [];
box.innerHTML = "";
const header = document.createElement("div");
header.className = "adminHeader";
header.innerHTML = `用户
操作
`;
box.appendChild(header);
for (const u of users) {
const editIcon = `
`;
const trashIcon = `
`;
const row = document.createElement("div");
row.className = "adminRow";
const nameClass =
u.level === 3 ? "userNameAdmin" : u.level === 2 ? "userNameVerified" : u.level === 1 ? "userNameMember" : "";
row.innerHTML = `
${escapeHtml(u.username)}
`;
row.querySelector('[data-action="edit"]').addEventListener("click", () => {
openUserEditor(u);
});
row.querySelector('[data-action="delete"]').addEventListener("click", async () => {
const ok = await confirmDialog({
title: "删除用户",
message: `确定删除用户 ${u.username}?\n历史消息会保留,但账号会被删除。`,
okText: "删除",
cancelText: "取消",
});
if (!ok) return;
try {
await api(`/api/admin/users/${u.id}`, { method: "DELETE" });
await loadUsersAdmin();
} catch (err) {
toast(err.message, "bad");
}
});
box.appendChild(row);
}
} catch (err) {
box.innerHTML = `失败:${escapeHtml(err.message)}
`;
}
}
function openUserEditor(user) {
const tools = $("adminToolsBox");
if (!tools) return;
tools.hidden = false;
tools.innerHTML = `
`;
const levelEl = $("editUserLevel");
const emailEl = $("editUserEmail");
const qqEl = $("editUserQq");
const phoneEl = $("editUserPhone");
const statusEl = $("editUserStatus");
levelEl.value = String(user.level);
emailEl.value = user.email || "";
qqEl.value = user.qq || "";
phoneEl.value = user.phone || "";
$("editUserCloseBtn").addEventListener("click", (e) => {
e.preventDefault();
tools.hidden = true;
tools.innerHTML = "";
});
$("editUserSaveBtn").addEventListener("click", async (e) => {
e.preventDefault();
statusEl.textContent = "保存中…";
try {
await api(`/api/admin/users/${user.id}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
level: Number(levelEl.value),
email: emailEl.value,
qq: qqEl.value,
phone: phoneEl.value,
}),
});
statusEl.textContent = "已保存";
toast("已保存", "ok");
await refreshMe();
await loadUsersAdmin();
} catch (err) {
statusEl.textContent = err.message;
toast(err.message, "bad");
}
});
}
async function showLobbyPasswordAdmin() {
if (!state.me || state.me.level < 3) return toast("需要管理员权限", "bad");
const tools = $("adminToolsBox");
if (!tools) return;
tools.hidden = !tools.hidden;
if (tools.hidden) return;
tools.innerHTML = `
`;
const lobbyPwdMode = $("lobbyPwdMode");
const lobbyPwdInput = $("lobbyPwdInput");
const lobbyPwdSaveBtn = $("lobbyPwdSaveBtn");
const lobbyPwdCloseBtn = $("lobbyPwdCloseBtn");
const lobbyPwdStatus = $("lobbyPwdStatus");
const refreshLobbyPwdMode = async () => {
if (!lobbyPwdMode || !lobbyPwdStatus) return;
lobbyPwdStatus.textContent = "";
lobbyPwdStatus.classList.remove("ok", "bad");
try {
const lobby = await getLobby();
lobbyPwdMode.textContent = lobby.is_private ? "当前:大厅已设置密码(游客进入需要密码)" : "当前:大厅未设置密码";
} catch {
lobbyPwdMode.textContent = "当前:未知(加载失败)";
}
};
const saveLobbyPassword = async () => {
if (!lobbyPwdStatus || !lobbyPwdInput) return;
lobbyPwdStatus.textContent = "保存中…";
lobbyPwdStatus.classList.remove("ok", "bad");
try {
const password = (lobbyPwdInput.value || "").trim();
await api("/api/admin/lobby/password", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ password }),
});
lobbyPwdStatus.textContent = "已保存";
lobbyPwdStatus.classList.add("ok");
lobbyPwdInput.value = "";
toast("已保存", "ok");
await refreshLobbyPwdMode();
await refreshGateLobbyHint();
if (state.currentRoom && state.currentRoom.id === "lobby") {
const lobby = await getLobby();
state.currentRoom = lobby;
$("roomSubtitle").textContent = state.me ? (lobby.is_private ? "私密" : "开放") : $("roomSubtitle").textContent;
updateComposerState();
}
} catch (err) {
lobbyPwdStatus.textContent = err.message || "保存失败";
lobbyPwdStatus.classList.add("bad");
toast(err.message || "保存失败", "bad");
}
};
if (lobbyPwdCloseBtn) {
lobbyPwdCloseBtn.addEventListener("click", (e) => {
e.preventDefault();
tools.hidden = true;
tools.innerHTML = "";
});
}
if (lobbyPwdSaveBtn) {
lobbyPwdSaveBtn.addEventListener("click", (e) => {
e.preventDefault();
saveLobbyPassword().catch((e2) => toast(e2.message, "bad"));
});
}
await refreshLobbyPwdMode();
if (lobbyPwdInput) lobbyPwdInput.focus();
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function openImageViewer(src) {
const viewer = $("imgViewer");
const img = $("imgViewerImg");
if (!viewer || !img) return;
img.src = src;
viewer.hidden = false;
}
function closeImageViewer() {
const viewer = $("imgViewer");
const img = $("imgViewerImg");
if (!viewer || !img) return;
viewer.hidden = true;
img.removeAttribute("src");
}
function bindUi() {
const imgViewerBackdrop = $("imgViewerBackdrop");
const imgViewerClose = $("imgViewerClose");
if (imgViewerBackdrop) imgViewerBackdrop.addEventListener("click", () => closeImageViewer());
if (imgViewerClose) imgViewerClose.addEventListener("click", () => closeImageViewer());
window.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeImageViewer();
if (e.key === "Escape") closeEmojiPanel();
});
// 找回密码模态框
const forgotModal = $("forgotModal");
const forgotStep1 = $("forgotStep1");
const forgotStep2 = $("forgotStep2");
const forgotStep1Msg = $("forgotStep1Msg");
const forgotStep2Msg = $("forgotStep2Msg");
function openForgotModal() {
$("forgotUsername").value = "";
$("forgotToken").value = "";
$("forgotNewPass").value = "";
forgotStep1Msg.textContent = "";
forgotStep2Msg.textContent = "";
forgotStep1.style.display = "";
forgotStep2.style.display = "none";
forgotModal.hidden = false;
$("forgotUsername").focus();
}
function closeForgotModal() {
forgotModal.hidden = true;
}
$("forgotPasswordBtn").addEventListener("click", openForgotModal);
$("forgotCancelBtn").addEventListener("click", closeForgotModal);
$("forgotSendBtn").addEventListener("click", async () => {
const username = $("forgotUsername").value.trim();
if (!username) { forgotStep1Msg.textContent = "请输入用户名"; return; }
$("forgotSendBtn").disabled = true;
forgotStep1Msg.textContent = "发送中...";
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) {
if (data.devResetLink) {
forgotStep1Msg.innerHTML = `开发模式:点击这里重置密码`;
} else {
forgotStep1Msg.textContent = data.message || "✅ 验证码已发送到邮箱,请查收";
setTimeout(() => {
forgotStep1.style.display = "none";
forgotStep2.style.display = "";
$("forgotToken").focus();
}, 1500);
}
} else {
forgotStep1Msg.textContent = data.error || "请求失败,请稍后重试";
}
} catch {
forgotStep1Msg.textContent = "网络错误,请稍后重试";
} finally {
$("forgotSendBtn").disabled = false;
}
});
$("forgotBackBtn").addEventListener("click", () => {
forgotStep2.style.display = "none";
forgotStep1.style.display = "";
forgotStep1Msg.textContent = "";
forgotStep2Msg.textContent = "";
});
$("forgotResetBtn").addEventListener("click", async () => {
const token = $("forgotToken").value.trim();
const newPassword = $("forgotNewPass").value;
if (!token || !/^\d{6}$/.test(token)) { forgotStep2Msg.textContent = "请输入 6 位数字验证码"; return; }
if (newPassword.length < 6) { forgotStep2Msg.textContent = "密码至少需要 6 位"; return; }
$("forgotResetBtn").disabled = true;
forgotStep2Msg.textContent = "重置中...";
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) {
forgotStep2Msg.textContent = "✅ 密码已重置成功!";
setTimeout(closeForgotModal, 1800);
} else {
forgotStep2Msg.textContent = data.error || "重置失败,验证码可能已过期";
}
} catch {
forgotStep2Msg.textContent = "网络错误,请稍后重试";
} finally {
$("forgotResetBtn").disabled = false;
}
});
renderThemeBtn();
const themeBtn = $("themeBtn");
if (themeBtn) {
themeBtn.addEventListener("click", () => {
setTheme(getCurrentTheme() === "dark" ? "light" : "dark");
});
}
$("sendBtn").addEventListener("click", () => sendMessage().catch((e) => toast(e.message, "bad")));
$("msgInput").addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage().catch((err) => toast(err.message, "bad"));
}
});
const emojiBtn = $("emojiBtn");
const emojiPanel = $("emojiPanel");
if (emojiBtn && emojiPanel) {
buildEmojiPanelOnce();
emojiBtn.addEventListener("click", () => {
if (emojiBtn.disabled) return;
emojiPanel.hidden = !emojiPanel.hidden;
});
document.addEventListener("click", (e) => {
if (emojiPanel.hidden) return;
const target = e.target;
if (target === emojiBtn || emojiBtn.contains(target)) return;
if (emojiPanel.contains(target)) return;
closeEmojiPanel();
});
}
const imageBtn = $("imageBtn");
const fileInput = $("fileInput");
if (imageBtn && fileInput) {
imageBtn.addEventListener("click", () => {
if (imageBtn.disabled || imageBtn.hidden) return;
fileInput.click();
});
fileInput.addEventListener("change", () => {
const file = fileInput.files?.[0] || null;
fileInput.value = "";
if (!file) return;
setPendingImage(file);
});
}
$("leaveRoomBtn").addEventListener("click", () => {
(async () => {
if (state.me) {
const ok = await confirmDialog({
title: "离开聊天室",
message: "当前版本只有一个聊天室:离开将同时退出登录。确定退出吗?",
okText: "退出登录",
cancelText: "取消",
});
if (!ok) return;
await api("/api/auth/logout", { method: "POST" }).catch(() => {});
state.me = null;
clearGuestSession();
stopStream();
resetChatView();
await refreshMe();
showGate();
return;
}
if (state.myAnonName) {
const ok = await confirmDialog({
title: "离开聊天室",
message: "离开将同时退出游客身份。确定退出吗?",
okText: "退出游客",
cancelText: "取消",
});
if (!ok) return;
state.myAnonName = null;
state.accessToken = null;
clearGuestSession();
stopStream();
resetChatView();
renderMe();
renderAuth();
renderOnlineList();
showGate();
return;
}
stopStream();
resetChatView();
showGate();
})().catch((e) => toast(e.message, "bad"));
});
$("loadUsersBtn").addEventListener("click", () => loadUsersAdmin().catch((e) => toast(e.message, "bad")));
$("lobbyPwdBtn").addEventListener("click", () => showLobbyPasswordAdmin().catch((e) => toast(e.message, "bad")));
$("gateTabLogin").addEventListener("click", () => setGateView("login"));
$("gateTabRegister").addEventListener("click", () => setGateView("register"));
$("gateTabGuest").addEventListener("click", () => setGateView("guest"));
$("gateModalClose").addEventListener("click", closeGateModal);
$("gateModal").addEventListener("click", (e) => {
if (e.target === $("gateModal")) closeGateModal();
});
$("gateLoginForm").addEventListener("submit", async (e) => {
e.preventDefault();
gateError("");
try {
await api("/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
username: $("gateLoginUser").value,
password: $("gateLoginPass").value,
}),
});
await refreshMe();
await enterLobbyAsLoggedIn();
} catch (err) {
gateError(err.message);
}
});
$("gateRegisterForm").addEventListener("submit", async (e) => {
e.preventDefault();
gateError("");
try {
await api("/api/auth/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
username: $("gateRegUser").value,
password: $("gateRegPass").value,
email: $("gateRegEmail").value,
qq: $("gateRegQq").value,
phone: $("gateRegPhone").value,
}),
});
await refreshMe();
await enterLobbyAsLoggedIn();
} catch (err) {
gateError(err.message);
}
});
$("gateGuestForm").addEventListener("submit", async (e) => {
e.preventDefault();
gateError("");
const nickname = state.pendingGuestName || randomGuestName();
const password = ($("gateGuestPass").value || "").trim();
try {
await enterLobbyAsGuest({ nickname, password });
prepareGuestName();
} catch (err) {
gateError(err.message);
}
});
setupMobileMenu();
}
async function tryRestoreGuestSession() {
const { exists, session } = loadGuestSession();
if (!exists) return null;
if (!session) return false;
try {
const lobby = await getLobby();
if (lobby.id !== session.roomId) {
clearGuestSession();
return false;
}
state.me = null;
state.myAnonName = session.nickname;
state.accessToken = lobby.is_private ? session.accessToken : null;
if (lobby.is_private && !state.accessToken) throw new Error("需要重新输入聊天室密码");
await enterLobbyView(lobby, lobby.is_private ? "私密 · 游客" : "开放 · 游客");
return true;
} catch {
clearGuestSession();
state.myAnonName = null;
state.accessToken = null;
return false;
}
}
async function main() {
bindUi();
resetChatView();
await refreshMe();
if (state.me) {
await enterLobbyAsLoggedIn();
} else {
const restored = await tryRestoreGuestSession();
if (restored === true) return;
if (restored === false) toast("需要重新输入聊天室密码(已过期)", "bad", 2600);
closeGateModal();
showGate();
}
}
window.addEventListener("error", (e) => {
// 让你在“没反应”时也能看到原因
if (String(e?.filename || "").includes("bootstrap-autofill-overlay")) return;
console.error(e.error || e.message || e);
});
main().catch((err) => {
console.error(err);
alert(err instanceof Error ? err.message : String(err));
});
})();