client / static /js /admin.js
P01yH3dr0n
delete env nai api key
c544155
Raw History Blame Contribute Delete
6.26 kB
(() => {
'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);
})();