128 lines
6.5 KiB
HTML
128 lines
6.5 KiB
HTML
{% extends "admin_base.html" %}
|
|
|
|
{% block title %}用户管理 - 智联青云管理后台{% endblock %}
|
|
|
|
{% block admin_content %}
|
|
<div class="space-y-6">
|
|
<h1 class="text-2xl font-bold text-slate-900">用户管理</h1>
|
|
|
|
<div class="flex gap-4">
|
|
<input id="search-input" type="text" placeholder="搜索用户名/邮箱..." class="flex-1 px-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<select id="role-filter" class="px-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<option value="">全部角色</option>
|
|
<option value="admin">管理员</option>
|
|
<option value="teacher">教师</option>
|
|
<option value="student">学生</option>
|
|
</select>
|
|
<button onclick="loadUsers()" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">搜索</button>
|
|
</div>
|
|
|
|
<div class="bg-white shadow-sm rounded-lg border border-slate-200 overflow-hidden">
|
|
<table class="min-w-full divide-y divide-slate-200">
|
|
<thead class="bg-slate-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">用户名</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">邮箱</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">角色</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">注册时间</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="users-tbody" class="bg-white divide-y divide-slate-200"></tbody>
|
|
</table>
|
|
<div id="loading-spinner" class="text-center py-8 text-slate-400 hidden">
|
|
<svg class="animate-spin h-6 w-6 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span class="text-sm">加载中...</span>
|
|
</div>
|
|
<div id="empty-msg" class="text-center py-12 text-slate-400 hidden">暂无用户</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
const roleMap = {
|
|
'admin': ['管理员', 'bg-red-100 text-red-800'],
|
|
'teacher': ['教师', 'bg-blue-100 text-blue-800'],
|
|
'student': ['学生', 'bg-green-100 text-green-800']
|
|
};
|
|
|
|
async function loadUsers() {
|
|
const q = document.getElementById('search-input').value;
|
|
const role = document.getElementById('role-filter').value;
|
|
const tbody = document.getElementById('users-tbody');
|
|
const spinner = document.getElementById('loading-spinner');
|
|
const empty = document.getElementById('empty-msg');
|
|
|
|
tbody.innerHTML = '';
|
|
spinner.classList.remove('hidden');
|
|
empty.classList.add('hidden');
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (q) params.set('q', q);
|
|
if (role) params.set('role', role);
|
|
const res = await fetch('/api/admin/users?' + params);
|
|
const data = await res.json();
|
|
spinner.classList.add('hidden');
|
|
|
|
if (!data.success || !data.users.length) {
|
|
empty.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
data.users.forEach(u => {
|
|
const [roleText, roleClass] = roleMap[u.role] || ['未知', 'bg-slate-100 text-slate-800'];
|
|
const banned = u.is_banned;
|
|
html += `<tr>
|
|
<td class="px-6 py-4 text-sm text-slate-900">${u.id}</td>
|
|
<td class="px-6 py-4 text-sm text-slate-900">${u.name}</td>
|
|
<td class="px-6 py-4 text-sm text-slate-500">${u.email || '-'}</td>
|
|
<td class="px-6 py-4">
|
|
<span class="px-2 py-1 text-xs rounded-full ${roleClass}">${roleText}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="px-2 py-1 text-xs rounded-full ${banned ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}">${banned ? '已封禁' : '正常'}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate-500">${u.created_at}</td>
|
|
<td class="px-6 py-4 space-x-2">
|
|
<button onclick="toggleBan(${u.id}, ${banned})" class="px-2 py-1 text-xs ${banned ? 'bg-green-100 text-green-700 border-green-300' : 'bg-yellow-100 text-yellow-700 border-yellow-300'} border rounded hover:opacity-80">${banned ? '解封' : '封禁'}</button>
|
|
<button onclick="deleteUser(${u.id}, '${u.name.replace(/'/g, "\\'")}')" class="px-2 py-1 text-xs bg-red-100 text-red-700 border border-red-300 rounded hover:bg-red-200">删除</button>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
} catch(e) {
|
|
spinner.classList.add('hidden');
|
|
empty.textContent = '加载失败,请稍后重试';
|
|
empty.classList.remove('hidden');
|
|
}
|
|
}
|
|
async function toggleBan(id, currentlyBanned) {
|
|
if (!confirm(currentlyBanned ? '确定解封该用户?' : '确定封禁该用户?')) return;
|
|
try {
|
|
const res = await fetch(`/api/admin/users/${id}/ban`, {method: 'PUT'});
|
|
const data = await res.json();
|
|
if (data.success) { loadUsers(); } else { alert(data.message || '操作失败'); }
|
|
} catch(e) { alert('网络错误'); }
|
|
}
|
|
|
|
async function deleteUser(id, name) {
|
|
if (!confirm(`确定删除用户「${name}」?此操作不可恢复!`)) return;
|
|
try {
|
|
const res = await fetch(`/api/admin/users/${id}`, {method: 'DELETE'});
|
|
const data = await res.json();
|
|
if (data.success) { loadUsers(); } else { alert(data.message || '操作失败'); }
|
|
} catch(e) { alert('网络错误'); }
|
|
}
|
|
|
|
document.getElementById('search-input').addEventListener('keydown', e => { if (e.key === 'Enter') loadUsers(); });
|
|
document.addEventListener('DOMContentLoaded', loadUsers);
|
|
</script>
|
|
{% endblock %}
|