Files
JieXi/templates/admin_users.html
2025-11-28 21:20:40 +08:00

400 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - 管理后台</title>
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
<link rel="stylesheet" href="/static/css/admin.css">
</head>
<body class="admin-layout">
<header class="admin-header">
<div class="header-container">
<a href="/admin/dashboard" class="brand">
<span style="font-size: 1.5rem;"></span> JieXi Admin
</a>
<nav class="nav-links">
<a href="/admin/dashboard" class="nav-item">仪表板</a>
<a href="/admin/users" class="nav-item active">用户管理</a>
<a href="/admin/apis" class="nav-item">接口管理</a>
<a href="/admin/config" class="nav-item">系统配置</a>
<a href="/admin/logs" class="nav-item">日志审计</a>
</nav>
<div class="user-actions">
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
</div>
</div>
</header>
<main class="main-container">
<div class="page-header">
<h1 class="page-title">用户管理</h1>
<div class="actions">
<button class="ui-btn ui-btn-primary" onclick="showGroupsModal()">管理用户组</button>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">总用户数</div>
<div style="font-size: 2rem; font-weight: 700; color: #1f2937;" id="totalUsers">-</div>
</div>
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">普通用户</div>
<div style="font-size: 2rem; font-weight: 700; color: #6b7280;" id="normalUsers">-</div>
</div>
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">VIP用户</div>
<div style="font-size: 2rem; font-weight: 700; color: #f59e0b;" id="vipUsers">-</div>
</div>
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">SVIP用户</div>
<div style="font-size: 2rem; font-weight: 700; color: #8b5cf6;" id="svipUsers">-</div>
</div>
</div>
<!-- 筛选器 -->
<div style="margin-bottom: 1rem;">
<select id="groupFilter" class="ui-input" style="width: 200px;" onchange="loadUsers(1)">
<option value="">全部用户组</option>
<option value="2">普通用户</option>
<option value="3">VIP</option>
<option value="4">SVIP</option>
</select>
</div>
<div class="ui-card">
<div class="table-container">
<table id="usersTable">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>用户组</th>
<th>解析次数</th>
<th>状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8" style="text-align: center; color: var(--text-muted);">加载中...</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination mt-4 flex justify-between items-center" id="pagination">
<!-- Pagination controls will be injected here -->
</div>
</div>
</main>
<!-- Edit User Modal -->
<div id="editModal" class="ui-modal">
<div class="ui-modal-overlay" onclick="closeModal()"></div>
<div class="ui-modal-content">
<h3>编辑用户</h3>
<form id="editForm">
<input type="hidden" id="editUserId">
<div class="form-group">
<label>用户分组</label>
<select id="editGroupId" class="ui-input"></select>
</div>
<div class="form-group">
<label>账号状态</label>
<select id="editIsActive" class="ui-input">
<option value="true">激活</option>
<option value="false">禁用</option>
</select>
</div>
<div class="ui-modal-actions">
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeModal()">取消</button>
<button type="submit" class="ui-btn ui-btn-primary">保存更改</button>
</div>
</form>
</div>
</div>
<!-- Groups Management Modal -->
<div id="groupsModal" class="ui-modal">
<div class="ui-modal-overlay" onclick="closeGroupsModal()"></div>
<div class="ui-modal-content" style="max-width: 700px;">
<h3>用户组管理</h3>
<div class="table-container" style="margin-top: 1rem;">
<table>
<thead>
<tr>
<th>分组名称</th>
<th>每日解析次数</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody id="groupsTableBody">
<tr><td colspan="4" style="text-align: center;">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="ui-modal-actions" style="margin-top: 1.5rem;">
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeGroupsModal()">关闭</button>
</div>
</div>
</div>
<!-- Edit Group Modal -->
<div id="editGroupModal" class="ui-modal">
<div class="ui-modal-overlay" onclick="closeEditGroupModal()"></div>
<div class="ui-modal-content">
<h3>编辑用户组</h3>
<form id="editGroupForm">
<input type="hidden" id="editGroupIdInput">
<div class="form-group">
<label>分组名称</label>
<input type="text" id="editGroupName" class="ui-input" readonly>
</div>
<div class="form-group">
<label>每日解析次数</label>
<input type="number" id="editGroupLimit" class="ui-input" min="0" required>
</div>
<div class="form-group">
<label>描述</label>
<textarea id="editGroupDesc" class="ui-input" rows="3"></textarea>
</div>
<div class="ui-modal-actions">
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeEditGroupModal()">取消</button>
<button type="submit" class="ui-btn ui-btn-primary">保存更改</button>
</div>
</form>
</div>
</div>
<script src="/static/js/ui-components.js"></script>
<script>
let currentPage = 1;
let groups = [];
async function loadGroups() {
try {
const response = await fetch('/admin/api/groups');
const result = await response.json();
if (result.success) {
groups = result.data;
const select = document.getElementById('editGroupId');
select.innerHTML = groups.map(g => `<option value="${g.id}">${g.name} (${g.daily_limit}次/天)</option>`).join('');
}
} catch (e) {
console.error('Failed to load groups', e);
}
}
async function loadUserStats() {
try {
const response = await fetch('/admin/api/users?page=1&per_page=1000');
const result = await response.json();
if (result.success) {
const users = result.data;
const total = users.length;
const normal = users.filter(u => u.group_id === 2).length;
const vip = users.filter(u => u.group_id === 3).length;
const svip = users.filter(u => u.group_id === 4).length;
document.getElementById('totalUsers').textContent = total;
document.getElementById('normalUsers').textContent = normal;
document.getElementById('vipUsers').textContent = vip;
document.getElementById('svipUsers').textContent = svip;
}
} catch (e) {
console.error('Failed to load user stats', e);
}
}
async function loadUsers(page = 1) {
try {
const groupFilter = document.getElementById('groupFilter').value;
let url = `/admin/api/users?page=${page}&per_page=20`;
if (groupFilter) {
url += `&group_id=${groupFilter}`;
}
const response = await fetch(url);
const result = await response.json();
if (result.success) {
const tbody = document.querySelector('#usersTable tbody');
tbody.innerHTML = result.data.map(u => `
<tr>
<td>#${u.id}</td>
<td><span class="font-medium">${u.username}</span></td>
<td class="text-muted">${u.email}</td>
<td><span class="badge badge-info">${u.group_name}</span></td>
<td>${u.total_parse_count}</td>
<td>
<span class="badge ${u.is_active ? 'badge-success' : 'badge-error'}">
${u.is_active ? '正常' : '禁用'}
</span>
</td>
<td class="text-muted text-sm">${new Date(u.created_at).toLocaleString('zh-CN')}</td>
<td>
<button class="ui-btn ui-btn-secondary ui-btn-sm"
onclick="editUser(${u.id}, ${u.group_id}, ${u.is_active})">
编辑
</button>
</td>
</tr>
`).join('');
const pagination = document.getElementById('pagination');
pagination.innerHTML = `
<button class="ui-btn ui-btn-secondary ui-btn-sm"
${page === 1 ? 'disabled' : ''}
onclick="loadUsers(${page - 1})">上一页</button>
<span class="text-sm text-muted">第 ${page} / ${result.pagination.pages} 页</span>
<button class="ui-btn ui-btn-secondary ui-btn-sm"
${page === result.pagination.pages ? 'disabled' : ''}
onclick="loadUsers(${page + 1})">下一页</button>
`;
currentPage = page;
}
} catch (error) {
UI.notify('加载用户列表失败', 'error');
}
}
function editUser(id, groupId, isActive) {
document.getElementById('editUserId').value = id;
document.getElementById('editGroupId').value = groupId;
document.getElementById('editIsActive').value = isActive;
document.getElementById('editModal').classList.add('show');
}
function closeModal() {
document.getElementById('editModal').classList.remove('show');
}
async function showGroupsModal() {
document.getElementById('groupsModal').classList.add('show');
await loadGroupsTable();
}
function closeGroupsModal() {
document.getElementById('groupsModal').classList.remove('show');
}
async function loadGroupsTable() {
try {
const response = await fetch('/admin/api/groups');
const result = await response.json();
if (result.success) {
const tbody = document.getElementById('groupsTableBody');
tbody.innerHTML = result.data.map(g => `
<tr>
<td><span class="font-medium">${g.name}</span></td>
<td><span class="badge badge-info">${g.daily_limit}次/天</span></td>
<td class="text-sm text-muted">${g.description || '-'}</td>
<td>
<button class="ui-btn ui-btn-secondary ui-btn-sm"
onclick='editGroup(${JSON.stringify(g)})'>
编辑
</button>
</td>
</tr>
`).join('');
}
} catch (error) {
UI.notify('加载用户组失败', 'error');
}
}
function editGroup(group) {
document.getElementById('editGroupIdInput').value = group.id;
document.getElementById('editGroupName').value = group.name;
document.getElementById('editGroupLimit').value = group.daily_limit;
document.getElementById('editGroupDesc').value = group.description || '';
document.getElementById('editGroupModal').classList.add('show');
}
function closeEditGroupModal() {
document.getElementById('editGroupModal').classList.remove('show');
}
async function logout() {
try {
await fetch('/admin/logout', { method: 'POST' });
window.location.href = '/admin/login';
} catch (error) {
UI.notify('退出失败', 'error');
}
}
document.getElementById('editForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('editUserId').value;
const data = {
group_id: parseInt(document.getElementById('editGroupId').value),
is_active: document.getElementById('editIsActive').value === 'true'
};
try {
const response = await fetch(`/admin/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
UI.notify('更新成功', 'success');
closeModal();
loadUsers(currentPage);
} else {
UI.notify(result.message || '更新失败', 'error');
}
} catch (error) {
UI.notify('网络错误', 'error');
}
});
document.getElementById('editGroupForm').addEventListener('submit', async (e) => {
e.preventDefault();
const groupId = document.getElementById('editGroupIdInput').value;
const data = {
daily_limit: parseInt(document.getElementById('editGroupLimit').value),
description: document.getElementById('editGroupDesc').value
};
try {
const response = await fetch(`/admin/api/groups/${groupId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
UI.notify('用户组更新成功', 'success');
closeEditGroupModal();
await loadGroupsTable();
await loadGroups(); // 刷新用户编辑表单中的分组列表
} else {
UI.notify(result.message || '更新失败', 'error');
}
} catch (error) {
UI.notify('网络错误', 'error');
}
});
loadGroups();
loadUsers();
loadUserStats();
</script>
</body>
</html>