440 lines
19 KiB
HTML
440 lines
19 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/redeem-codes" 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" onchange="onGroupChange()"></select>
|
|
</div>
|
|
<div class="form-group" id="expiryGroup" style="display: none;">
|
|
<label>套餐到期时间</label>
|
|
<input type="datetime-local" id="editExpiresAt" class="ui-input">
|
|
<small style="color: var(--text-muted); font-size: 0.75rem;">游客和普通用户无需设置到期时间</small>
|
|
</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>
|
|
${u.group_id > 2 && u.expires_at ? `<br><small class="text-muted">${u.is_expired ? '已过期' : '至 ' + u.expires_at}</small>` : ''}
|
|
</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}, ${u.expires_at ? `'${u.expires_at}'` : null})">
|
|
编辑
|
|
</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, expiresAt) {
|
|
document.getElementById('editUserId').value = id;
|
|
document.getElementById('editGroupId').value = groupId;
|
|
document.getElementById('editIsActive').value = isActive;
|
|
|
|
// 设置到期时间
|
|
if (expiresAt) {
|
|
document.getElementById('editExpiresAt').value = expiresAt.slice(0, 16);
|
|
} else {
|
|
document.getElementById('editExpiresAt').value = '';
|
|
}
|
|
|
|
// 根据用户组显示/隐藏到期时间
|
|
onGroupChange();
|
|
document.getElementById('editModal').classList.add('show');
|
|
}
|
|
|
|
function onGroupChange() {
|
|
const groupId = parseInt(document.getElementById('editGroupId').value);
|
|
const expiryGroup = document.getElementById('expiryGroup');
|
|
// 游客(1)和普通用户(2)不显示到期时间
|
|
if (groupId > 2) {
|
|
expiryGroup.style.display = 'block';
|
|
} else {
|
|
expiryGroup.style.display = 'none';
|
|
document.getElementById('editExpiresAt').value = '';
|
|
}
|
|
}
|
|
|
|
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 groupId = parseInt(document.getElementById('editGroupId').value);
|
|
const expiresAt = document.getElementById('editExpiresAt').value;
|
|
|
|
const data = {
|
|
group_id: groupId,
|
|
is_active: document.getElementById('editIsActive').value === 'true'
|
|
};
|
|
|
|
// VIP/SVIP 需要设置到期时间
|
|
if (groupId > 2) {
|
|
data.expires_at = expiresAt || null;
|
|
}
|
|
|
|
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);
|
|
loadUserStats();
|
|
} 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> |