-
系统全局 WebDAV
-
全局云盘存储同步配置(未配置私有 WebDAV 时兜底使用)。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -1195,11 +1310,69 @@ async function api(u,o){const r=await fetch('/api'+u,o);if(r.status===401){showL
function showLogin(){document.getElementById('loginOverlay').style.display='flex'}
async function loadAccount(){try{const c=await api('/credits');$('creditPanel').textContent='余额 '+c.balance+' · 预留 '+c.reserved+' · 可用 '+c.available;const u=window.currentUser||{};$('userDavCard').style.display=(u.role==='admin'||u.role==='privileged')?'':'none';if(u.role==='admin'||u.role==='privileged'){const d=await api('/webdav/config');$('userDavUrl').value=d.url||'';$('userDavUser').value=d.username||'';$('userDavRoot').value=d.rootPath||''}}catch(e){$('creditPanel').textContent=e.message}}
let adminUsersCache=[];let adminRoleFilter='all';
-function showAdminPanel(name){['adminDashboard','adminUsersPanel','adminCreditsPanel','adminRedeemPanel','adminAuditPanel','adminUserDetail'].forEach(id=>$(id).style.display='none');$(name).style.display='block';if(name==='adminUsersPanel')renderAdminUsers();if(name==='adminCreditsPanel')loadCreditLedger();if(name==='adminRedeemPanel')loadRedeemAdmin();if(name==='adminAuditPanel')loadAuditAdmin()}
-async function loadAdmin(){if(!window.isAdmin)return;try{const x=await api('/admin/users');adminUsersCache=x.data||[];showAdminPanel('adminDashboard')}catch(e){$('adminDashboard').innerHTML='
加载失败:'+esc(e.message)+'
'}}
+let currentAdminTab = 'users';
+function switchAdminTab(tab){
+ currentAdminTab = tab;
+ document.querySelectorAll('.admin-tab-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.adminTab === tab);
+ });
+ const panelMap = {
+ users: 'adminUsersPanel',
+ credits: 'adminCreditsPanel',
+ redeem: 'adminRedeemPanel',
+ audit: 'adminAuditPanel',
+ settings: 'adminSettingsPanel'
+ };
+ ['adminUsersPanel','adminCreditsPanel','adminRedeemPanel','adminAuditPanel','adminSettingsPanel'].forEach(id => {
+ const el = $(id);
+ if(el) el.style.display = 'none';
+ });
+ const activePanelId = panelMap[tab] || 'adminUsersPanel';
+ if($(activePanelId)) $(activePanelId).style.display = 'block';
+
+ if(tab === 'users') renderAdminUsers();
+ if(tab === 'credits') loadCreditLedger();
+ if(tab === 'redeem') loadRedeemAdmin();
+ if(tab === 'audit') loadAuditAdmin();
+ if(tab === 'settings') loadSettings();
+}
+
+function showAdminPanel(name){
+ if(name === 'adminUserDetail') return;
+ const map = {
+ adminDashboard: 'users',
+ adminUsersPanel: 'users',
+ adminCreditsPanel: 'credits',
+ adminRedeemPanel: 'redeem',
+ adminAuditPanel: 'audit',
+ adminSettingsPanel: 'settings'
+ };
+ switchAdminTab(map[name] || 'users');
+}
+
+async function loadAdmin(){
+ if(!window.isAdmin) return;
+ try {
+ const x = await api('/admin/users');
+ adminUsersCache = x.data || [];
+ switchAdminTab(currentAdminTab || 'users');
+ } catch(e) {
+ alert('加载管理信息失败:' + e.message);
+ }
+}
+
+function closeUserDrawer(){
+ const drawer = $('adminUserDetail');
+ const backdrop = $('drawerBackdrop');
+ if(drawer) drawer.classList.remove('active');
+ if(backdrop) backdrop.classList.remove('active');
+}
function renderAdminUsers(){const users=adminUsersCache.filter(u=>adminRoleFilter==='all'||u.role===adminRoleFilter);const groups=[['admin','管理员'],['privileged','高级用户'],['user','普通用户']];$('users').innerHTML=groups.filter(g=>adminRoleFilter==='all'||adminRoleFilter===g[0]).map(([role,label])=>{const list=users.filter(u=>u.role===role);return `
${label}
${list.length} 个账号
${list.length?list.map(u=>`
${esc(u.username)}
#${u.id} · ${esc(u.status)}
积分加载中…
`).join(''):'
暂无账号
'}
`}).join('');users.forEach(u=>loadUserCredit(u.id))}
async function loadUserCredit(id){try{const c=await api('/admin/users/'+id+'/credits');const e=$('credit-'+id);if(e)e.textContent='余额 '+c.balance+' · 可用 '+c.available+' · 下载 '+c.tasks}catch(e){}}
-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="
账户
角色
"+esc(u.role)+"
状态
"+esc(u.status)+"
加载积分中…
";showAdminPanel('adminUserDetail');$("detailReset").onclick=()=>resetUserPassword(id);$("detailToggle").onclick=()=>toggleUser(id,next);try{const c=await api('/admin/users/'+id+'/credits');$("detailCredits").innerHTML="
积分
"+c.balance+"
可用 "+c.available+" · 预留 "+c.reserved+" · 下载 "+c.tasks+" 次
";$("detailAdd").onclick=()=>adjustCredits(id,1);$("detailSub").onclick=()=>adjustCredits(id,-1)}catch(e){$("detailCredits").textContent=e.message}}
+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="
账户
角色
"+esc(u.role)+"
状态
"+esc(u.status)+"
加载积分中…
";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="
积分
"+c.balance+"
可用 "+c.available+" · 预留 "+c.reserved+" · 下载 "+c.tasks+" 次
";$("detailAdd").onclick=()=>adjustCredits(id,1);$("detailSub").onclick=()=>adjustCredits(id,-1)}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='
用户'+users.length+'
当前余额'+total+'
单位积分
';$('creditLedger').innerHTML='
积分流水按用户查看。进入「用户管理 → 查看详情」即可调整积分并查看该用户当前余额。
'}catch(e){$('adminCreditSummary').textContent=e.message}}
@@ -1210,11 +1383,23 @@ async function revokeBatch(id){try{await api('/admin/redeem/'+id+'/revoke',{meth
function hideLogin(){document.getElementById('loginOverlay').style.display='none'}
async function initAuth(){try{const x=await api('/auth/me');const u=x.user;window.currentUser=u;window.isAdmin=u.role==='admin';const pill=document.getElementById('userPill');if(pill)pill.textContent=u.username+' · '+u.role;const s=document.getElementById('settings');if(s)s.style.display=u.role==='admin'?'':'none';const an=document.getElementById('adminNav');if(an)an.style.display=u.role==='admin'?'':'none';const mn=document.getElementById('mobileAdminNav');if(mn)mn.style.display=u.role==='admin'?'':'none';const msn=document.getElementById('mobileSettingsNav');if(msn)msn.style.display=u.role==='admin'?'':'none';const ae=document.getElementById('accountAdminEntry');if(ae)ae.style.display=u.role==='admin'?'':'none';document.querySelectorAll('[data-view="settings"]').forEach(x=>x.style.display=u.role==='admin'?'':'none');const c=await api('/credits');if(pill)pill.textContent=u.username+' · '+u.role+' · 积分 '+c.balance+' / 可用 '+c.available;hideLogin();return true}catch(e){console.error('initAuth error:',e);showLogin();return false}}
-document.querySelectorAll('[data-admin-view]').forEach(b=>b.addEventListener('click',()=>showAdminPanel('admin'+b.dataset.adminView.charAt(0).toUpperCase()+b.dataset.adminView.slice(1)+'Panel')));document.querySelectorAll('.admin-back').forEach(b=>b.addEventListener('click',()=>showAdminPanel('adminDashboard')));document.querySelectorAll('[data-role-filter]').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('[data-role-filter]').forEach(x=>x.classList.remove('active'));b.classList.add('active');adminRoleFilter=b.dataset.roleFilter;renderAdminUsers()}));$('backToUsers').addEventListener('click',()=>showAdminPanel('adminUsersPanel'));
+document.querySelectorAll('[data-admin-view]').forEach(b=>b.addEventListener('click',()=>showAdminPanel('admin'+b.dataset.adminView.charAt(0).toUpperCase()+b.dataset.adminView.slice(1)+'Panel')));document.querySelectorAll('.admin-back').forEach(b=>b.addEventListener('click',()=>showAdminPanel('adminDashboard')));document.querySelectorAll('[data-role-filter]').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('[data-role-filter]').forEach(x=>x.classList.remove('active'));b.classList.add('active');adminRoleFilter=b.dataset.roleFilter;renderAdminUsers()}));
+document.querySelectorAll('.admin-tab-btn').forEach(btn => {
+ btn.addEventListener('click', () => switchAdminTab(btn.dataset.adminTab));
+});
+if($('drawerBackdrop')) $('drawerBackdrop').addEventListener('click', closeUserDrawer);
+if($('backToUsers')) $('backToUsers').addEventListener('click', closeUserDrawer);
+
$('loginForm').addEventListener('submit',async e=>{e.preventDefault();$('loginError').textContent='登录中…';try{const x=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:$('loginUser').value.trim(),password:$('loginPass').value})});if(!x.ok)throw new Error('用户名或密码错误');await initAuth();go(location.hash.slice(1)||'home');loadRecent();loadAccount()}catch(e){$('loginError').textContent=e.message}});
$('logoutBtn').addEventListener('click',async()=>{try{await api('/auth/logout',{method:'POST'})}finally{showLogin()}});
function esc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
-function go(view){if(view==='admin' && !window.isAdmin){view='home'}document.querySelectorAll('.view').forEach(x=>x.classList.toggle('active',x.id===view));document.querySelectorAll('[data-view]').forEach(x=>x.classList.toggle('active',x.dataset.view===view));if(view==='settings')loadSettings();if(view==='account')loadAccount();if(view==='admin')loadAdmin();if(view==='queue')loadTasks();if(view==='home')loadRecent();history.replaceState(null,'','#'+view)}
+function go(view){
+ if(view === 'settings') {
+ if(!window.isAdmin) { view = 'home'; }
+ else { view = 'admin'; switchAdminTab('settings'); }
+ } else if(view === 'admin' && !window.isAdmin){
+ view = 'home';
+ }document.querySelectorAll('.view').forEach(x=>x.classList.toggle('active',x.id===view));document.querySelectorAll('[data-view]').forEach(x=>x.classList.toggle('active',x.dataset.view===view));if(view==='settings')loadSettings();if(view==='account')loadAccount();if(view==='admin')loadAdmin();if(view==='queue')loadTasks();if(view==='home')loadRecent();history.replaceState(null,'','#'+view)}
document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>go(b.dataset.view)));document.querySelectorAll('[data-go]').forEach(b=>b.addEventListener('click',()=>go(b.dataset.go)));
function qualityRank(q){return {standard:1,exhigh:2,sq:3,lossless:4,hires:5,jyeffect:6,sky:7,jymaster:8,atmos:9,atmos_plus:10,master:11}[q]||0}
function qualityName(q){return {standard:'标准',exhigh:'高品质',sq:'SQ',lossless:'无损',hires:'Hi-Res',jyeffect:'高清音效',sky:'Sky',jymaster:'母带',atmos:'Atmos',atmos_plus:'Atmos+',master:'Master'}[q]||q}