初始化项目:基础设施 + 用户认证 + 后台管理系统 + AGPL 3.0 许可

This commit is contained in:
2026-05-26 13:46:33 +08:00
parent 1315df6501
commit 483fdd919f
56 changed files with 5804 additions and 40 deletions

View File

@ -0,0 +1,156 @@
/* =====================================================
MetaLab 管理面板 - 用户管理 JS
单一职责:用户列表渲染、搜索筛选、封禁/解封、强制下线
===================================================== */
let currentPage = 1;
const pageSize = 20;
document.addEventListener('DOMContentLoaded', () => {
loadUsers();
document.getElementById('searchBtn').addEventListener('click', () => {
currentPage = 1;
loadUsers();
});
document.getElementById('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { currentPage = 1; loadUsers(); }
});
document.getElementById('roleFilter').addEventListener('change', () => {
currentPage = 1; loadUsers();
});
document.getElementById('statusFilter').addEventListener('change', () => {
currentPage = 1; loadUsers();
});
});
async function loadUsers() {
const keyword = document.getElementById('searchInput').value.trim();
const role = document.getElementById('roleFilter').value;
const status = document.getElementById('statusFilter').value;
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize,
keyword, role, status
});
try {
const res = await api.get('/api/admin/users?' + params);
if (res.success) {
renderTable(res.data.users);
renderPagination(res.data.total, res.data.page);
} else {
showToast(res.message || '加载失败', 'error');
}
} catch (e) {
showToast('网络错误', 'error');
}
}
function renderTable(users) {
const tbody = document.getElementById('userTableBody');
if (!users || users.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;color:#636e72;">没有找到用户</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr>
<td class="col-uid">${u.uid}</td>
<td class="col-username">${escapeHtml(u.username)}</td>
<td class="col-email">${escapeHtml(u.email)}</td>
<td class="col-role">${roleLabel(u.role)}</td>
<td class="col-status">${statusLabel(u.status)}</td>
<td class="col-time">${formatTime(u.created_at)}</td>
<td class="col-actions">
<div class="action-group">
${renderActions(u)}
</div>
</td>
</tr>
`).join('');
}
function renderActions(u) {
// 不可操作自己
if (u.uid === window.__currentUid) return '—';
let btns = '';
const isOwner = window.__isOwner || false;
// 封禁/解封按钮
if (u.status === 'active') {
btns += `<button class="btn btn-danger btn-sm" onclick="banUser(${u.uid}, '${escapeHtml(u.username)}')">封禁</button>`;
} else if (u.status === 'banned') {
btns += `<button class="btn btn-outline btn-sm" onclick="unbanUser(${u.uid}, '${escapeHtml(u.username)}')">解封</button>`;
}
// 强制下线按钮
if (u.status === 'active' || u.status === 'banned') {
btns += `<button class="btn btn-outline btn-sm" onclick="resetToken(${u.uid}, '${escapeHtml(u.username)}')">踢下线</button>`;
}
// 删除按钮(仅 owner 可见,且目标不是 locked/deleted
if (isOwner && u.status !== 'locked' && u.status !== 'deleted') {
btns += `<button class="btn btn-danger btn-sm" onclick="deleteUser(${u.uid}, '${escapeHtml(u.username)}')">删除</button>`;
}
// 解锁按钮(仅 owner 可见,状态为 locked
if (isOwner && u.status === 'locked') {
btns += `<button class="btn btn-outline btn-sm" onclick="unlockUser(${u.uid}, '${escapeHtml(u.username)}')">解锁</button>`;
}
return btns || '—';
}
async function banUser(uid, username) {
const ok = await showConfirm('封禁用户', `确定要封禁用户「${username}」吗?封禁后该用户将无法登录。`);
if (!ok) return;
const res = await api.put(`/api/admin/users/${uid}/status`, { status: 'banned' });
if (res.success) { showToast(res.message, 'success'); loadUsers(); }
else { showToast(res.message || '操作失败', 'error'); }
}
async function unbanUser(uid, username) {
const ok = await showConfirm('解封用户', `确定要解封用户「${username}」吗?`);
if (!ok) return;
const res = await api.put(`/api/admin/users/${uid}/status`, { status: 'active' });
if (res.success) { showToast(res.message, 'success'); loadUsers(); }
else { showToast(res.message || '操作失败', 'error'); }
}
async function deleteUser(uid, username) {
const ok = await showConfirm('删除用户', `确定要永久删除用户「${username}」吗?此操作不可撤销,该账号将被立即锁定。`);
if (!ok) return;
const res = await api.put(`/api/admin/users/${uid}/status`, { status: 'locked' });
if (res.success) { showToast(res.message, 'success'); loadUsers(); }
else { showToast(res.message || '操作失败', 'error'); }
}
async function unlockUser(uid, username) {
const ok = await showConfirm('解锁用户', `确定要解锁用户「${username}」吗?该账号将恢复为正常状态。`);
if (!ok) return;
const res = await api.put(`/api/admin/users/${uid}/status`, { status: 'active' });
if (res.success) { showToast(res.message, 'success'); loadUsers(); }
else { showToast(res.message || '操作失败', 'error'); }
}
async function resetToken(uid, username) {
const ok = await showConfirm('强制下线', `确定要强制「${username}」下线吗?该用户需要重新登录。`);
if (!ok) return;
const res = await api.post(`/api/admin/users/${uid}/reset-token`);
if (res.success) { showToast(res.message, 'success'); }
else { showToast(res.message || '操作失败', 'error'); }
}
function renderPagination(total, page) {
const totalPages = Math.ceil(total / pageSize);
const bar = document.getElementById('paginationBar');
if (totalPages <= 1) { bar.innerHTML = ''; return; }
let html = `<button onclick="goPage(${page - 1})" ${page <= 1 ? 'disabled' : ''}>上一页</button>`;
html += `<span class="page-current">第 ${page} / ${totalPages} 页(共 ${total} 条)</span>`;
html += `<button onclick="goPage(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>下一页</button>`;
bar.innerHTML = html;
}
function goPage(p) {
currentPage = p;
loadUsers();
window.scrollTo({ top: 0 });
}