// 登录/注册页面逻辑 (function () { // DOM元素 const loginOverlay = document.getElementById('loginOverlay'); const loginContainer = document.getElementById('loginContainer'); const closeBtn = document.getElementById('closeBtn'); const tabBtns = document.querySelectorAll('.tab-btn'); const loginForm = document.getElementById('loginForm'); const registerForm = document.getElementById('registerForm'); const typeBtns = document.querySelectorAll('.type-btn'); const accountLogin = document.getElementById('accountLogin'); const phoneLogin = document.getElementById('phoneLogin'); // 登录表单元素 const loginAccount = document.getElementById('loginAccount'); const loginPassword = document.getElementById('loginPassword'); const loginPhone = document.getElementById('loginPhone'); const loginCode = document.getElementById('loginCode'); const getLoginCodeBtn = document.getElementById('getLoginCode'); const loginSubmit = document.getElementById('loginSubmit'); // 注册表单元素 const registerAvatar = document.getElementById('registerAvatar'); const avatarPreview = document.getElementById('avatarPreview'); const avatarImage = document.getElementById('avatarImage'); const registerUsername = document.getElementById('registerUsername'); const registerPhone = document.getElementById('registerPhone'); const registerCode = document.getElementById('registerCode'); const getRegisterCodeBtn = document.getElementById('getRegisterCode'); const registerPassword = document.getElementById('registerPassword'); const registerPasswordConfirm = document.getElementById('registerPasswordConfirm'); const registerSubmit = document.getElementById('registerSubmit'); // 验证码倒计时 let loginCodeCountdown = 0; let registerCodeCountdown = 0; // 默认头像URL(注册时使用) let defaultAvatarUrl = null; // 初始化 function init() { // 标签切换 tabBtns.forEach(btn => { btn.addEventListener('click', () => { const tab = btn.getAttribute('data-tab'); switchTab(tab); }); }); // 登录方式切换 typeBtns.forEach(btn => { btn.addEventListener('click', () => { const type = btn.getAttribute('data-type'); switchLoginType(type); }); }); // 关闭按钮 closeBtn.addEventListener('click', closeLogin); loginOverlay.addEventListener('click', closeLogin); // 阻止容器内点击关闭 loginContainer.addEventListener('click', (e) => { e.stopPropagation(); }); // 头像上传 avatarPreview.addEventListener('click', () => { registerAvatar.click(); }); registerAvatar.addEventListener('change', handleAvatarUpload); // 获取验证码按钮 getLoginCodeBtn.addEventListener('click', () => { getVerificationCode('login'); }); getRegisterCodeBtn.addEventListener('click', () => { getVerificationCode('register'); }); // 提交按钮 loginSubmit.addEventListener('click', handleLogin); registerSubmit.addEventListener('click', handleRegister); // 密码验证提示 registerPassword.addEventListener('input', validatePassword); registerPasswordConfirm.addEventListener('input', validatePasswordConfirm); // 手机号实时验证 loginPhone.addEventListener('input', validatePhone); loginPhone.addEventListener('blur', validatePhone); registerPhone.addEventListener('input', validatePhone); registerPhone.addEventListener('blur', validatePhone); // 监听来自父窗口的消息 window.addEventListener('message', handleMessage); } // 切换标签 function switchTab(tab) { tabBtns.forEach(btn => { if (btn.getAttribute('data-tab') === tab) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); if (tab === 'login') { loginForm.style.display = 'block'; registerForm.style.display = 'none'; } else { loginForm.style.display = 'none'; registerForm.style.display = 'block'; // 切换到注册标签时,加载默认头像 loadDefaultAvatar(); } } // 加载默认头像 async function loadDefaultAvatar() { try { const response = await fetch('http://localhost:3000/api/avatars/default'); if (!response.ok) { throw new Error('获取默认头像失败'); } const data = await response.json(); if (data.success && data.avatars && data.avatars.length > 0) { // 随机选择一个默认头像 const randomIndex = Math.floor(Math.random() * data.avatars.length); const avatarFilename = data.avatars[randomIndex]; defaultAvatarUrl = `http://localhost:3000/avatar/${encodeURIComponent(avatarFilename)}`; // 显示默认头像 if (avatarImage && !registerAvatar.files[0]) { avatarImage.src = defaultAvatarUrl; avatarImage.style.display = 'block'; avatarPreview.querySelector('.avatar-placeholder').style.display = 'none'; } } } catch (error) { console.error('[Login] 加载默认头像失败:', error); } } // 切换登录方式 function switchLoginType(type) { typeBtns.forEach(btn => { if (btn.getAttribute('data-type') === type) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); if (type === 'account') { accountLogin.style.display = 'block'; phoneLogin.style.display = 'none'; } else { accountLogin.style.display = 'none'; phoneLogin.style.display = 'block'; } } // 处理头像上传 function handleAvatarUpload(e) { const file = e.target.files[0]; if (!file) return; // 验证文件类型 if (!file.type.startsWith('image/')) { showError('请上传图片文件'); return; } // 验证文件大小(最大5MB) if (file.size > 5 * 1024 * 1024) { showError('图片大小不能超过5MB'); return; } // 预览图片 const reader = new FileReader(); reader.onload = (e) => { avatarImage.src = e.target.result; avatarImage.style.display = 'block'; avatarPreview.querySelector('.avatar-placeholder').style.display = 'none'; }; reader.readAsDataURL(file); } // 获取验证码 function getVerificationCode(type) { const phoneInput = type === 'login' ? loginPhone : registerPhone; const phone = phoneInput.value.trim(); // 验证手机号 if (!phone) { showError('请输入手机号'); return; } // 验证手机号格式:1开头,第二位3-9,总共11位 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phone)) { if (phone.length !== 11) { showError('手机号必须是11位数字'); } else if (!phone.startsWith('1')) { showError('手机号必须以1开头'); } else if (!/^1[3-9]/.test(phone)) { showError('手机号第二位必须是3-9'); } else { showError('请输入正确的手机号格式'); } return; } // 先检查手机号是否存在(登录时检查是否存在,注册时检查是否已存在) httpRequest('http://localhost:3000/api/check-phone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: phone, type: type }) }, (error, response, result) => { if (error) { showError('网络错误,请稍后重试'); return; } if (!response) { showError('检查失败,请稍后重试'); return; } // 如果检查失败,显示错误信息 if (result.error) { if (response.status === 404 && type === 'login') { showError('手机未注册'); } else if (response.status === 409 && type === 'register') { showError('手机号已被注册'); } else { showError(result.message || '检查失败'); } return; } // 如果检查失败(手机号不符合要求) if (!result.success) { showError(result.message || '检查失败'); return; } // 检查通过,开始倒计时并发送验证码 const codeBtn = type === 'login' ? getLoginCodeBtn : getRegisterCodeBtn; let countdown = 50; if (type === 'login') { loginCodeCountdown = countdown; } else { registerCodeCountdown = countdown; } codeBtn.disabled = true; codeBtn.classList.add('countdown'); codeBtn.textContent = `${countdown}秒后重试`; const timer = setInterval(() => { countdown--; codeBtn.textContent = `${countdown}秒后重试`; if (countdown <= 0) { clearInterval(timer); codeBtn.disabled = false; codeBtn.classList.remove('countdown'); codeBtn.textContent = '获取验证码'; if (type === 'login') { loginCodeCountdown = 0; } else { registerCodeCountdown = 0; } } else { if (type === 'login') { loginCodeCountdown = countdown; } else { registerCodeCountdown = countdown; } } }, 1000); // 验证码功能(固定验证码 9527,仅开发者可见) // TODO: 后续实现真实的验证码发送 console.log('[验证码] 9527 (仅开发者可见)'); }); } // 验证密码格式 function validatePassword() { const password = registerPassword.value; const hint = registerPassword.parentElement.querySelector('.hint-text'); if (!password) { hint.style.color = '#6b7280'; return false; } // 密码要求:8-20位,包含大小写字母和数字 const hasLength = password.length >= 8 && password.length <= 20; const hasUpper = /[A-Z]/.test(password); const hasLower = /[a-z]/.test(password); const hasNumber = /\d/.test(password); if (hasLength && hasUpper && hasLower && hasNumber) { hint.style.color = '#10b981'; hint.textContent = '✓ 密码格式正确'; return true; } else { hint.style.color = '#ef4444'; const issues = []; if (!hasLength) issues.push('长度8-20位'); if (!hasUpper) issues.push('大写字母'); if (!hasLower) issues.push('小写字母'); if (!hasNumber) issues.push('数字'); hint.textContent = `缺少:${issues.join('、')}`; return false; } } // 验证确认密码 function validatePasswordConfirm() { const password = registerPassword.value; const passwordConfirm = registerPasswordConfirm.value; const input = registerPasswordConfirm; if (!passwordConfirm) { input.style.borderColor = '#e5e7eb'; return false; } if (password === passwordConfirm) { input.style.borderColor = '#10b981'; return true; } else { input.style.borderColor = '#ef4444'; return false; } } // 处理登录 function handleLogin() { const isAccountLogin = accountLogin.style.display !== 'none'; if (isAccountLogin) { // 账号密码登录 const account = loginAccount.value.trim(); const password = loginPassword.value; if (!account) { showError('请输入账号'); return; } if (!password) { showError('请输入密码'); return; } // 发送登录请求到服务器 const formData = new FormData(); formData.append('loginType', 'account'); formData.append('account', account); formData.append('password', password); httpRequest('http://localhost:3000/api/login', { method: 'POST', body: formData }, (error, response, result) => { if (error) { // 网络错误通过 alert-view 显示 showError('网络错误,请稍后重试'); return; } if (!response) { showError('登录失败,请稍后重试'); return; } // 如果解析JSON失败,根据状态码显示错误 if (result.error) { if (response.status === 401) { showError('账号或密码错误'); } else if (response.status === 400) { showError('请求参数错误'); } else if (response.status >= 500) { showError('服务器错误,请稍后重试'); } else { showError('登录失败,请稍后重试'); } return; } if (response.ok && result.success) { // 保存登录信息到 localStorage(有效期2小时) if (result.user) { const loginData = { user: result.user, expireTime: Date.now() + 2 * 60 * 60 * 1000 // 2小时后过期 }; try { localStorage.setItem('loginData', JSON.stringify(loginData)); console.log('[Login] 登录信息已保存到 localStorage,有效期2小时'); } catch (error) { console.warn('[Login] 保存登录信息失败:', error); } } // 显示成功提示(在登录 iframe 中) if (window.HintView) { window.HintView.success('登录成功', 2000); } else { showSuccess('登录成功'); } // 通知父窗口登录成功,更新导航栏 if (window.parent !== window && result.user) { window.parent.postMessage({ type: 'login-success', user: result.user }, '*'); } // 延迟关闭 setTimeout(() => { closeLogin(); }, 2000); } else { // 处理各种错误状态,使用 alert-view 显示错误 // 优先使用服务器返回的错误消息(区分账号不存在和密码错误) const errorMessage = result.message || (response.status === 401 ? '账号或密码错误' : response.status === 400 ? '请求参数错误' : response.status >= 500 ? '服务器错误,请稍后重试' : '登录失败'); // 使用 alert-view 显示错误提示 showError(errorMessage); } }); } else { // 手机验证码登录 const phone = loginPhone.value.trim(); const code = loginCode.value.trim(); if (!phone) { showError('请输入手机号'); return; } // 验证手机号格式:1开头,第二位3-9,总共11位 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phone)) { if (phone.length !== 11) { showError('手机号必须是11位数字'); } else if (!phone.startsWith('1')) { showError('手机号必须以1开头'); } else if (!/^1[3-9]/.test(phone)) { showError('手机号第二位必须是3-9'); } else { showError('请输入正确的手机号格式'); } return; } if (!code) { showError('请输入验证码'); return; } // 验证码固定为 9527 if (code !== '9527') { showError('验证码错误'); return; } // 发送登录请求到服务器 const formData = new FormData(); formData.append('loginType', 'phone'); formData.append('phone', phone); formData.append('code', code); httpRequest('http://localhost:3000/api/login', { method: 'POST', body: formData }, (error, response, result) => { if (error) { // 网络错误通过 alert-view 显示 showError('网络错误,请稍后重试'); return; } if (!response) { showError('登录失败,请稍后重试'); return; } // 如果解析JSON失败,根据状态码显示错误 if (result.error) { if (response.status === 401) { showError('手机未注册'); } else if (response.status === 400) { showError('请求参数错误'); } else if (response.status >= 500) { showError('服务器错误,请稍后重试'); } else { showError('登录失败,请稍后重试'); } return; } if (response.ok && result.success) { // 保存登录信息到 localStorage(有效期2小时) if (result.user) { const loginData = { user: result.user, expireTime: Date.now() + 2 * 60 * 60 * 1000 // 2小时后过期 }; try { localStorage.setItem('loginData', JSON.stringify(loginData)); console.log('[Login] 登录信息已保存到 localStorage,有效期2小时'); } catch (error) { console.warn('[Login] 保存登录信息失败:', error); } } // 显示成功提示(在登录 iframe 中) if (window.HintView) { window.HintView.success('登录成功', 2000); } else { showSuccess('登录成功'); } // 通知父窗口登录成功,更新导航栏 if (window.parent !== window && result.user) { window.parent.postMessage({ type: 'login-success', user: result.user }, '*'); } // 延迟关闭 setTimeout(() => { closeLogin(); }, 2000); } else { // 处理各种错误状态 if (response.status === 401) { // 手机验证码登录时,401 表示手机未注册 showError(result.message || '手机未注册'); } else if (response.status === 400) { // 400 可能是验证码错误或其他参数错误 showError(result.message || '请求参数错误'); } else if (response.status >= 500) { showError(result.message || '服务器错误,请稍后重试'); } else { showError(result.message || '登录失败'); } } }); } } // 处理注册 function handleRegister() { const username = registerUsername.value.trim(); const phone = registerPhone.value.trim(); const code = registerCode.value.trim(); const password = registerPassword.value; const passwordConfirm = registerPasswordConfirm.value; const avatarFile = registerAvatar.files[0]; // 注意:头像不再是必填,如果没有上传则使用默认头像 // 验证用户名(现代网站标准) if (!username) { showError('请输入用户名'); return; } // 长度:4-20个字符 if (username.length < 4 || username.length > 20) { showError('用户名长度为4-20个字符'); return; } // 只能包含字母、数字、下划线、连字符,且必须以字母开头 if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(username)) { showError('用户名只能包含字母、数字、下划线和连字符,且必须以字母开头'); return; } // 不能全部是数字 if (/^\d+$/.test(username)) { showError('用户名不能全部是数字'); return; } // 验证手机号 if (!phone) { showError('请输入手机号'); return; } // 验证手机号格式:1开头,第二位3-9,总共11位 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phone)) { if (phone.length !== 11) { showError('手机号必须是11位数字'); } else if (!phone.startsWith('1')) { showError('手机号必须以1开头'); } else if (!/^1[3-9]/.test(phone)) { showError('手机号第二位必须是3-9'); } else { showError('请输入正确的手机号格式'); } return; } // 验证验证码(固定验证码 9527) if (!code) { showError('请输入验证码'); return; } if (code !== '9527') { showError('验证码错误'); return; } // 验证密码 if (!validatePassword()) { showError('密码格式不正确'); return; } if (!validatePasswordConfirm()) { showError('两次输入的密码不一致'); return; } // 发送注册请求到服务器 const formData = new FormData(); formData.append('username', username); formData.append('phone', phone); formData.append('code', code); formData.append('password', password); formData.append('passwordConfirm', passwordConfirm); // 添加头像文件(必填,已在前面验证) formData.append('avatar', avatarFile); httpRequest('http://localhost:3000/api/register', { method: 'POST', body: formData }, (error, response, result) => { if (error) { // 网络错误通过 alert-view 显示 showError('网络错误,请稍后重试'); return; } if (!response) { showError('注册失败,请稍后重试'); return; } // 如果解析JSON失败 if (result.error) { showError('注册失败,请稍后重试'); return; } if (result.success) { // 显示成功提示 if (window.HintView) { window.HintView.success('注册成功', 2000); } else { showSuccess('注册成功'); } // 延迟关闭并切换到登录 setTimeout(() => { switchTab('login'); clearRegisterForm(); }, 2000); } else { showError(result.message || '注册失败'); } }); } // 清空注册表单 function clearRegisterForm() { registerUsername.value = ''; registerPhone.value = ''; registerCode.value = ''; registerPassword.value = ''; registerPasswordConfirm.value = ''; registerAvatar.value = ''; avatarImage.src = ''; avatarImage.style.display = 'none'; avatarPreview.querySelector('.avatar-placeholder').style.display = 'block'; registerPasswordConfirm.style.borderColor = '#e5e7eb'; const hint = registerPassword.parentElement.querySelector('.hint-text'); hint.style.color = '#6b7280'; hint.textContent = '密码要求:8-20位,包含大小写字母和数字'; // 重新加载默认头像 loadDefaultAvatar(); } // 验证手机号格式 function validatePhone(event) { const phoneInput = event.target; const phone = phoneInput.value.trim(); // 清除之前的错误样式 phoneInput.style.borderColor = ''; // 如果为空,不验证 if (!phone) { phoneInput.removeAttribute('title'); return; } // 验证手机号格式:1开头,第二位3-9,总共11位 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phone)) { // 设置错误样式 phoneInput.style.borderColor = '#ef4444'; // 根据错误类型显示提示 if (phone.length !== 11) { phoneInput.setAttribute('title', '手机号必须是11位数字'); } else if (!phone.startsWith('1')) { phoneInput.setAttribute('title', '手机号必须以1开头'); } else if (!/^1[3-9]/.test(phone)) { phoneInput.setAttribute('title', '手机号第二位必须是3-9'); } else { phoneInput.setAttribute('title', '请输入正确的手机号格式'); } } else { // 验证通过,清除提示 phoneInput.removeAttribute('title'); phoneInput.style.borderColor = '#10b981'; } } // HTTP 请求封装(使用回调函数) function httpRequest(url, options, callback) { fetch(url, options) .then(response => { response.text() .then(text => { let result = {}; if (text) { try { result = JSON.parse(text); } catch (e) { // JSON 解析失败,返回原始文本 result = { error: text }; } } callback(null, response, result); }) .catch(error => { callback(error, response, null); }); }) .catch(error => { callback(error, null, null); }); } // 显示错误信息(直接在 login.html 的 UI 中显示) function showError(message) { if (!message) { message = '操作失败,请稍后重试'; } // 根据当前显示的表单,选择对应的错误提示元素 const isLoginForm = loginForm.style.display !== 'none'; const errorElement = isLoginForm ? document.getElementById('loginError') : document.getElementById('registerError'); if (errorElement) { errorElement.textContent = message; errorElement.classList.add('show'); // 3秒后自动隐藏 setTimeout(() => { errorElement.classList.remove('show'); }, 3000); } else { // 降级处理 alert(message); } } // 显示成功信息(通过 postMessage 发送消息给 alert-view.js) function showSuccess(message) { if (!message) { message = '操作成功'; } try { // 通过 postMessage 发送消息给父窗口的 alert-view const targetWindow = window.parent && window.parent !== window ? window.parent : window; targetWindow.postMessage({ type: 'alert-view', message: message, messageType: 'success', duration: 2000 }, '*'); } catch (error) { // 静默处理错误 } } // 关闭登录窗口 function closeLogin() { if (loginOverlay) { loginOverlay.style.display = 'none'; } if (loginContainer) { loginContainer.style.display = 'none'; } if (window.parent !== window) { window.parent.postMessage({ type: 'close-login-view' }, '*'); } } // 处理来自父窗口的消息 function handleMessage(event) { if (event.origin !== window.location.origin) { return; } const { data } = event; if (data && data.type === 'open-login-view') { // console.log('[5-Login] 收到open-login-view消息'); const mode = data.mode || 'login'; switchTab(mode); if (loginOverlay) { loginOverlay.style.display = 'block'; } if (loginContainer) { loginContainer.style.display = 'block'; } // console.log('[6-Login] overlay和container已显示'); } } // 页面加载完成后初始化 window.addEventListener('DOMContentLoaded', function() { init(); }); })();