fix: 同步两个页面的弹窗结构
- index.html 添加 profileModal HTML - analysis.html 移除错误位置的 twofaBindModal - analysis.html 添加 twofaBindModal 到正确位置 - analysis.html profileModal 添加完整 2FA 设置部分 - 两个页面的 header/menu/modal 现在一致 不打包,等用户确认后再打包
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user