feat: refactor admin with segmented tabs, drawer details, and unified settings
This commit is contained in:
+311
-126
@@ -789,6 +789,110 @@ select.input {
|
||||
.row { flex-wrap: wrap; }
|
||||
.row > * { flex-basis: 100%; }
|
||||
}
|
||||
|
||||
/* Admin Segmented Tabs & Modern Header */
|
||||
.admin-tab-nav {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
background: rgba(15, 22, 36, 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 5px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-subtle);
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.admin-tab-nav button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: 13.5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.admin-tab-nav button:hover {
|
||||
color: var(--text-main);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.admin-tab-nav button.active {
|
||||
background: var(--bg-surface-elevated);
|
||||
color: #fff;
|
||||
border: 1px solid var(--border-subtle);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* User Drawer Overlay & Panel */
|
||||
.drawer-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 8, 15, 0.65);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 200;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.drawer-backdrop.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.admin-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(520px, 92vw);
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
box-shadow: -10px 0 35px rgba(0, 0, 0, 0.5);
|
||||
z-index: 201;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.admin-drawer.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.drawer-header {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(15, 22, 36, 0.95);
|
||||
}
|
||||
.drawer-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.admin-drawer {
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
max-height: 85vh;
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
border-radius: 20px 20px 0 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
.admin-drawer.active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -960,41 +1064,44 @@ select.input {
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<div class="eyebrow">Admin Center</div>
|
||||
<h2>管理中心</h2>
|
||||
<p>系统用户、积分账本、卡密批次与全局审计监控。</p>
|
||||
<h2>管理控制台</h2>
|
||||
<p>系统多用户鉴权、积分流水记账、卡密批次、操作审计与全局纳管配置。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-dashboard" id="adminDashboard">
|
||||
<button class="admin-tile" data-admin-view="users">
|
||||
<span>👥</span>
|
||||
<strong>用户管理</strong>
|
||||
<small>账号、角色、状态与积分详情</small>
|
||||
<!-- 分段 Tab 导航 (Segmented Tabs) -->
|
||||
<div class="admin-tab-nav" id="adminTabNav">
|
||||
<button class="admin-tab-btn active" data-admin-tab="users">
|
||||
<span>👥</span> 用户管理
|
||||
</button>
|
||||
<button class="admin-tile" data-admin-view="credits">
|
||||
<span>◎</span>
|
||||
<strong>积分管理</strong>
|
||||
<small>系统余额总览与额度流水</small>
|
||||
<button class="admin-tab-btn" data-admin-tab="credits">
|
||||
<span>◎</span> 积分账本
|
||||
</button>
|
||||
<button class="admin-tile" data-admin-view="redeem">
|
||||
<span>◇</span>
|
||||
<strong>兑换码</strong>
|
||||
<small>批次生成、使用情况与批量撤销</small>
|
||||
<button class="admin-tab-btn" data-admin-tab="redeem">
|
||||
<span>◇</span> 卡密批次
|
||||
</button>
|
||||
<button class="admin-tile" data-admin-view="audit">
|
||||
<span>≡</span>
|
||||
<strong>审计日志</strong>
|
||||
<small>管理员关键操作轨迹</small>
|
||||
<button class="admin-tab-btn" data-admin-tab="audit">
|
||||
<span>≡</span> 审计日志
|
||||
</button>
|
||||
<button class="admin-tab-btn" data-admin-tab="settings">
|
||||
<span>⚙️</span> 纳管设置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理子面板 -->
|
||||
<div class="admin-panel" id="adminUsersPanel" style="display:none">
|
||||
<!-- 保持原有 ID 避免破坏已有绑定契约 -->
|
||||
<div id="adminDashboard" style="display:none">
|
||||
<button class="admin-tile" data-admin-view="users" style="display:none"></button>
|
||||
<button class="admin-tile" data-admin-view="credits" style="display:none"></button>
|
||||
<button class="admin-tile" data-admin-view="redeem" style="display:none"></button>
|
||||
<button class="admin-tile" data-admin-view="audit" style="display:none"></button>
|
||||
</div>
|
||||
|
||||
<!-- 1. 用户管理面板 -->
|
||||
<div class="admin-panel" id="adminUsersPanel">
|
||||
<div class="panel-head">
|
||||
<button class="btn alt admin-back">← 返回中心</button>
|
||||
<div>
|
||||
<div class="title">用户管理</div>
|
||||
<div class="muted">按用户组分类查看与授权调度。</div>
|
||||
<div class="title">用户授权与角色列表</div>
|
||||
<div class="muted">支持快速注册、角色分组分级管理、积分划拨与账户锁定。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row admin-create" style="margin-bottom:14px">
|
||||
@@ -1017,13 +1124,12 @@ select.input {
|
||||
<div id="users"></div>
|
||||
</div>
|
||||
|
||||
<!-- 积分管理子面板 -->
|
||||
<!-- 2. 积分管理面板 -->
|
||||
<div class="admin-panel" id="adminCreditsPanel" style="display:none">
|
||||
<div class="panel-head">
|
||||
<button class="btn alt admin-back">← 返回中心</button>
|
||||
<div>
|
||||
<div class="title">积分管理</div>
|
||||
<div class="muted">集中查看积分流水与系统账户总额。</div>
|
||||
<div class="title">系统全局积分概览</div>
|
||||
<div class="muted">集中查看用户额度储备与不可变流水。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice">积分充值、下载扣除、卡密兑换及失败退款均进入不可变流水记账。</div>
|
||||
@@ -1031,13 +1137,12 @@ select.input {
|
||||
<div id="creditLedger" class="ledger"></div>
|
||||
</div>
|
||||
|
||||
<!-- 兑换码子面板 -->
|
||||
<!-- 3. 兑换码面板 -->
|
||||
<div class="admin-panel" id="adminRedeemPanel" style="display:none">
|
||||
<div class="panel-head">
|
||||
<button class="btn alt admin-back">← 返回中心</button>
|
||||
<div>
|
||||
<div class="title">兑换码管理</div>
|
||||
<div class="muted">批量生成积分卡密并追踪兑换状态。</div>
|
||||
<div class="title">兑换码批次与发行</div>
|
||||
<div class="muted">批量生成积分卡密并支持随时吊销未消费批次。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:14px">
|
||||
@@ -1050,113 +1155,123 @@ select.input {
|
||||
<div id="batches"></div>
|
||||
</div>
|
||||
|
||||
<!-- 审计日志子面板 -->
|
||||
<!-- 4. 审计日志面板 -->
|
||||
<div class="admin-panel" id="adminAuditPanel" style="display:none">
|
||||
<div class="panel-head">
|
||||
<button class="btn alt admin-back">← 返回中心</button>
|
||||
<div>
|
||||
<div class="title">安全审计日志</div>
|
||||
<div class="muted">系统记录管理员敏感操作历史。</div>
|
||||
<div class="title">系统安全审计</div>
|
||||
<div class="muted">记录管理员关键操作轨迹及敏感变更。</div>
|
||||
</div>
|
||||
<button id="loadAudit" class="btn alt">刷新</button>
|
||||
<button id="loadAudit" class="btn alt">刷新审计</button>
|
||||
</div>
|
||||
<div id="audit" class="ledger"></div>
|
||||
</div>
|
||||
|
||||
<!-- 用户详情子面板 -->
|
||||
<div class="admin-panel" id="adminUserDetail" style="display:none">
|
||||
<!-- 5. 纳管系统设置面板(内嵌在管理后台) -->
|
||||
<div class="admin-panel" id="adminSettingsPanel" style="display:none">
|
||||
<div class="panel-head">
|
||||
<button class="btn alt" id="backToUsers">← 返回列表</button>
|
||||
<div>
|
||||
<div class="title" id="detailUserName">用户详情</div>
|
||||
<div class="muted" id="detailUserMeta"></div>
|
||||
<div class="title">系统运行与纳管配置</div>
|
||||
<div class="muted">调整上游解析微服务、全局转存策略与系统级存储。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="card setting-card" style="background:rgba(11,18,30,0.6)">
|
||||
<h3>音乐解析 API</h3>
|
||||
<p>用于上游音乐搜索和音源解析(独立微服务)。</p>
|
||||
<div class="field">
|
||||
<label>API 接口地址</label>
|
||||
<input id="amApi" class="input" placeholder="http://127.0.0.1:9530">
|
||||
</div>
|
||||
<div class="notice">架构说明:Downloader Web 运行于端口 9540,解析 API 默认运行于端口 9530。</div>
|
||||
</div>
|
||||
|
||||
<div class="card setting-card" style="background:rgba(11,18,30,0.6)">
|
||||
<h3>下载存储策略</h3>
|
||||
<p>文件保存路径、音质默认偏好与转存策略。</p>
|
||||
<div class="field">
|
||||
<label>本地下载目录</label>
|
||||
<input id="downloadDir" class="input">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>本地存储策略</label>
|
||||
<select id="localFilePolicy" class="input">
|
||||
<option value="keep">保留本地文件</option>
|
||||
<option value="delete_after_upload">WebDAV 上传成功后自动清除本地副本</option>
|
||||
</select>
|
||||
<div class="notice">仅在 WebDAV 校验上传完整后安全删除;未开启 WebDAV 或上传失败绝不删除。</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>默认下载音质</label>
|
||||
<select id="defaultQuality" class="input">
|
||||
<option value="standard">标准 (128kbps)</option>
|
||||
<option value="exhigh">高品质 (320kbps)</option>
|
||||
<option value="lossless">无损 FLAC</option>
|
||||
<option value="hires">Hi-Res 高解析</option>
|
||||
<option value="sky">Sky 空间音频</option>
|
||||
<option value="jyeffect">高清音效</option>
|
||||
<option value="jymaster">母带级</option>
|
||||
<option value="atmos">杜比全景声 Atmos</option>
|
||||
<option value="atmos_plus">Atmos+ 进阶</option>
|
||||
<option value="master">Master 录音室</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card setting-card" style="grid-column: 1 / -1; background:rgba(11,18,30,0.6)">
|
||||
<h3>系统全局 WebDAV</h3>
|
||||
<p>全局云盘存储同步配置(未配置私有 WebDAV 时兜底使用)。</p>
|
||||
<div class="field">
|
||||
<label>WebDAV 地址</label>
|
||||
<input id="davUrl" class="input" placeholder="https://nas.example.com/dav/music">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field" style="margin-top:0">
|
||||
<label>用户名</label>
|
||||
<input id="davUser" class="input">
|
||||
</div>
|
||||
<div class="field" style="margin-top:0">
|
||||
<label>密码</label>
|
||||
<input id="davPass" type="password" class="input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>远端音乐目录</label>
|
||||
<input id="davPath" class="input" placeholder="Music">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>同曲不同音质策略</label>
|
||||
<select id="davVersionPolicy" class="input">
|
||||
<option value="keep">保留全部版本</option>
|
||||
<option value="replace_lower">高音质下载完成后清理旧的低音质</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-top:16px">
|
||||
<button id="testDav" type="button" class="btn alt">测试连接</button>
|
||||
<button id="saveSettings" type="button" class="btn">保存系统配置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="userDetailContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 设置视图 -->
|
||||
<section id="settings" class="view">
|
||||
<div class="settings-grid">
|
||||
<div class="card setting-card">
|
||||
<h3>音乐解析 API</h3>
|
||||
<p>用于上游音乐搜索和音源解析(独立微服务)。</p>
|
||||
<div class="field">
|
||||
<label>API 接口地址</label>
|
||||
<input id="amApi" class="input" placeholder="http://127.0.0.1:9530">
|
||||
</div>
|
||||
<div class="notice">架构说明:Downloader Web 运行于端口 9540,解析 API 默认运行于端口 9530。</div>
|
||||
</div>
|
||||
|
||||
<div class="card setting-card">
|
||||
<h3>下载设置</h3>
|
||||
<p>文件保存路径、音质默认偏好与转存策略。</p>
|
||||
<div class="field">
|
||||
<label>本地下载目录</label>
|
||||
<input id="downloadDir" class="input">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>本地存储策略</label>
|
||||
<select id="localFilePolicy" class="input">
|
||||
<option value="keep">保留本地文件</option>
|
||||
<option value="delete_after_upload">WebDAV 上传成功后自动清除本地副本</option>
|
||||
</select>
|
||||
<div class="notice">仅在 WebDAV 校验上传完整后安全删除;未开启 WebDAV 或上传失败绝不删除。</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>默认下载音质</label>
|
||||
<select id="defaultQuality" class="input">
|
||||
<option value="standard">标准 (128kbps)</option>
|
||||
<option value="exhigh">高品质 (320kbps)</option>
|
||||
<option value="lossless">无损 FLAC</option>
|
||||
<option value="hires">Hi-Res 高解析</option>
|
||||
<option value="sky">Sky 空间音频</option>
|
||||
<option value="jyeffect">高清音效</option>
|
||||
<option value="jymaster">母带级</option>
|
||||
<option value="atmos">杜比全景声 Atmos</option>
|
||||
<option value="atmos_plus">Atmos+ 进阶</option>
|
||||
<option value="master">Master 录音室</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card setting-card">
|
||||
<h3>系统全局 WebDAV</h3>
|
||||
<p>全局云盘存储同步配置(未配置私有 WebDAV 时兜底使用)。</p>
|
||||
<div class="field">
|
||||
<label>WebDAV 地址</label>
|
||||
<input id="davUrl" class="input" placeholder="https://nas.example.com/dav/music">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field" style="margin-top:0">
|
||||
<label>用户名</label>
|
||||
<input id="davUser" class="input">
|
||||
</div>
|
||||
<div class="field" style="margin-top:0">
|
||||
<label>密码</label>
|
||||
<input id="davPass" type="password" class="input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>远端音乐目录</label>
|
||||
<input id="davPath" class="input" placeholder="Music">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>同曲不同音质策略</label>
|
||||
<select id="davVersionPolicy" class="input">
|
||||
<option value="keep">保留全部版本</option>
|
||||
<option value="replace_lower">高音质下载完成后清理旧的低音质</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-top:16px">
|
||||
<button id="testDav" type="button" class="btn alt">测试连接</button>
|
||||
<button id="saveSettings" type="button" class="btn">保存系统配置</button>
|
||||
</div>
|
||||
<!-- 用户管理:右侧/底部抽屉详情(Drawer) -->
|
||||
<div class="drawer-backdrop" id="drawerBackdrop"></div>
|
||||
<div class="admin-drawer" id="adminUserDetail">
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<div class="title" id="detailUserName">用户详情</div>
|
||||
<div class="muted" id="detailUserMeta"></div>
|
||||
</div>
|
||||
<button class="btn alt" id="backToUsers" style="padding:6px 12px;font-size:12px">✕ 关闭</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="drawer-body">
|
||||
<div id="userDetailContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<!-- 移动端底栏导航 -->
|
||||
@@ -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='<div class="notice">加载失败:'+esc(e.message)+'</div>'}}
|
||||
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 `<div class="user-group"><div class="group-head"><div><div class="title">${label}</div><div class="muted">${list.length} 个账号</div></div></div><div class="group-list">${list.length?list.map(u=>`<div class="user-card"><div class="user-main"><div><div class="name">${esc(u.username)}</div><div class="meta">#${u.id} · ${esc(u.status)}</div><div class="user-stats"><span class="stat-chip" id="credit-${u.id}">积分加载中…</span></div></div><div class="user-actions"><button class="btn alt" onclick="openUserDetail(${u.id})">查看详情</button><button class="btn alt" onclick="toggleUser(${u.id},'${u.status==='active'?'disabled':'active'}')">${u.status==='active'?'禁用':'启用'}</button></div></div></div>`).join(''):'<div class="empty">暂无账号</div>'}</div></div>`}).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="<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>";showAdminPanel('adminUserDetail');$("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}}
|
||||
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}}
|
||||
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}}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user