Files
chatroom/resources/js/chat-room/profile-controls.js
T

716 lines
20 KiB
JavaScript
Raw Normal View History

2026-04-25 14:51:07 +08:00
// 聊天室头像选择和个人设置模块,负责头像 API、密码修改、邮箱验证码和微信绑定操作。
2026-04-25 08:04:22 +08:00
let profileControlEventsBound = false;
2026-04-25 14:51:07 +08:00
let avatarPickerLoaded = false;
2026-04-25 08:04:22 +08:00
/**
2026-04-25 14:51:07 +08:00
* 读取 CSRF Token。
2026-04-25 08:04:22 +08:00
*
2026-04-25 14:51:07 +08:00
* @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<void>}
*/
export async function loadHeadfaces() {
const grid = element("avatar-grid");
if (!grid) {
return;
}
grid.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
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 = '<div style="text-align:center;padding:20px;color:red;">加载失败</div>';
}
}
/**
* 选中头像并刷新预览。
*
* @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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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
2026-04-25 08:04:22 +08:00
* @returns {void}
*/
2026-04-25 14:51:07 +08:00
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<void>}
*/
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<void>}
*/
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");
2026-04-25 08:04:22 +08:00
}
}
2026-04-25 10:40:01 +08:00
/**
* 处理动态头像项选择。
*
2026-04-25 14:51:07 +08:00
* @param {Element} target
2026-04-25 10:40:01 +08:00
* @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;
}
2026-04-25 14:51:07 +08:00
selectAvatar(file, avatarOption);
2026-04-25 10:40:01 +08:00
return true;
}
2026-04-25 14:51:07 +08:00
/**
* 挂载头像与设置全局兼容函数。
*
* @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;
}
2026-04-25 08:04:22 +08:00
/**
* 绑定头像选择器和个人设置弹窗事件。
*
* @returns {void}
*/
export function bindProfileControls() {
2026-04-25 14:51:07 +08:00
if (typeof window === "undefined") {
return;
}
exposeProfileGlobals();
2026-04-25 08:04:22 +08:00
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();
2026-04-25 14:51:07 +08:00
closeAvatarPicker();
2026-04-25 08:04:22 +08:00
return;
}
2026-04-25 10:40:01 +08:00
if (handleAvatarOptionClick(event.target)) {
event.preventDefault();
return;
}
2026-04-25 08:04:22 +08:00
if (event.target.closest("[data-avatar-save]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void saveAvatar();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-avatar-upload-trigger]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
element("avatar-upload-input")?.click();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-modal-close]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
closeSettingsModal();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-save-password]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void savePassword();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-send-email-code]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void sendEmailCode();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-copy-wechat-code]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
copyWechatBindCode();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-generate-wechat-code]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void generateWechatBindCode();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-unbind-wechat]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void unbindWechat();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-save-profile]")) {
event.preventDefault();
2026-04-25 14:51:07 +08:00
void saveSettings();
2026-04-25 08:04:22 +08:00
return;
}
if (event.target.closest("[data-settings-modal-panel]")) {
event.stopPropagation();
return;
}
if (event.target.closest("[data-settings-modal-overlay]")) {
2026-04-25 14:51:07 +08:00
closeSettingsModal();
2026-04-25 08:04:22 +08:00
}
});
document.addEventListener("change", (event) => {
if (!(event.target instanceof HTMLInputElement) || !event.target.matches("[data-avatar-upload-input]")) {
return;
}
2026-04-25 14:51:07 +08:00
void handleAvatarUpload(event.target);
2026-04-25 08:04:22 +08:00
});
}