const API = (typeof SITE_CONFIG !== 'undefined' ? SITE_CONFIG.backendUrl : 'http://127.0.0.1:8008'); const Auth = { getToken() { return localStorage.getItem('sa_token'); }, getUser() { const u = localStorage.getItem('sa_user'); return u ? JSON.parse(u) : null; }, save(token, user) { localStorage.setItem('sa_token', token); localStorage.setItem('sa_user', JSON.stringify(user)); }, clear() { localStorage.removeItem('sa_token'); localStorage.removeItem('sa_user'); }, isLoggedIn() { return !!this.getToken(); } }; // 渲染导航栏右侧用户区域 function renderNavUser() { const el = document.getElementById('navUserArea'); if (!el) return; if (Auth.isLoggedIn()) { const user = Auth.getUser(); el.innerHTML = ` `; document.getElementById('navUserBtn').onclick = (e) => { e.stopPropagation(); document.getElementById('navUserDropdown').classList.toggle('show'); }; document.addEventListener('click', () => { const d = document.getElementById('navUserDropdown'); if (d) d.classList.remove('show'); }); } else { el.innerHTML = ` ${authT('nav_login')} ${authT('nav_submit')}`; } } // 登录/注册弹窗 function showLoginModal(tab = 'login') { let modal = document.getElementById('authModal'); if (modal) modal.remove(); // 每次重新生成,避免缓存 modal = document.createElement('div'); modal.id = 'authModal'; modal.innerHTML = `
忘记密码?
${authT('or')}
${authT('github_login')}
`; document.body.appendChild(modal); modal.style.display = 'flex'; switchAuthTab(tab); } function closeAuthModal() { const m = document.getElementById('authModal'); if (m) m.style.display = 'none'; } function switchAuthTab(tab) { document.getElementById('panelLogin').style.display = tab === 'login' ? '' : 'none'; document.getElementById('panelRegister').style.display = tab === 'register' ? '' : 'none'; document.getElementById('panelForgot').style.display = tab === 'forgot' ? '' : 'none'; document.getElementById('tabLogin').classList.toggle('active', tab === 'login'); document.getElementById('tabRegister').classList.toggle('active', tab === 'register'); document.getElementById('authMsg').textContent = ''; } // 忘记密码 - 发送验证码 async function sendForgotCode(){ const email = document.getElementById('forgotEmail').value.trim(); const msg = document.getElementById('authMsg'); if(!email){ msg.textContent = '请输入邮箱'; return; } const btn = document.getElementById('sendForgotBtn'); btn.disabled = true; try { const res = await fetch(`${API}/api/auth/send-code`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email, type: 'reset'}) }); const data = await res.json(); if(res.ok){ msg.style.color = '#22c55e'; msg.textContent = '验证码已发送'; let sec = 60; const timer = setInterval(()=>{ btn.textContent = `${sec}s`; sec--; if(sec < 0){ clearInterval(timer); btn.textContent = '发送'; btn.disabled = false; } }, 1000); } else { msg.style.color = '#ef4444'; msg.textContent = data.detail || '发送失败'; btn.disabled = false; } } catch(e){ msg.style.color='#ef4444'; msg.textContent='网络错误'; btn.disabled=false; } } // 忘记密码 - 重置密码 async function doResetPwd(){ const email = document.getElementById('forgotEmail').value.trim(); const code = document.getElementById('forgotCode').value.trim(); const newPwd = document.getElementById('forgotNewPwd').value; const newPwd2 = document.getElementById('forgotNewPwd2').value; const msg = document.getElementById('authMsg'); msg.style.color = '#ef4444'; if(!email){ msg.textContent = '请输入邮箱'; return; } if(!code){ msg.textContent = '请输入验证码'; return; } if(newPwd.length < 6){ msg.textContent = '密码至少6位'; return; } if(newPwd !== newPwd2){ msg.textContent = '两次密码不一致'; return; } try { const res = await fetch(`${API}/api/auth/reset-password`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email, code, new_password: newPwd}) }); const data = await res.json(); if(res.ok){ msg.style.color = '#22c55e'; msg.textContent = '密码重置成功,请重新登录'; setTimeout(()=>switchAuthTab('login'), 1500); } else { msg.textContent = data.detail || '重置失败'; } } catch(e){ msg.textContent = '网络错误'; } } function checkPwdStrength(pwd){ const fill = document.getElementById('pwdStrengthFill'); if(!fill) return; let score = 0; if(pwd.length >= 6) score++; if(pwd.length >= 10) score++; if(/[A-Z]/.test(pwd)) score++; if(/[0-9]/.test(pwd)) score++; if(/[^A-Za-z0-9]/.test(pwd)) score++; const colors = ['#ef4444','#f97316','#eab308','#22c55e','#16a34a']; const widths = ['20%','40%','60%','80%','100%']; fill.style.width = pwd ? widths[Math.min(score,4)] : '0'; fill.style.background = pwd ? colors[Math.min(score,4)] : 'transparent'; } async function doLogin() { const email = document.getElementById('loginEmail').value.trim(); const pwd = document.getElementById('loginPwd').value; const msg = document.getElementById('authMsg'); if (!email || !pwd) { msg.textContent = authT('err_fill'); return; } try { const res = await fetch(`${API}/api/auth/login`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email, password: pwd}) }); const data = await res.json(); if (!res.ok) { msg.textContent = data.detail || authT('err_login'); return; } Auth.save(data.token, data.user); closeAuthModal(); renderNavUser(); } catch(e) { msg.textContent = '网络错误,请重试'; } } // 注册 - 发送验证码 async function sendRegCode(){ const email = document.getElementById('regEmail').value.trim(); const msg = document.getElementById('authMsg'); if(!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){ msg.textContent='请先填写正确的邮箱'; return; } const btn = document.getElementById('sendRegCodeBtn'); btn.disabled = true; try { const res = await fetch(`${API}/api/auth/send-code`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email, type: 'verify'}) }); const data = await res.json(); if(res.ok){ msg.style.color = '#22c55e'; msg.textContent = '验证码已发送到邮箱'; let sec = 60; const timer = setInterval(()=>{ btn.textContent = `${sec}s`; sec--; if(sec < 0){ clearInterval(timer); btn.textContent = '发送'; btn.disabled = false; } }, 1000); } else { msg.style.color = '#ef4444'; msg.textContent = data.detail || '发送失败'; btn.disabled = false; } } catch(e){ msg.style.color='#ef4444'; msg.textContent='网络错误'; btn.disabled=false; } } async function doRegister() { const username = document.getElementById('regUsername').value.trim(); const email = document.getElementById('regEmail').value.trim(); const code = document.getElementById('regCode').value.trim(); const pwd = document.getElementById('regPwd').value; const pwd2 = document.getElementById('regPwd2').value; const msg = document.getElementById('authMsg'); msg.style.color = '#ef4444'; if (!username || !email || !pwd) { msg.textContent = '请填写所有字段'; return; } if (username.length < 2) { msg.textContent = '用户名至少2个字符'; return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { msg.textContent = '邮箱格式不正确'; return; } if (!code) { msg.textContent = '请输入邮箱验证码'; return; } if (pwd.length < 6) { msg.textContent = '密码至少6位'; return; } if (pwd !== pwd2) { msg.textContent = '两次密码不一致'; return; } try { const res = await fetch(`${API}/api/auth/register`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, email, password: pwd, code}) }); const data = await res.json(); if (!res.ok) { msg.textContent = data.detail || authT('err_register'); return; } Auth.save(data.token, data.user); closeAuthModal(); renderNavUser(); } catch(e) { msg.textContent = '网络错误,请重试'; } } function signOut() { Auth.clear(); renderNavUser(); } // GitHub OAuth 回调处理:URL 带 gh_token 参数时自动登录 (function handleGithubCallback() { const params = new URLSearchParams(window.location.search); const token = params.get('gh_token'); const username = params.get('gh_user'); const email = params.get('gh_email'); const err = params.get('gh_error'); if (token && username) { Auth.save(token, { username, email: email || '' }); // 清除 URL 参数 const url = window.location.pathname; window.history.replaceState({}, '', url); } if (err) { console.warn('GitHub 登录失败:', err); window.history.replaceState({}, '', window.location.pathname); } })(); // 页面加载时渲染 document.addEventListener('DOMContentLoaded', () => { renderNavUser(); // 导航渲染完后重新应用语言 if (typeof applyI18n === 'function') { const urlLang = new URLSearchParams(window.location.search).get('lang'); const lang = urlLang || localStorage.getItem('sa_lang') || 'zh'; applyI18n(lang); if (typeof applyDetailI18n === 'function') applyDetailI18n(lang); if (typeof applyDaleiI18n === 'function') applyDaleiI18n(lang); if (typeof applyXiaoleiI18n === 'function') applyXiaoleiI18n(lang); if (typeof applyCatI18n === 'function') applyCatI18n(lang); if (typeof applyDongchaI18n === 'function') applyDongchaI18n(lang); if (typeof applySubmitI18n === 'function') applySubmitI18n(lang); if (typeof applyMyfavI18n === 'function') applyMyfavI18n(lang); if (typeof applyMyskillsI18n === 'function') applyMyskillsI18n(lang); } }); // 注入 GitHub 按钮样式 (function injectGithubStyle() { const style = document.createElement('style'); style.textContent = `.auth-github-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:9px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;transition:border-color 0.15s;}.auth-github-btn:hover{border-color:var(--accent);color:var(--accent);}`; document.head.appendChild(style); })(); // ========== 语言下拉菜单 ========== (function initLangMenu() { const LANGS = [ { code: 'ar', label: 'العربية', flag: 'EG' }, { code: 'de', label: 'Deutsch', flag: 'DE' }, { code: 'en', label: 'English', flag: 'US' }, { code: 'es', label: 'Español', flag: 'ES' }, { code: 'fr', label: 'Français', flag: 'FR' }, { code: 'ja', label: '日本語', flag: 'JP' }, { code: 'ko', label: '한국어', flag: 'KR' }, { code: 'pt', label: 'Português', flag: 'BR' }, { code: 'zh', label: '中文', flag: 'CN' }, ]; const style = document.createElement('style'); style.textContent = ` .lang-wrap{position:relative;} .lang-btn{font-size:13px;color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:5px 12px;cursor:pointer;background:transparent;display:flex;align-items:center;gap:5px;white-space:nowrap;} .lang-btn:hover{border-color:var(--accent);color:var(--text);} .lang-dropdown{position:absolute;top:calc(100% + 6px);right:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;min-width:160px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:300;overflow:hidden;display:none;} .lang-dropdown.show{display:block;} .lang-item{display:flex;align-items:center;gap:10px;padding:8px 14px;font-size:13px;cursor:pointer;transition:background 0.12s;color:var(--text);} .lang-item:hover{background:var(--border2);} .lang-item.active{color:var(--accent);font-weight:600;} .lang-flag{font-size:11px;color:var(--text3);font-family:monospace;width:22px;flex-shrink:0;} `; document.head.appendChild(style); document.addEventListener('DOMContentLoaded', () => { const el = document.querySelector('.snav-lang'); if (!el) return; // 优先读取 URL 参数,其次 localStorage,默认 zh const urlLang = new URLSearchParams(window.location.search).get('lang'); const curLang = urlLang || localStorage.getItem('sa_lang') || 'zh'; if (urlLang) localStorage.setItem('sa_lang', urlLang); const cur = LANGS.find(l => l.code === curLang) || LANGS[8]; // 替换原有静态按钮 const wrap = document.createElement('div'); wrap.className = 'lang-wrap'; wrap.innerHTML = `
🌐 ${cur.flag} ▼
${LANGS.map(l => `
${l.flag} ${l.label}
`).join('')}
`; el.replaceWith(wrap); document.getElementById('langBtn').onclick = (e) => { e.stopPropagation(); document.getElementById('langDropdown').classList.toggle('show'); }; document.addEventListener('click', () => { const d = document.getElementById('langDropdown'); if (d) d.classList.remove('show'); }); document.getElementById('langDropdown').addEventListener('click', (e) => { const item = e.target.closest('.lang-item'); if (!item) return; const code = item.dataset.code; const flag = item.dataset.flag; localStorage.setItem('sa_lang', code); // 更新 URL 参数(不刷新页面) const url = new URL(window.location.href); if (code === 'zh') { url.searchParams.delete('lang'); } else { url.searchParams.set('lang', code); } window.history.pushState({}, '', url.toString()); document.getElementById('langBtn').innerHTML = `🌐 ${flag} ▼`; document.querySelectorAll('.lang-item').forEach(i => i.classList.toggle('active', i.dataset.code === code)); document.getElementById('langDropdown').classList.remove('show'); if (typeof loadOccNames === 'function') loadOccNames(code); if (typeof applyI18n === 'function') applyI18n(code); if (typeof applyDetailI18n === 'function') applyDetailI18n(code); if (typeof applyCatI18n === 'function') applyCatI18n(code); if (typeof applyDaleiI18n === 'function') applyDaleiI18n(code); if (typeof applyXiaoleiI18n === 'function') applyXiaoleiI18n(code); if (typeof applyDongchaI18n === 'function') applyDongchaI18n(code); if (typeof applySubmitI18n === 'function') applySubmitI18n(code); if (typeof applyMyfavI18n === 'function') applyMyfavI18n(code); if (typeof applyMyskillsI18n === 'function') applyMyskillsI18n(code); if (typeof applyAccountI18n === 'function') applyAccountI18n(code); // 详情页描述翻译 if (typeof translateSkillDesc === 'function') { const slug = new URLSearchParams(location.search).get('slug') || ''; if (slug) { if (code === 'en') { const el = document.getElementById('skillComment'); if (el && typeof skillData !== 'undefined' && skillData) el.textContent = skillData.description || ''; } else { translateSkillDesc(slug, code); } } } renderNavUser(); }); }); })();