init
This commit is contained in:
400
templates/admin_users.html
Normal file
400
templates/admin_users.html
Normal file
@@ -0,0 +1,400 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user