(() => { 'use strict'; const $ = id => document.getElementById(id); let snapshot = {users: []}; function node(tag, text) { const n = document.createElement(tag); n.textContent = text; return n; } async function api(url, options = {}) { const response = await fetch(url, { ...options, headers: {'Content-Type': 'application/json'}, cache: 'no-store' }); if (response.status === 401) { location.href = '/login'; throw new Error('请重新登录'); } const data = await response.json(); if (!response.ok) throw new Error(data.detail || '请求失败'); return data; } function reset() { HTMLFormElement.prototype.reset.call($('user-form')); $('user-id').value = ''; $('api-key').value = ''; } function renderUsers() { $('user-list').replaceChildren(); const selected = $('filter-user').value; $('filter-user').replaceChildren(new Option('全部用户', '')); snapshot.users.forEach(user => { $('filter-user').add(new Option(user.username, user.id)); const tr = document.createElement('tr'); [user.username, $('reveal').checked ? user.password : '••••••••', $('reveal').checked ? user.api_key : '••••••••'].forEach(v => tr.append(node('td', v))); const actions = document.createElement('td'); const edit = node('button', '编辑'); edit.onclick = () => { $('user-id').value = user.id; $('username').value = user.username; $('password').value = user.password; $('api-key').value = user.api_key; $('username').focus(); }; const remove = node('button', '删除'); remove.onclick = async () => { if (!confirm('删除用户“' + user.username + '”?统计也将删除,所有登录将失效。')) return; remove.disabled = true; try { await api('/api/admin/users/' + user.id, {method: 'DELETE'}); await refresh(); reset(); } catch(e) { $('status').textContent = e.message; } finally { remove.disabled = false; } }; actions.append(edit, remove); tr.append(actions); $('user-list').append(tr); }); $('filter-user').value = selected; } function bars(id, entries) { const el = $(id); el.replaceChildren(); const max = Math.max(1, ...entries.map(e => e[1])); entries.forEach(([label, value]) => { const row = node('div', ''); row.className = 'bar'; const track = node('div', ''); const bar = node('i', ''); bar.style.display = 'block'; bar.style.width = (value / max * 100) + '%'; track.append(bar); row.append(node('span', label), track, node('span', value)); el.append(row); }); if (!entries.length) el.append(node('p', '所选时间范围内暂无数据')); } function renderStats() { const start = $('start').value, end = $('end').value; if (start && end && start > end) { $('status').textContent = '开始日期不能晚于结束日期'; return; } const daily = {}, models = {}, totals = Array(6).fill(0); $('statistics').replaceChildren(); snapshot.users.filter(u => !$('filter-user').value || u.id === $('filter-user').value).forEach(user => { const values = Array(6).fill(0); Object.entries(user.stats).forEach(([day, metrics]) => { if ((start && day < start) || (end && day > end)) return; Object.entries(metrics).forEach(([metric, count]) => { const kind = metric.split(':')[0]; const index = ['generate','upload','tagger','augment','upscale','vibe_encode'].indexOf(kind); if (index >= 0) values[index] += count; if (kind === 'generate') { const model = metric.slice('generate:'.length); models[model] = (models[model] || 0) + count; daily[day] = (daily[day] || 0) + count; } }); }); values.forEach((v,i) => totals[i] += v); const row = node('tr',''); [user.username,...values].forEach(v => row.append(node('td',v))); $('statistics').append(row); }); $('totals').replaceChildren(); ['生成图片','云端上传','Tagger','修图','放大','Vibe 编码'].forEach((label,i) => { const card = node('div',label); card.className = 'card'; card.append(node('strong',totals[i])); $('totals').append(card); }); bars('daily', Object.entries(daily).sort((a,b) => a[0].localeCompare(b[0]))); bars('models', Object.entries(models).sort((a,b) => b[1]-a[1])); } async function refresh() { snapshot = await api('/api/admin/users'); renderUsers(); renderStats(); $('status').textContent = snapshot.sync_error || (snapshot.cloud_enabled ? '已刷新;云端同步每 60 秒合并执行。' : '尚未配置云存储,数据仅保存在本机。'); } $('user-form').onsubmit = async event => { event.preventDefault(); const button = event.submitter; button.disabled = true; const id = $('user-id').value; try { await api('/api/admin/users' + (id ? '/' + id : ''), {method: id ? 'PUT' : 'POST', body: JSON.stringify({username: $('username').value, password: $('password').value, api_key: $('api-key').value})}); await refresh(); reset(); } catch(e) { $('status').textContent = e.message; } finally { button.disabled = false; } }; $('reset-user-form').onclick = reset; $('reveal').onchange = () => { renderUsers(); ['password','api-key'].forEach(id => $(id).type = $('reveal').checked ? 'text' : 'password'); }; $('refresh').onclick = () => refresh().catch(e => $('status').textContent = e.message); ['filter-user','start','end'].forEach(id => $(id).onchange = renderStats); const end = new Date(), start = new Date(end); start.setUTCDate(start.getUTCDate()-29); $('start').value = start.toISOString().slice(0,10); $('end').value = end.toISOString().slice(0,10); refresh().then(reset).catch(e => $('status').textContent = e.message); })();