// 聊天室头像选择和个人设置模块,负责头像 API、密码修改、邮箱验证码和微信绑定操作。 let profileControlEventsBound = false; let avatarPickerLoaded = false; /** * 读取 CSRF Token。 * * @returns {string} */ function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content || ""; } /** * 按 ID 获取 DOM 节点。 * * @param {string} id * @returns {HTMLElement|null} */ function element(id) { return document.getElementById(id); } /** * 显示内联操作结果提示。 * * @param {string} elementId * @param {string} message * @param {boolean} success * @returns {void} */ export function showInlineMsg(elementId, message, success) { const target = element(elementId); if (!target) { return; } target.style.background = success ? "#f0fdf4" : "#fff5f5"; target.style.border = success ? "1px solid #86efac" : "1px solid #fecaca"; target.style.color = success ? "#16a34a" : "#dc2626"; target.textContent = message; target.style.display = "block"; target.style.opacity = "1"; target.style.transition = "opacity .4s"; window.clearTimeout(target._hideTimer); target._hideTimer = window.setTimeout(() => { target.style.opacity = "0"; window.setTimeout(() => { target.style.display = "none"; }, 420); }, 3000); } /** * 打开头像选择弹窗并懒加载头像列表。 * * @returns {void} */ export function openAvatarPicker() { const modal = element("avatar-picker-modal"); if (!modal) { return; } modal.style.display = "flex"; if (!avatarPickerLoaded) { void loadHeadfaces(); avatarPickerLoaded = true; } } /** * 关闭头像选择弹窗。 * * @returns {void} */ export function closeAvatarPicker() { const modal = element("avatar-picker-modal"); if (modal) { modal.style.display = "none"; } } /** * 打开个人设置弹窗。 * * @returns {void} */ export function openSettingsModal() { const modal = element("settings-modal"); if (modal) { modal.style.display = "flex"; } } /** * 关闭个人设置弹窗。 * * @returns {void} */ export function closeSettingsModal() { const modal = element("settings-modal"); if (modal) { modal.style.display = "none"; } } /** * 加载系统头像列表。 * * @returns {Promise} */ export async function loadHeadfaces() { const grid = element("avatar-grid"); if (!grid) { return; } grid.innerHTML = '
加载中...
'; try { const response = await fetch("/headface/list", { headers: { Accept: "application/json", }, }); const data = await response.json(); grid.innerHTML = ""; for (const file of data.headfaces || []) { const image = document.createElement("img"); image.src = `/images/headface/${file}`; image.className = "avatar-option"; image.title = file; image.dataset.file = file; image.dataset.avatarFile = file; image.onerror = () => { image.style.display = "none"; }; grid.appendChild(image); } } catch (error) { grid.innerHTML = '
加载失败
'; } } /** * 选中头像并刷新预览。 * * @param {string} file * @param {HTMLElement} imageElement * @returns {void} */ export function selectAvatar(file, imageElement) { document.querySelectorAll(".avatar-option.selected").forEach((item) => item.classList.remove("selected")); imageElement.classList.add("selected"); element("avatar-preview").src = `/images/headface/${file}`; element("avatar-selected-name").textContent = file; const saveButton = element("avatar-save-btn"); saveButton.disabled = false; saveButton.dataset.file = file; } /** * 同步当前用户头像到在线名单。 * * @param {string} headface * @returns {void} */ function syncOnlineUserHeadface(headface) { const username = window.chatContext?.username; // 在线名单仍由存量聊天室脚本维护为全局词法变量,这里保留同名访问兼容。 if (username && typeof onlineUsers !== "undefined" && onlineUsers[username]) { onlineUsers[username].headface = headface; } if (typeof renderUserList === "function") { renderUserList(); } } /** * 处理本地头像上传。 * * @param {HTMLInputElement} input * @returns {Promise} */ export async function handleAvatarUpload(input) { if (!input.files || !input.files[0]) { return; } const file = input.files[0]; if (file.size > 2 * 1024 * 1024) { window.chatDialog?.alert?.("图片大小不可超过 2MB", "上传失败", "#cc4444"); input.value = ""; return; } const button = element("avatar-upload-btn"); button.disabled = true; button.textContent = "上传中..."; const formData = new FormData(); formData.append("file", file); try { const response = await fetch("/headface/upload", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, body: formData, }); const data = await response.json(); if (response.ok && data.status === "success") { window.chatDialog?.alert?.("自定义头像上传成功!", "提示", "#16a34a"); const relativeUrl = `/${data.headface}`; element("avatar-preview").src = relativeUrl; element("avatar-selected-name").textContent = data.headface; syncOnlineUserHeadface(data.headface); document.querySelectorAll(".avatar-option.selected").forEach((item) => item.classList.remove("selected")); element("avatar-save-btn").disabled = true; closeAvatarPicker(); } else { window.chatDialog?.alert?.(data.message || "上传失败", "操作失败", "#cc4444"); } } catch (error) { window.chatDialog?.alert?.("网络错误,上传失败", "网络异常", "#cc4444"); } button.disabled = false; button.textContent = "选择本地图片上传"; input.value = ""; } /** * 保存当前选中的系统头像。 * * @returns {Promise} */ export async function saveAvatar() { const button = element("avatar-save-btn"); const file = button?.dataset.file || ""; if (!file) { return; } button.disabled = true; button.textContent = "保存中..."; try { const response = await fetch("/headface/change", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, body: JSON.stringify({ headface: file }), }); const data = await response.json(); if (data.status === "success") { window.chatDialog?.alert?.("头像修改成功!", "提示", "#16a34a"); syncOnlineUserHeadface(data.headface); closeAvatarPicker(); } else { window.chatDialog?.alert?.(data.message || "修改失败", "操作失败", "#cc4444"); } } catch (error) { window.chatDialog?.alert?.("网络错误", "网络异常", "#cc4444"); } button.disabled = false; button.textContent = "确定更换"; } /** * 保存登录密码。 * * @returns {Promise} */ export async function savePassword() { const oldPassword = element("set-old-pwd").value; const newPassword = element("set-new-pwd").value; const confirmPassword = element("set-new-pwd2").value; if (!oldPassword || !newPassword) { showInlineMsg("pwd-inline-msg", "⚠️ 请填写旧密码和新密码", false); return; } if (newPassword.length < 6) { showInlineMsg("pwd-inline-msg", "⚠️ 新密码最少6位!", false); return; } if (newPassword !== confirmPassword) { showInlineMsg("pwd-inline-msg", "⚠️ 两次输入的新密码不一致!", false); return; } try { const response = await fetch("/user/password", { method: "PUT", headers: { "X-CSRF-TOKEN": csrf(), "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, new_password_confirmation: confirmPassword, }), }); const data = await response.json(); if (response.ok && data.status === "success") { showInlineMsg("pwd-inline-msg", "🔒 密码修改成功!", true); element("set-old-pwd").value = ""; element("set-new-pwd").value = ""; element("set-new-pwd2").value = ""; return; } showInlineMsg("pwd-inline-msg", `❌ ${data.message || "请输入正确的旧密码"}`, false); } catch (error) { showInlineMsg("pwd-inline-msg", "🌐 网络异常,请稍后重试", false); } } /** * 保存个人资料和密保设置。 * * @returns {Promise} */ export async function saveSettings() { const profileData = { sex: element("set-sex").value, email: element("set-email").value, email_code: element("set-email-code")?.value || "", question: element("set-question").value, answer: element("set-answer").value, headface: window.chatContext?.profileHeadface || "1.gif", sign: window.chatContext?.profileSign || "", }; try { const response = await fetch("/user/profile", { method: "PUT", headers: { "X-CSRF-TOKEN": csrf(), "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify(profileData), }); const data = await response.json(); if (response.ok && data.status === "success") { showInlineMsg("settings-inline-msg", "✅ 资料保存成功!", true); return; } showInlineMsg("settings-inline-msg", `❌ ${data.message || "输入有误"}`, false); } catch (error) { showInlineMsg("settings-inline-msg", "🌐 网络异常,请稍后重试", false); } } /** * 发送邮箱验证码并启动按钮倒计时。 * * @returns {Promise} */ export async function sendEmailCode() { const email = element("set-email").value.trim(); if (!email) { window.chatDialog?.alert?.("请先填写邮箱地址后再获取验证码!", "提示", "#d97706"); return; } const button = element("btn-send-code"); button.disabled = true; button.innerText = "正在发送..."; button.style.opacity = "0.6"; button.style.cursor = "not-allowed"; try { const response = await fetch("/user/send-email-code", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ email }), }); const data = await response.json(); if (response.ok && data.status === "success") { window.chatDialog?.alert?.(data.message || "验证码发送成功,请前往邮箱查收!(有效期5分钟)", "发送成功", "#16a34a"); startEmailCodeCountdown(button); return; } window.chatDialog?.alert?.(`发送失败:${data.message || "系统繁忙"}`, "发送失败", "#dc2626"); resetEmailCodeButton(button); } catch (error) { window.chatDialog?.alert?.("网络异常,验证码发送失败,请稍后重试。", "错误", "#6b7280"); resetEmailCodeButton(button); } } /** * 启动邮箱验证码按钮 60 秒倒计时。 * * @param {HTMLButtonElement} button * @returns {void} */ function startEmailCodeCountdown(button) { let remaining = 60; button.innerText = `${remaining}s 后重试`; const timer = window.setInterval(() => { remaining -= 1; if (remaining <= 0) { window.clearInterval(timer); resetEmailCodeButton(button); return; } button.innerText = `${remaining}s 后重试`; }, 1000); } /** * 恢复邮箱验证码按钮状态。 * * @param {HTMLButtonElement} button * @returns {void} */ function resetEmailCodeButton(button) { button.innerText = "获取验证码"; button.disabled = false; button.style.opacity = "1"; button.style.cursor = "pointer"; } /** * 生成微信绑定验证码。 * * @returns {Promise} */ export async function generateWechatBindCode() { const button = element("btn-generate-bind-code"); const input = element("wechat-bind-code"); const tip = element("bind-code-tip"); button.disabled = true; button.innerText = "生成中..."; try { const response = await fetch("/user/generate-wechat-code", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, }); const data = await response.json(); if (response.ok && data.status === "success") { input.value = data.code; tip.style.display = "block"; element("btn-copy-bind-code").style.display = "inline-block"; showInlineMsg("settings-inline-msg", "✅ 绑定代码生成成功,请在5分钟内发送给机器人", true); } else { showInlineMsg("settings-inline-msg", `❌ 生成失败:${data.message || "未知错误"}`, false); } } catch (error) { showInlineMsg("settings-inline-msg", "🌐 网络异常,请稍后重试", false); } button.disabled = false; button.innerText = "重新生成"; } /** * 复制微信绑定验证码。 * * @returns {void} */ export function copyWechatBindCode() { const input = element("wechat-bind-code"); if (!input.value || input.value === "点击生成" || input.value === "生成中...") { return; } input.select(); input.setSelectionRange(0, 99999); try { if (navigator.clipboard && window.isSecureContext) { void navigator.clipboard.writeText(input.value); } else { document.execCommand("copy"); } const button = element("btn-copy-bind-code"); const originalText = button.innerText; button.innerText = "已复制"; window.setTimeout(() => { button.innerText = originalText; }, 2000); } catch (error) { showInlineMsg("settings-inline-msg", "❌ 复制失败,请手动复制", false); } } /** * 解除微信绑定。 * * @returns {Promise} */ export async function unbindWechat() { const confirmed = typeof window.chatDialog?.confirm === "function" ? await window.chatDialog.confirm("确定要解除微信绑定吗?解除后将无法接收任何机器人推送通知。", "解除微信绑定") : window.confirm("确定要解除微信绑定吗?解除后将无法接收任何机器人推送通知。"); if (!confirmed) { return; } try { const response = await fetch("/user/unbind-wechat", { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, }); const data = await response.json(); if (response.ok && data.status === "success") { window.chatDialog?.alert?.("✅ 解绑成功!请刷新页面获取最新状态。", "提示", "#16a34a"); window.location.reload(); return; } window.chatDialog?.alert?.(`❌ 解绑失败:${data.message || "未知错误"}`, "操作失败", "#cc4444"); } catch (error) { window.chatDialog?.alert?.("网络异常,解绑失败", "网络异常", "#cc4444"); } } /** * 处理动态头像项选择。 * * @param {Element} target * @returns {boolean} */ function handleAvatarOptionClick(target) { const avatarOption = target.closest("[data-avatar-file]"); if (!(avatarOption instanceof HTMLElement)) { return false; } const file = avatarOption.getAttribute("data-avatar-file") || ""; if (!file) { return false; } selectAvatar(file, avatarOption); return true; } /** * 挂载头像与设置全局兼容函数。 * * @returns {void} */ function exposeProfileGlobals() { window.openAvatarPicker = openAvatarPicker; window.closeAvatarPicker = closeAvatarPicker; window.openSettingsModal = openSettingsModal; window.closeSettingsModal = closeSettingsModal; window.loadHeadfaces = loadHeadfaces; window.selectAvatar = selectAvatar; window.handleAvatarUpload = handleAvatarUpload; window.saveAvatar = saveAvatar; window.showInlineMsg = showInlineMsg; window.savePassword = savePassword; window.saveSettings = saveSettings; window.sendEmailCode = sendEmailCode; window.generateWechatBindCode = generateWechatBindCode; window.copyWechatBindCode = copyWechatBindCode; window.unbindWechat = unbindWechat; } /** * 绑定头像选择器和个人设置弹窗事件。 * * @returns {void} */ export function bindProfileControls() { if (typeof window === "undefined") { return; } exposeProfileGlobals(); if (profileControlEventsBound || typeof document === "undefined") { return; } profileControlEventsBound = true; document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } if (event.target.closest("[data-avatar-picker-close]")) { event.preventDefault(); closeAvatarPicker(); return; } if (handleAvatarOptionClick(event.target)) { event.preventDefault(); return; } if (event.target.closest("[data-avatar-save]")) { event.preventDefault(); void saveAvatar(); return; } if (event.target.closest("[data-avatar-upload-trigger]")) { event.preventDefault(); element("avatar-upload-input")?.click(); return; } if (event.target.closest("[data-settings-modal-close]")) { event.preventDefault(); closeSettingsModal(); return; } if (event.target.closest("[data-settings-save-password]")) { event.preventDefault(); void savePassword(); return; } if (event.target.closest("[data-settings-send-email-code]")) { event.preventDefault(); void sendEmailCode(); return; } if (event.target.closest("[data-settings-copy-wechat-code]")) { event.preventDefault(); copyWechatBindCode(); return; } if (event.target.closest("[data-settings-generate-wechat-code]")) { event.preventDefault(); void generateWechatBindCode(); return; } if (event.target.closest("[data-settings-unbind-wechat]")) { event.preventDefault(); void unbindWechat(); return; } if (event.target.closest("[data-settings-save-profile]")) { event.preventDefault(); void saveSettings(); return; } if (event.target.closest("[data-settings-modal-panel]")) { event.stopPropagation(); return; } if (event.target.closest("[data-settings-modal-overlay]")) { closeSettingsModal(); } }); document.addEventListener("change", (event) => { if (!(event.target instanceof HTMLInputElement) || !event.target.matches("[data-avatar-upload-input]")) { return; } void handleAvatarUpload(event.target); }); }