feat: wire admin UI to credit ledger APIs

This commit is contained in:
OpenClaw Agent
2026-09-17 06:08:20 +08:00
parent d9df512e0a
commit 3025019504
4 changed files with 146 additions and 2 deletions
+2 -2
View File
@@ -1367,10 +1367,10 @@ async function loadUserCredit(id){try{const c=await api('/admin/users/'+id+'/cre
async function openUserDetail(id){const u=adminUsersCache.find(x=>x.id===id);if(!u)return;const next=u.status==='active'?'disabled':'active';$("detailUserName").textContent=u.username;$("detailUserMeta").textContent="#"+u.id+" · "+u.role+" · "+u.status;$("userDetailContent").innerHTML="<div class='detail-grid'><div class='detail-card'><h4>账户</h4><div class='muted'>角色</div><div>"+esc(u.role)+"</div><div class='muted' style='margin-top:8px'>状态</div><div>"+esc(u.status)+"</div><div class='detail-actions'><button class='btn alt' id='detailReset'>重置密码</button><button class='btn alt' id='detailToggle'>"+(u.status==='active'?'禁用账户':'启用账户')+"</button></div></div><div class='detail-card' id='detailCredits'>加载积分中…</div></div>";const drawer = $('adminUserDetail');
const backdrop = $('drawerBackdrop');
if(drawer) drawer.classList.add('active');
if(backdrop) backdrop.classList.add('active');$("detailReset").onclick=()=>resetUserPassword(id);$("detailToggle").onclick=()=>toggleUser(id,next);try{const c=await api('/admin/users/'+id+'/credits');$("detailCredits").innerHTML="<h4>积分</h4><div class='detail-value'>"+c.balance+"</div><div class='muted'>可用 "+c.available+" · 预留 "+c.reserved+" · 下载 "+c.tasks+" 次</div><div class='detail-actions'><button class='btn' id='detailAdd'>增加积分</button><button class='btn alt' id='detailSub'>扣除积分</button></div>";$("detailAdd").onclick=()=>adjustCredits(id,1);$("detailSub").onclick=()=>adjustCredits(id,-1)}catch(e){$("detailCredits").textContent=e.message}}
if(backdrop) backdrop.classList.add('active');$("detailReset").onclick=()=>resetUserPassword(id);$("detailToggle").onclick=()=>toggleUser(id,next);try{const c=await api('/admin/users/'+id+'/credits');$("detailCredits").innerHTML="<h4>积分</h4><div class='detail-value'>"+c.balance+"</div><div class='muted'>可用 "+c.available+" · 预留 "+c.reserved+" · 下载 "+c.tasks+" 次</div><div class='detail-actions'><button class='btn' id='detailAdd'>增加积分</button><button class='btn alt' id='detailSub'>扣除积分</button></div>";$("detailAdd").onclick=()=>adjustCredits(id,1);$("detailSub").onclick=()=>adjustCredits(id,-1);const l=await api('/admin/users/'+id+'/credits/transactions?limit=30');$("detailCredits").innerHTML+="<div class='ledger' style='margin-top:18px'><div class='muted' style='margin-bottom:6px'>最近积分流水</div>"+((l.data||[]).map(t=>"<div class='ledger-item'><div><b>"+esc(t.type)+"</b> <span class='"+(t.amount>=0?'positive':'negative')+"'>"+(t.amount>=0?'+':'')+t.amount+"</span></div><div class='meta'>余额 "+t.balanceAfter+" · "+esc(t.reason||'')+" · "+esc(t.createdAt||'')+"</div></div>").join('')||"<div class='empty'>暂无流水</div>")+"</div>"}catch(e){$("detailCredits").textContent=e.message}}
async function adjustCredits(id,sign){const raw=prompt(sign>0?'增加多少积分?':'扣除多少积分?','10');if(raw===null)return;const amount=Math.floor(Number(raw));if(!Number.isFinite(amount)||amount<=0){alert('请输入正整数');return}const reason=prompt('调整原因(可选)','管理员调整积分');if(reason===null)return;try{const x=await api('/admin/users/'+id+'/credits',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({amount:sign*amount,reason})});alert('调整成功,当前可用积分:'+x.available);openUserDetail(id);loadAdmin()}catch(e){alert('积分调整失败:'+e.message)}}
async function resetUserPassword(id){const pass=prompt('输入新密码(至少 8 位):');if(pass===null)return;if(pass.length<8){alert('密码至少 8 位');return}try{await api('/admin/users/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pass})});alert('密码已重置,该用户的其他登录会话已失效')}catch(e){alert('密码重置失败:'+e.message)}}
async function loadCreditLedger(){try{const u=await api('/admin/users');const users=u.data||[];let total=0;for(const x of users){try{const c=await api('/admin/users/'+x.id+'/credits');total+=Number(c.balance)||0}catch(e){}}$('adminCreditSummary').innerHTML='<div class="summary-card">用户<strong>'+users.length+'</strong></div><div class="summary-card">当前余额<strong>'+total+'</strong></div><div class="summary-card">单位<strong>积分</strong></div>';$('creditLedger').innerHTML='<div class="notice">积分流水按用户查看。进入「用户管理 → 查看详情」即可调整积分并查看该用户当前余额。</div>'}catch(e){$('adminCreditSummary').textContent=e.message}}
async function loadCreditLedger(){try{const u=await api('/admin/users');const users=u.data||[];let total=0;for(const x of users)total+=Number(x.balance)||0;const l=await api('/admin/credits/transactions?limit=100');$('adminCreditSummary').innerHTML='<div class="summary-card">用户<strong>'+users.length+'</strong></div><div class="summary-card">当前余额<strong>'+total+'</strong></div><div class="summary-card">流水<strong>'+((l.data||[]).length)+'</strong></div>';$('creditLedger').innerHTML=(l.data||[]).map(t=>'<div class="ledger-item"><div><b>#'+t.userId+'</b> · '+esc(t.type)+' <span class="'+(t.amount>=0?'positive':'negative')+'">'+(t.amount>=0?'+':'')+t.amount+'</span></div><div class="meta">余额 '+t.balanceAfter+' · '+esc(t.reason||'')+' · '+esc(t.createdAt||'')+'</div></div>').join('')||'<div class="empty">暂无积分流水</div>'}catch(e){$('adminCreditSummary').textContent=e.message}}
async function loadRedeemAdmin(){const b=await api('/admin/redeem');$('batches').innerHTML=(b.data||[]).map(x=>'<div class="notice">#'+x.id+' · '+x.credits+' 积分 × '+x.count+(x.note?' · '+esc(x.note):'')+' <button class="btn alt" onclick="revokeBatch('+x.id+')">撤销未使用</button></div>').join('')}
async function loadAuditAdmin(){const l=await api('/admin/audit');$('audit').innerHTML=(l.data||[]).map(x=>'<div class="ledger-item"><b>'+esc(x.action)+'</b><div class="meta">'+esc(x.targetType)+'#'+esc(x.targetId)+' · '+esc(x.metadata||'')+' · '+esc(x.createdAt)+'</div></div>').join('')||'<div class="empty">暂无日志</div>'}
async function toggleUser(id,status){const u=adminUsersCache.find(x=>x.id===id);status=status||(u&&u.status==='active'?'disabled':'active');try{await api('/admin/users/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status})});const u=adminUsersCache.find(x=>x.id===id);if(u)u.status=status;renderAdminUsers()}catch(e){alert('操作失败:'+e.message)}}