fix: 同步两个页面的弹窗结构

- index.html 添加 profileModal HTML
- analysis.html 移除错误位置的 twofaBindModal
- analysis.html 添加 twofaBindModal 到正确位置
- analysis.html profileModal 添加完整 2FA 设置部分
- 两个页面的 header/menu/modal 现在一致

不打包,等用户确认后再打包
This commit is contained in:
OP
2026-07-02 20:53:29 +08:00
parent a0c57e0f21
commit 908bab811e
3 changed files with 204 additions and 116 deletions

View File

@@ -147,7 +147,7 @@ tr:hover{background:#f8fafc}
</div> </div>
<div class="card-body"> <div class="card-body">
<table> <table>
<thead><tr><th>账号</th><th>昵称</th><th>邮箱</th><th>角色</th><th></th><th>债务</th><th>状态</th><th>操作</th></tr></thead> <thead><tr><th>账号</th><th>昵称</th><th>邮箱</th><th>角色</th><th></th><th>债务</th><th>状态</th><th>2FA</th><th>操作</th></tr></thead>
<tbody id="userTable"></tbody> <tbody id="userTable"></tbody>
</table> </table>
</div> </div>
@@ -348,7 +348,9 @@ async function loadUsers(){
actions+=` <button class="btn-sm btn-enable" onclick="sendResetLink(${u.id},'${escapeHtml(u.account)}')">改密</button>`; actions+=` <button class="btn-sm btn-enable" onclick="sendResetLink(${u.id},'${escapeHtml(u.account)}')">改密</button>`;
actions+=` <button class="btn-sm btn-danger" onclick="deleteUser(${u.id},'${escapeHtml(u.account)}')">删除</button>`; actions+=` <button class="btn-sm btn-danger" onclick="deleteUser(${u.id},'${escapeHtml(u.account)}')">删除</button>`;
} }
return`<tr><td>${escapeHtml(u.account)}</td><td>${escapeHtml(u.nickname)}</td><td>${emailDisplay}</td><td>${roleBadge}</td><td>${groupBadge}</td><td>${u.debt_count}笔</td><td>${statusBadge}</td><td>${actions}</td></tr>`; return`<tr><td>${escapeHtml(u.account)}</td><td>${escapeHtml(u.nickname)}</td><td>${emailDisplay}</td><td>${roleBadge}</td><td>${groupBadge}</td><td>${u.debt_count}笔</td><td>${statusBadge}</td>
<td>${u.role==='admin'?'<span style="color:#94a3b8">-</span>':(u.two_factor_enabled?'<span class="badge badge-active">已启用</span> <button class="btn-sm btn-disable" onclick="toggleUser2FA('+u.id+',false)">禁用</button>':'<span class="badge badge-disabled">未启用</span> <button class="btn-sm btn-enable" onclick="toggleUser2FA('+u.id+',true)">启用</button>')}</td>
<td>${actions}</td></tr>`;
}).join(''); }).join('');
} }
@@ -514,6 +516,25 @@ function renderNotifList(items){const el=$("notifList");if(!items.length){el.inn
async function readNotif(id){await apiFetch('/notifications/'+id+'/read',{method:'PUT'});loadNotifications();} async function readNotif(id){await apiFetch('/notifications/'+id+'/read',{method:'PUT'});loadNotifications();}
async function markAllNotifRead(){await apiFetch('/notifications/read-all',{method:'PUT'});loadNotifications();} async function markAllNotifRead(){await apiFetch('/notifications/read-all',{method:'PUT'});loadNotifications();}
async function toggleUser2FA(userId, enabled) {
const action = enabled ? '启用' : '禁用';
if (!confirm(`确定${action}该用户的双因素认证?`)) return;
const res = await apiFetch('/admin/users/' + userId + '/2fa', {
method: 'PUT',
body: JSON.stringify({enabled})
});
if (res && res.ok) {
alert(`用户 2FA 已${action}`);
loadUsers();
} else {
const data = await res.json().catch(() => ({}));
alert(data.detail || '操作失败');
}
}
init(); init();
</script> </script>
</body> </body>

View File

@@ -5,6 +5,33 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache"> <meta http-equiv="Cache-Control" content="no-cache">
<title>数据分析</title> <title>数据分析</title>
<div style="background:#fff;border-radius:16px;padding:28px;max-width:360px;width:90%;box-shadow:0 24px 80px rgba(0,0,0,.2)">
<h3>双因素认证</h3>
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;text-align:center">请使用验证器应用扫描二维码</p>
<div class="twofa-qr"><img id="twofaBindQr" width="200" height="200"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">密钥:<code id="twofaBindSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px;font-size:11px"></code></p>
<div class="form-row">
<input type="text" id="twofaBindCode" placeholder="输入 6 位验证码" maxlength="6" style="text-align:center;font-size:20px;letter-spacing:4px">
</div>
<div id="twofaBindError" class="error-msg" style="display:none"></div>
<div class="btn-row">
<button class="btn btn-main" onclick="verifyTwofaBind()">确认绑定</button>
</div>
</div>
</div>
<div id="twofaBindModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:9998;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:16px;padding:28px;max-width:360px;width:90%;box-shadow:0 24px 80px rgba(0,0,0,.2)">
<h3>双因素认证</h3>
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;text-align:center">请使用验证器应用扫描二维码</p>
<div class="twofa-qr"><img id="twofaBindQr" width="200" height="200"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">密钥:<code id="twofaBindSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px;font-size:11px"></code></p>
<div class="form-row"><input type="text" id="twofaBindCode" placeholder="输入 6 位验证码" maxlength="6" style="text-align:center;font-size:20px;letter-spacing:4px"></div>
<div id="twofaBindError" class="error-msg" style="display:none"></div>
<div class="btn-row"><button class="btn btn-main" onclick="verifyTwofaBind()">确认绑定</button></div>
</div>
</div>
<script src="/chart.min.js"></script> <script src="/chart.min.js"></script>
<style> <style>
:root{--bg:#f5f5f7;--card:rgba(255,255,255,.8);--border:rgba(0,0,0,.06);--text:#1d1d1f;--text2:#6e6e73;--text3:#aeaeb2;--blue:#0071e3;--green:#34c759;--red:#ff3b30;--orange:#ff9500;--purple:#af52de;--radius:16px;--radius-sm:10px;--radius-xs:8px} :root{--bg:#f5f5f7;--card:rgba(255,255,255,.8);--border:rgba(0,0,0,.06);--text:#1d1d1f;--text2:#6e6e73;--text3:#aeaeb2;--blue:#0071e3;--green:#34c759;--red:#ff3b30;--orange:#ff9500;--purple:#af52de;--radius:16px;--radius-sm:10px;--radius-xs:8px}
@@ -437,6 +464,16 @@ function exportHTML(e){e.preventDefault();if(!loans.length)return;let h=`<html><
</div> </div>
<div id="profileError" class="error-msg" style="display:none"></div> <div id="profileError" class="error-msg" style="display:none"></div>
<div id="profileSuccess" class="success-msg" style="display:none"></div> <div id="profileSuccess" class="success-msg" style="display:none"></div>
<div class="twofa-section" style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border)">
<label style="display:block;font-size:13px;font-weight:500;margin-bottom:8px">双因素认证</label>
<div class="twofa-status" id="twofaStatus"><span style="font-size:12px;color:var(--text3)">加载中...</span></div>
<div id="twofaSetupSection" style="display:none">
<p style="font-size:12px;color:var(--text2);margin-bottom:12px">请使用验证器应用扫描下方二维码:</p>
<div style="display:flex;justify-content:center;margin:16px 0"><img id="twofaQrCode" width="200" height="200" style="border:1px solid var(--border);border-radius:8px"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">密钥:<code id="twofaSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px;font-size:11px"></code></p>
<div style="display:flex;gap:8px;align-items:center"><input type="text" id="twofaSetupCode" placeholder="输入 6 位验证码" maxlength="6" style="flex:1"><button class="btn btn-main" style="flex:none;width:auto;padding:8px 16px;font-size:12px" onclick="verifyTwofaSetup()">确认启用</button></div>
</div>
</div>
<div class="btn-row"> <div class="btn-row">
<button class="btn btn-ghost" onclick="closeProfile()">取消</button> <button class="btn btn-ghost" onclick="closeProfile()">取消</button>
<button class="btn btn-main" onclick="saveProfile()">保存</button> <button class="btn btn-main" onclick="saveProfile()">保存</button>
@@ -451,14 +488,8 @@ function showImportModal(e){e.preventDefault();$('importMenu').classList.remove(
checkAuth().then(ok=>{if(ok)loadLoans();}); checkAuth().then(ok=>{if(ok)loadLoans();});
</script> </script>
<div class="modal-mask" id="changeNickModal"><div class="modal"><h3>修改昵称</h3><div class="form-row"><label>新昵称</label><input type="text" id="newNick" placeholder="请输入新昵称"></div><div id="nickError" class="error-msg" style="display:none"></div><div id="nickSuccess" class="success-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-ghost" onclick="closeChangeNick()">取消</button><button class="btn btn-main" onclick="doChangeNick()">确认修改</button></div></div></div>
<div class="modal-mask" id="changePwdModal"><div class="modal"><h3>修改密码</h3><div class="form-row"><label>原密码</label><input type="password" id="oldPwd" placeholder="请输入原密码"></div><div class="form-row"><label>新密码</label><input type="password" id="newPwd" placeholder="请输入新密码"></div><div class="form-row"><label>确认新密码</label><input type="password" id="newPwd2" placeholder="请再次输入新密码"></div><div id="pwdError" class="error-msg" style="display:none"></div><div id="pwdSuccess" class="success-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-ghost" onclick="closeChangePwd()">取消</button><button class="btn btn-main" onclick="doChangePwd()">确认修改</button></div></div></div>
<div class="modal-mask" id="profileModal">
<div class="modal" style="max-width:480px">
<h3>个人中心</h3>
<div class="profile-section">
<div class="form-row"><label>账号</label><input type="text" id="profileAccount" disabled style="background:#f5f5f7"></div>
<div class="form-row"><label>昵称</label><input type="text" id="profileNickname" placeholder="请输入昵称"></div> <div class="form-row"><label>昵称</label><input type="text" id="profileNickname" placeholder="请输入昵称"></div>
<div class="form-row"> <div class="form-row">
<label>邮箱</label> <label>邮箱</label>
@@ -473,6 +504,16 @@ checkAuth().then(ok=>{if(ok)loadLoans();});
</div> </div>
<div id="profileError" class="error-msg" style="display:none"></div> <div id="profileError" class="error-msg" style="display:none"></div>
<div id="profileSuccess" class="success-msg" style="display:none"></div> <div id="profileSuccess" class="success-msg" style="display:none"></div>
<div class="twofa-section" style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border)">
<label style="display:block;font-size:13px;font-weight:500;margin-bottom:8px">双因素认证</label>
<div class="twofa-status" id="twofaStatus"><span style="font-size:12px;color:var(--text3)">加载中...</span></div>
<div id="twofaSetupSection" style="display:none">
<p style="font-size:12px;color:var(--text2);margin-bottom:12px">请使用验证器应用扫描下方二维码:</p>
<div style="display:flex;justify-content:center;margin:16px 0"><img id="twofaQrCode" width="200" height="200" style="border:1px solid var(--border);border-radius:8px"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">密钥:<code id="twofaSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px;font-size:11px"></code></p>
<div style="display:flex;gap:8px;align-items:center"><input type="text" id="twofaSetupCode" placeholder="输入 6 位验证码" maxlength="6" style="flex:1"><button class="btn btn-main" style="flex:none;width:auto;padding:8px 16px;font-size:12px" onclick="verifyTwofaSetup()">确认启用</button></div>
</div>
</div>
<div class="btn-row"> <div class="btn-row">
<button class="btn btn-ghost" onclick="closeProfile()">取消</button> <button class="btn btn-ghost" onclick="closeProfile()">取消</button>
<button class="btn btn-main" onclick="saveProfile()">保存</button> <button class="btn btn-main" onclick="saveProfile()">保存</button>

View File

@@ -538,6 +538,24 @@ body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text"
<div class="modal-mask" id="changePwdModal"><div class="modal"><h3>修改密码</h3><div class="form-row"><label>原密码</label><input type="password" id="oldPwd" placeholder="请输入原密码"></div><div class="form-row"><label>新密码</label><input type="password" id="newPwd" placeholder="请输入新密码"></div><div class="form-row"><label>确认新密码</label><input type="password" id="newPwd2" placeholder="请再次输入新密码"></div><div id="pwdError" class="error-msg" style="display:none"></div><div id="pwdSuccess" class="success-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-ghost" onclick="closeChangePwd()">取消</button><button class="btn btn-main" onclick="doChangePwd()">确认修改</button></div></div></div> <div class="modal-mask" id="changePwdModal"><div class="modal"><h3>修改密码</h3><div class="form-row"><label>原密码</label><input type="password" id="oldPwd" placeholder="请输入原密码"></div><div class="form-row"><label>新密码</label><input type="password" id="newPwd" placeholder="请输入新密码"></div><div class="form-row"><label>确认新密码</label><input type="password" id="newPwd2" placeholder="请再次输入新密码"></div><div id="pwdError" class="error-msg" style="display:none"></div><div id="pwdSuccess" class="success-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-ghost" onclick="closeChangePwd()">取消</button><button class="btn btn-main" onclick="doChangePwd()">确认修改</button></div></div></div>
<div class="modal-mask" id="emailModal"><div class="modal"><h3>绑定邮箱</h3><p style="font-size:13px;color:var(--text2);line-height:1.6;margin-bottom:14px">首次使用请绑定邮箱,用于接收还款提醒和密码找回。</p><div class="form-row"><label>邮箱地址</label><input type="email" id="bindEmail" placeholder="请输入有效邮箱"></div><div id="bindEmailError" class="error-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-main" onclick="doBindEmail()">确认绑定</button></div></div></div> <div class="modal-mask" id="emailModal"><div class="modal"><h3>绑定邮箱</h3><p style="font-size:13px;color:var(--text2);line-height:1.6;margin-bottom:14px">首次使用请绑定邮箱,用于接收还款提醒和密码找回。</p><div class="form-row"><label>邮箱地址</label><input type="email" id="bindEmail" placeholder="请输入有效邮箱"></div><div id="bindEmailError" class="error-msg" style="display:none"></div><div class="btn-row"><button class="btn btn-main" onclick="doBindEmail()">确认绑定</button></div></div></div>
<div id="twofaBindModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:9998;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:16px;padding:28px;max-width:360px;width:90%;box-shadow:0 24px 80px rgba(0,0,0,.2)">
<h3>双因素认证</h3>
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;text-align:center">请使用验证器应用扫描二维码</p>
<div class="twofa-qr"><img id="twofaBindQr" width="200" height="200"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">密钥:<code id="twofaBindSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px"></code></p>
<div class="form-row">
<input type="text" id="twofaBindCode" placeholder="输入 6 位验证码" maxlength="6" style="text-align:center;font-size:20px;letter-spacing:4px">
</div>
<div id="twofaBindError" class="error-msg" style="display:none"></div>
<div class="btn-row">
<button class="btn btn-main" onclick="verifyTwofaBind()">确认绑定</button>
</div>
</div>
</div>
<script> <script>
const API='/api/v1'; const API='/api/v1';
let token=localStorage.getItem('access_token')||''; let token=localStorage.getItem('access_token')||'';
@@ -657,12 +675,6 @@ async function saveProfile(){
// 2FA 相关函数 // 2FA 相关函数
let twofaTempToken = ''; let twofaTempToken = '';
function showTwofaModal(tempToken) {
twofaTempToken = tempToken;
$('twofaCode').value = '';
$('twofaError').style.display = 'none';
$('twofaModal').classList.add('show');
}
function closeTwofaModal() { function closeTwofaModal() {
$('twofaModal').classList.remove('show'); $('twofaModal').classList.remove('show');
@@ -782,27 +794,120 @@ async function showDisableTwofa() {
} }
} }
let twofaBindToken = '';
function closeTwofaBindModal() {
$('twofaBindModal').classList.remove('show');
twofaBindToken = '';
}
async function verifyTwofaBind() {
var code = $('twofaBindCode').value.trim();
if (!code || code.length !== 6) {
$('twofaBindError').textContent = '请输入 6 位验证码';
$('twofaBindError').style.display = '';
return;
}
var res = await apiFetch('/auth/2fa/verify', {
method: 'POST',
body: JSON.stringify({code: code})
});
if (res && res.ok) {
twofaRequired = false;
$('twofaBindModal').style.display = 'none';
document.body.style.overflow = '';
showToast('双因素认证已绑定成功', 'success');
} else {
var data = await res.json().catch(function(){return {};});
$('twofaBindError').textContent = data.detail || '验证码错误';
$('twofaBindError').style.display = '';
}
}
function checkTwofaBinding(){
apiFetch('/auth/2fa/status').then(function(res){
if(res&&res.ok){
res.json().then(function(data){
if(!data.enabled){
// 需要绑定 2FA显示弹窗并阻止操作
showTwofaBindModal();
}
});
}
});
}
var twofaRequired=false;
function showTwofaBindModal(){
twofaRequired=true;
// 检查是否已经绑定
apiFetch('/auth/2fa/status').then(function(res){
if(res&&res.ok){
res.json().then(function(data){
if(data.enabled && data.has_secret){
// 已经绑定了,直接关闭
twofaRequired=false;
$('twofaBindModal').style.display='none';
document.body.style.overflow='';
return;
}
// 获取二维码
apiFetch('/auth/2fa/setup',{method:'POST'}).then(function(res2){
if(res2&&res2.ok){
res2.json().then(function(data2){
$('twofaBindQr').src=data2.qr_code;
$('twofaBindSecret').textContent=data2.secret;
$('twofaBindCode').value='';
$('twofaBindError').style.display='none';
$('twofaBindModal').style.display='flex';
document.body.style.overflow='hidden';
});
}
});
});
}
});
}
function closeTwofaBindModal(){
if(!twofaRequired) return; // 如果必须绑定,不允许关闭
$('twofaBindModal').style.display='none';
document.body.style.overflow='';
}
async function doLogin(){ async function doLogin(){
const account=$('loginEmail').value.trim(),pwd=$('loginPwd').value; var account = $('loginEmail').value.trim();
if(!account||!pwd)return showLoginError('请输入账号和密码'); var pwd = $('loginPwd').value;
hideLoginError(); if(!account||!pwd){$('loginError').textContent='请输入账号和密码';$('loginError').style.display='';return;}
$('loginError').style.display='none';
try{ try{
const res=await fetch(API+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account,password:pwd})}); var res = await fetch(API+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account:account,password:pwd})});
const data=await res.json(); var data = await res.json();
if(!res.ok){ if(!res.ok){
if(res.status===429&&data.retry_after){startLoginCountdown(data.retry_after);} if(res.status===429&&data.retry_after){startLoginCountdown(data.retry_after);}
else{showLoginError(data.detail||'登录失败');} else{$('loginError').textContent=data.detail||'登录失败';$('loginError').style.display='';}
return; return;
} }
// 检查是否需要 2FA token=data.access_token;
if(data.require_2fa){ refreshToken=data.refresh_token;
showTwofaModal(data.temp_token); localStorage.setItem('access_token',token);
return; localStorage.setItem('refresh_token',refreshToken);
await checkAuth();
await loadLoans();
// 登录后检查是否需要绑定 2FA
checkTwofaBinding();
}catch(e){
$('loginError').textContent='网络连接异常,请稍后重试';
$('loginError').style.display='';
} }
token=data.access_token;refreshToken=data.refresh_token;
localStorage.setItem('access_token',token);localStorage.setItem('refresh_token',refreshToken);
await checkAuth();await loadLoans();
}catch(e){showLoginError('网络连接异常,请稍后重试');}
} }
async function checkAuth(){ async function checkAuth(){
@@ -1022,24 +1127,10 @@ function exportHTML(e){e.preventDefault();const vl=getVisibleLoans();if(!vl.leng
.twofa-qr img{border:1px solid var(--border);border-radius:8px} .twofa-qr img{border:1px solid var(--border);border-radius:8px}
.twofa-input{display:flex;gap:8px;align-items:center} .twofa-input{display:flex;gap:8px;align-items:center}
.twofa-input input{flex:1} .twofa-input input{flex:1}
</style></head><body><h2>债务统计报表 - ${today()}</h2>`;vl.forEach(l=>{const ti=l.schedule.reduce((s,r)=>s+r.it,0);const st=l.paid.length>=l.periods;h+=`<h3>${escapeHtml(l.name)} - ${l.date} - ¥${fmt(l.amount)}${st?' <span class="done">已还清</span>':''}</h3><p>年化率 ${l.rate}% · ${methodLabel(l.method)} · ${l.periods}期 · 总利息 ¥${ti.toFixed(2)}</p><table><tr><th>期数</th><th>日期</th><th>还款额</th><th>本金</th><th>利息</th><th>剩余本金</th><th>状态</th><th>用途</th></tr>`;l.schedule.forEach(r=>{h+=`<tr><td>${r.p}</td><td>${r.date}</td><td>¥${fmt(r.pay)}</td><td>¥${fmt(r.pr)}</td><td>¥${fmt(r.it)}</td><td>¥${fmt(r.rp)}</td><td>${l.paid.includes(r.p)?'✓ 已还':'待还'}</td></tr>`;});h+=`</table>`;});h+=` </style>
<div class="modal-mask" id="twofaModal">
<div class="modal">
<h3>双因素认证</h3>
<p style="font-size:13px;color:var(--text2);margin-bottom:16px;text-align:center">请输入手机验证器应用中的 6 位验证码</p>
<div class="form-row">
<input type="text" id="twofaCode" placeholder="000000" maxlength="6" style="text-align:center;font-size:24px;letter-spacing:8px;font-weight:600">
</div>
<div id="twofaError" class="error-msg" style="display:none"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="closeTwofaModal()">取消</button>
<button class="btn btn-main" onclick="verifyTwofaLogin()">验证</button>
</div>
</div>
</div>
<div class="modal-mask" id="profileModal"> <div id="profileModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:9998;align-items:center;justify-content:center">
<div class="modal" style="max-width:480px"> <div class="modal" style="max-width:480px">
<h3>个人中心</h3> <h3>个人中心</h3>
<div class="profile-section"> <div class="profile-section">
@@ -1157,68 +1248,3 @@ async function doForgotStep3(){
checkAuth().then(ok=>{if(ok)loadLoans();}); checkAuth().then(ok=>{if(ok)loadLoans();});
</script> </script>
<div class="modal-mask" id="twofaModal">
<div class="modal">
<h3>双因素认证</h3>
<p style="font-size:13px;color:var(--text2);margin-bottom:16px;text-align:center">请输入手机验证器应用中的 6 位验证码</p>
<div class="form-row">
<input type="text" id="twofaCode" placeholder="000000" maxlength="6" style="text-align:center;font-size:24px;letter-spacing:8px;font-weight:600">
</div>
<div id="twofaError" class="error-msg" style="display:none"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="closeTwofaModal()">取消</button>
<button class="btn btn-main" onclick="verifyTwofaLogin()">验证</button>
</div>
</div>
</div>
<div class="modal-mask" id="profileModal">
<div class="modal" style="max-width:480px">
<h3>个人中心</h3>
<div class="profile-section">
<div class="form-row"><label>账号</label><input type="text" id="profileAccount" disabled style="background:#f5f5f7"></div>
<div class="form-row"><label>昵称</label><input type="text" id="profileNickname" placeholder="请输入昵称"></div>
<div class="form-row">
<label>邮箱</label>
<div style="display:flex;gap:8px">
<input type="email" id="profileEmail" placeholder="请输入邮箱" style="flex:1">
<button class="btn btn-ghost" style="flex:none;width:auto;padding:8px 16px;font-size:12px" onclick="sendEmailVerify()">发送验证码</button>
</div>
</div>
<div class="form-row" id="emailCodeRow" style="display:none">
<label>验证码</label>
<input type="text" id="profileEmailCode" placeholder="请输入6位验证码" maxlength="6">
</div>
<div class="twofa-section">
<label style="display:block;font-size:13px;font-weight:500;margin-bottom:8px">双因素认证 (2FA)</label>
<div class="twofa-status" id="twofaStatus">
<span class="twofa-badge disabled">加载中...</span>
</div>
<div id="twofaSetupSection" style="display:none">
<p style="font-size:12px;color:var(--text2);margin-bottom:12px">请使用 Google Authenticator 等应用扫描下方二维码:</p>
<div class="twofa-qr"><img id="twofaQrCode" width="200" height="200"></div>
<p style="font-size:11px;color:var(--text3);text-align:center;margin-bottom:12px">或手动输入密钥:<code id="twofaSecret" style="background:#f5f5f7;padding:2px 6px;border-radius:4px"></code></p>
<div class="form-row">
<label>验证码</label>
<div class="twofa-input">
<input type="text" id="twofaSetupCode" placeholder="输入 6 位验证码" maxlength="6">
<button class="btn btn-main" style="flex:none;width:auto;padding:8px 16px;font-size:12px" onclick="verifyTwofaSetup()">确认启用</button>
</div>
</div>
</div>
</div>
<div id="profileError" class="error-msg" style="display:none"></div>
<div id="profileSuccess" class="success-msg" style="display:none"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="closeProfile()">取消</button>
<button class="btn btn-main" onclick="saveProfile()">保存</button>
</div>
</div>
</div>
</div>
</body>
</html>