v2.4: 响应式布局 + 借款按钮优化

- 所有页面添加响应式CSS,支持手机/电脑自适应
- index.html: 导航栏active状态、表单、统计、借款列表响应式
- analysis.html: 图表、统计卡片响应式
- admin.html: 表格、搜索栏响应式
- reset-password.html: 登录卡片响应式
- 修复导航栏active类:analysis.html的分析标签高亮
- 非本人借款:隐藏修改按钮,快速还款改为快速查看
- 修复main.py重复函数
- 添加debt-manager.sh管理脚本
This commit is contained in:
OP
2026-07-01 22:50:09 +08:00
parent f18cdf5249
commit 056c4defec
7 changed files with 597 additions and 41 deletions

View File

@@ -287,6 +287,80 @@ body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text"
.profile-section .form-row input{width:100%;padding:10px 14px;border:1px solid #d2d2d7;border-radius:var(--radius-xs);font-size:14px;outline:none;transition:all .2s}
.profile-section .form-row input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,113,227,.12)}
.profile-section .form-row input:disabled{background:#f5f5f7;color:var(--text3)}
/* RESPONSIVE - Mobile & Tablet */
@media(max-width:768px){
.header-inner{padding:0 12px;height:48px}
.header h1{font-size:15px}
.header-center{position:static;transform:none}
.nav-tabs{gap:2px;padding:2px}
.nav-tab{padding:5px 12px;font-size:12px}
.header-right{gap:2px}
.header-right a,.hdr-btn{padding:5px 8px;font-size:11px}
.user-info{padding:5px 8px;font-size:12px}
.notif-bell{padding:4px 6px}
.notif-dropdown{width:calc(100vw - 20px);right:-10px}
.main{padding:12px;gap:12px}
.panel-title{padding:12px 14px;font-size:13px}
.panel-body{padding:12px 14px}
.form-grid{grid-template-columns:1fr;gap:10px}
.form-row input,.form-row select{padding:10px 12px;font-size:14px}
.loan-item{padding:8px 14px 8px 28px}
.loan-item .iamt{font-size:12px}
.loan-actions-popup{padding:6px 14px;flex-wrap:wrap}
.stats{grid-template-columns:repeat(2,1fr);gap:8px}
.stat-card{padding:12px}
.stat-card .val{font-size:16px}
.month-header{padding:10px 14px}
.month-header .total{font-size:13px}
.loan-row .right{width:90px}
.loan-row .pay-amt{font-size:13px}
.modal{padding:20px;width:95%;max-width:none}
.modal h3{font-size:16px;margin-bottom:14px}
.auth-card{width:100%;max-width:360px;padding:32px 24px;margin:16px}
.auth-card h2{font-size:20px;margin-bottom:24px}
.btn-row{flex-direction:column;gap:8px}
.btn{padding:12px 0}
.toast{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
}
@media(max-width:480px){
.header-inner{padding:0 10px;height:44px}
.header h1{font-size:14px}
.nav-tab{padding:4px 10px;font-size:11px}
.header-right a,.hdr-btn{padding:4px 6px;font-size:10px}
.main{padding:10px;gap:10px}
.panel-title{padding:10px 12px}
.panel-body{padding:10px 12px}
.stats{grid-template-columns:1fr 1fr}
.stat-card{padding:10px}
.stat-card .lbl{font-size:10px}
.stat-card .val{font-size:14px}
.form-grid{gap:8px}
.loan-group-header{padding:10px 12px}
.loan-group-header .gname{font-size:14px}
.loan-item{padding:8px 12px}
.month-header{padding:8px 12px}
.month-header .ym{font-size:13px}
.month-detail{padding:0 12px 8px}
.auth-card{padding:24px 20px}
.auth-card h2{font-size:18px;margin-bottom:20px}
}
</style>
</head>
<body>
@@ -636,7 +710,7 @@ $('fTotalInt').addEventListener('focus',function(){$('intHint').textContent='输
function calcSchedule(a,r,n,m,start){const mr=r/100/12,sd=new Date(start),list=[];if(m==='e'){const mp=mr===0?a/n:a*mr*Math.pow(1+mr,n)/(Math.pow(1+mr,n)-1);let rp=a;for(let i=0;i<n;i++){const it=rp*mr,pr=mp-it;rp-=pr;const dd=new Date(sd);dd.setMonth(dd.getMonth()+i+1);list.push({p:i+1,date:dd.toISOString().slice(0,10),pay:Math.round(mp*100)/100,pr:Math.round(pr*100)/100,it:Math.round(it*100)/100,rp:Math.round(Math.max(0,rp)*100)/100});}}else if(m==='p'){const pp=a/n;let rp=a;for(let i=0;i<n;i++){const it=rp*mr,pay=pp+it;rp-=pp;const dd=new Date(sd);dd.setMonth(dd.getMonth()+i+1);list.push({p:i+1,date:dd.toISOString().slice(0,10),pay:Math.round(pay*100)/100,pr:Math.round(pp*100)/100,it:Math.round(it*100)/100,rp:Math.round(Math.max(0,rp)*100)/100});}}else{const mi=a*mr;let rp=a;for(let i=0;i<n;i++){const isLast=i===n-1;const pr=isLast?a:0;const pay=mi+pr;const dd=new Date(sd);dd.setMonth(dd.getMonth()+i+1);list.push({p:i+1,date:dd.toISOString().slice(0,10),pay:Math.round(pay*100)/100,pr:Math.round(pr*100)/100,it:Math.round(mi*100)/100,rp:Math.round(Math.max(0,rp-pr)*100)/100});if(isLast)rp=0;}}return list;}
function renderAll(){renderList();renderRight();}
function renderList(){const el=$('loanList');if(!loans.length){el.innerHTML='<div class="empty-tip">暂无记录</div>';return;}let visibleLoans=loans;if(hideOthers)visibleLoans=loans.filter(l=>l.user_id===currentUser.id);const groups=new Map();visibleLoans.forEach(l=>{if(!groups.has(l.name))groups.set(l.name,[]);groups.get(l.name).push(l);});const sortedGroups=[...groups.entries()].sort(([,a],[,b])=>{const ea=a.reduce((m,l)=>l.date<m?l.date:m,'9999-99-99');const eb=b.reduce((m,l)=>l.date<m?l.date:m,'9999-99-99');return eb.localeCompare(ea);});let html='';sortedGroups.forEach(([name,items])=>{items.sort((a,b)=>b.date.localeCompare(a.date));let visibleItems=items;if(hidePaidItems)visibleItems=items.filter(l=>l.paid.length<l.periods);if(hidePaidItems&&!visibleItems.length)return;const isOpen=openGroups.has(name);const totalAmt=visibleItems.reduce((s,l)=>s+l.amount,0);const doneCount=items.filter(l=>l.paid.length>=l.periods).length;html+=`<div class="loan-group"><div class="loan-group-header" onclick="toggleGroup('${name}')"><span><span class="arrow${isOpen?' open':''}">▶</span><span class="gname">${name}</span></span><span class="gsummary">${visibleItems.length}笔 · ¥${fmt(totalAmt)}${doneCount===items.length?' · <span style="color:var(--green)">全部还清</span>':''}</span></div><div class="loan-group-body${isOpen?' open':''}">`;visibleItems.forEach(l=>{const mt=methodLabel(l.method),tc=methodTag(l.method);const allDone=l.paid.length>=l.periods;const noInt=l.rate===0;const rc=l.rate===0?'':l.rate<12?'rate-low':l.rate<=24?'rate-mid':'rate-high';const hasPopup=actionLoanId===l.id;const isOwner=l.user_id===currentUser.id;html+=`<div class="loan-item${l.id===editId?' active':''}${allDone?' done-item':''}" onclick="toggleLoanAction(${l.id})"><div style="flex:1"><div class="iname">${escapeHtml(l.purpose||'')}${allDone?'<span class="badge-done">已还清</span>':''} <span class="tag ${tc}">${mt}</span>${noInt?' <span class="tag tag-0">无息</span>':''}${l.owner_name?` <span class="tag" style="background:rgba(0,0,0,.04);color:var(--text2)">${l.owner_name}</span>`:''}</div><div class="isub">${l.paid.length}/${l.periods}期 · <span class="${rc}">${l.rate}%</span> · ${l.date}</div></div><div style="text-align:right"><div class="iamt">¥${fmt(l.amount)}</div></div></div>${hasPopup?`<div class="loan-actions-popup"><button class="act-btn act-btn-edit" onclick="event.stopPropagation();doEditLoan(${l.id})">✏️ 修改</button><button class="act-btn act-btn-pay" onclick="event.stopPropagation();toggleQuickPay(${l.id})">💰 快速还款</button>${isOwner?`<button class="act-btn act-btn-del" onclick="event.stopPropagation();showDel(${l.id})">🗑 删除</button>`:''}</div>`:''}${quickPayLoanId===l.id?renderQuickPay(l):''}`;});html+=`</div></div>`;});el.innerHTML=html;}
function renderList(){const el=$('loanList');if(!loans.length){el.innerHTML='<div class="empty-tip">暂无记录</div>';return;}let visibleLoans=loans;if(hideOthers)visibleLoans=loans.filter(l=>l.user_id===currentUser.id);const groups=new Map();visibleLoans.forEach(l=>{if(!groups.has(l.name))groups.set(l.name,[]);groups.get(l.name).push(l);});const sortedGroups=[...groups.entries()].sort(([,a],[,b])=>{const ea=a.reduce((m,l)=>l.date<m?l.date:m,'9999-99-99');const eb=b.reduce((m,l)=>l.date<m?l.date:m,'9999-99-99');return eb.localeCompare(ea);});let html='';sortedGroups.forEach(([name,items])=>{items.sort((a,b)=>b.date.localeCompare(a.date));let visibleItems=items;if(hidePaidItems)visibleItems=items.filter(l=>l.paid.length<l.periods);if(hidePaidItems&&!visibleItems.length)return;const isOpen=openGroups.has(name);const totalAmt=visibleItems.reduce((s,l)=>s+l.amount,0);const doneCount=items.filter(l=>l.paid.length>=l.periods).length;html+=`<div class="loan-group"><div class="loan-group-header" onclick="toggleGroup('${name}')"><span><span class="arrow${isOpen?' open':''}">▶</span><span class="gname">${name}</span></span><span class="gsummary">${visibleItems.length}笔 · ¥${fmt(totalAmt)}${doneCount===items.length?' · <span style="color:var(--green)">全部还清</span>':''}</span></div><div class="loan-group-body${isOpen?' open':''}">`;visibleItems.forEach(l=>{const mt=methodLabel(l.method),tc=methodTag(l.method);const allDone=l.paid.length>=l.periods;const noInt=l.rate===0;const rc=l.rate===0?'':l.rate<12?'rate-low':l.rate<=24?'rate-mid':'rate-high';const hasPopup=actionLoanId===l.id;const isOwner=l.user_id===currentUser.id;html+=`<div class="loan-item${l.id===editId?' active':''}${allDone?' done-item':''}" onclick="toggleLoanAction(${l.id})"><div style="flex:1"><div class="iname">${escapeHtml(l.purpose||'')}${allDone?'<span class="badge-done">已还清</span>':''} <span class="tag ${tc}">${mt}</span>${noInt?' <span class="tag tag-0">无息</span>':''}${l.owner_name?` <span class="tag" style="background:rgba(0,0,0,.04);color:var(--text2)">${l.owner_name}</span>`:''}</div><div class="isub">${l.paid.length}/${l.periods}期 · <span class="${rc}">${l.rate}%</span> · ${l.date}</div></div><div style="text-align:right"><div class="iamt">¥${fmt(l.amount)}</div></div></div>${hasPopup?`<div class="loan-actions-popup">${isOwner?`<button class="act-btn act-btn-edit" onclick="event.stopPropagation();doEditLoan(${l.id})">✏️ 修改</button>`:''}<button class="act-btn act-btn-pay" onclick="event.stopPropagation();toggleQuickPay(${l.id})">${isOwner?'💰 快速还款':'👁 快速查看'}</button>${isOwner?`<button class="act-btn act-btn-del" onclick="event.stopPropagation();showDel(${l.id})">🗑 删除</button>`:''}</div>`:''}${quickPayLoanId===l.id?renderQuickPay(l):''}`;});html+=`</div></div>`;});el.innerHTML=html;}
function toggleGroup(name){if(openGroups.has(name))openGroups.delete(name);else openGroups.add(name);renderList();}
function showLoanDetail(id){const l=loans.find(x=>x.id===id);if(!l)return;editId=id;$('fDate').value=l.date;$('fName').value=l.name;$('fPurpose').value=l.purpose||'';$('fAmt').value=l.amount;$('fRate').value=l.rate;$('fTotalInt').value='';$('fN').value=l.periods;$('fMethod').value=l.method;$('rateHint').textContent='';$('intHint').textContent='';$('formTitle').textContent='编辑中';$('btnSubmit').textContent='修改';document.querySelector('.main').scrollIntoView({behavior:'smooth',block:'start'});}
@@ -704,6 +778,80 @@ function exportHTML(e){e.preventDefault();const vl=getVisibleLoans();if(!vl.leng
.profile-section .form-row input{width:100%;padding:10px 14px;border:1px solid #d2d2d7;border-radius:var(--radius-xs);font-size:14px;outline:none;transition:all .2s}
.profile-section .form-row input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,113,227,.12)}
.profile-section .form-row input:disabled{background:#f5f5f7;color:var(--text3)}
/* RESPONSIVE - Mobile & Tablet */
@media(max-width:768px){
.header-inner{padding:0 12px;height:48px}
.header h1{font-size:15px}
.header-center{position:static;transform:none}
.nav-tabs{gap:2px;padding:2px}
.nav-tab{padding:5px 12px;font-size:12px}
.header-right{gap:2px}
.header-right a,.hdr-btn{padding:5px 8px;font-size:11px}
.user-info{padding:5px 8px;font-size:12px}
.notif-bell{padding:4px 6px}
.notif-dropdown{width:calc(100vw - 20px);right:-10px}
.main{padding:12px;gap:12px}
.panel-title{padding:12px 14px;font-size:13px}
.panel-body{padding:12px 14px}
.form-grid{grid-template-columns:1fr;gap:10px}
.form-row input,.form-row select{padding:10px 12px;font-size:14px}
.loan-item{padding:8px 14px 8px 28px}
.loan-item .iamt{font-size:12px}
.loan-actions-popup{padding:6px 14px;flex-wrap:wrap}
.stats{grid-template-columns:repeat(2,1fr);gap:8px}
.stat-card{padding:12px}
.stat-card .val{font-size:16px}
.month-header{padding:10px 14px}
.month-header .total{font-size:13px}
.loan-row .right{width:90px}
.loan-row .pay-amt{font-size:13px}
.modal{padding:20px;width:95%;max-width:none}
.modal h3{font-size:16px;margin-bottom:14px}
.auth-card{width:100%;max-width:360px;padding:32px 24px;margin:16px}
.auth-card h2{font-size:20px;margin-bottom:24px}
.btn-row{flex-direction:column;gap:8px}
.btn{padding:12px 0}
.toast{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
}
@media(max-width:480px){
.header-inner{padding:0 10px;height:44px}
.header h1{font-size:14px}
.nav-tab{padding:4px 10px;font-size:11px}
.header-right a,.hdr-btn{padding:4px 6px;font-size:10px}
.main{padding:10px;gap:10px}
.panel-title{padding:10px 12px}
.panel-body{padding:10px 12px}
.stats{grid-template-columns:1fr 1fr}
.stat-card{padding:10px}
.stat-card .lbl{font-size:10px}
.stat-card .val{font-size:14px}
.form-grid{gap:8px}
.loan-group-header{padding:10px 12px}
.loan-group-header .gname{font-size:14px}
.loan-item{padding:8px 12px}
.month-header{padding:8px 12px}
.month-header .ym{font-size:13px}
.month-detail{padding:0 12px 8px}
.auth-card{padding:24px 20px}
.auth-card h2{font-size:18px;margin-bottom:20px}
}
</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+=`
<div class="modal-mask" id="profileModal">
<div class="modal" style="max-width:480px">