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

@@ -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="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>
const API='/api/v1';
let token=localStorage.getItem('access_token')||'';
@@ -657,12 +675,6 @@ async function saveProfile(){
// 2FA 相关函数
let twofaTempToken = '';
function showTwofaModal(tempToken) {
twofaTempToken = tempToken;
$('twofaCode').value = '';
$('twofaError').style.display = 'none';
$('twofaModal').classList.add('show');
}
function closeTwofaModal() {
$('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(){
const account=$('loginEmail').value.trim(),pwd=$('loginPwd').value;
if(!account||!pwd)return showLoginError('请输入账号和密码');
hideLoginError();
try{
const res=await fetch(API+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account,password:pwd})});
const data=await res.json();
if(!res.ok){
if(res.status===429&&data.retry_after){startLoginCountdown(data.retry_after);}
else{showLoginError(data.detail||'登录失败');}
return;
}
// 检查是否需要 2FA
if(data.require_2fa){
showTwofaModal(data.temp_token);
return;
}
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('网络连接异常,请稍后重试');}
var account = $('loginEmail').value.trim();
var pwd = $('loginPwd').value;
if(!account||!pwd){$('loginError').textContent='请输入账号和密码';$('loginError').style.display='';return;}
$('loginError').style.display='none';
try{
var res = await fetch(API+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account:account,password:pwd})});
var data = await res.json();
if(!res.ok){
if(res.status===429&&data.retry_after){startLoginCountdown(data.retry_after);}
else{$('loginError').textContent=data.detail||'登录失败';$('loginError').style.display='';}
return;
}
token=data.access_token;
refreshToken=data.refresh_token;
localStorage.setItem('access_token',token);
localStorage.setItem('refresh_token',refreshToken);
await checkAuth();
await loadLoans();
// 登录后检查是否需要绑定 2FA
checkTwofaBinding();
}catch(e){
$('loginError').textContent='网络连接异常,请稍后重试';
$('loginError').style.display='';
}
}
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-input{display:flex;gap:8px;align-items:center}
.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">
<h3>个人中心</h3>
<div class="profile-section">
@@ -1157,68 +1248,3 @@ async function doForgotStep3(){
checkAuth().then(ok=>{if(ok)loadLoans();});
</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>