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 = `
${user.username[0].toUpperCase()}
${user.username}
`;
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 = `
`;
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();
});
});
})();