657 lines
21 KiB
HTML
657 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>用户管理</title>
|
||
<style>
|
||
body {margin: 0;font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;background: #fafafa;}
|
||
/* 导航栏样式 */
|
||
.sidebar {position: fixed;top: 0;left: 0;width: 180px;height: 100vh;background: #1e1e2e;color: white;padding: 20px;box-shadow: 2px 0 5px rgba(0,0,0,0.1);z-index: 1000;display: flex;
|
||
flex-direction: column;align-items: center;}
|
||
.user-id {text-align: center;margin-bottom: 0px;}
|
||
.sidebar h3 {margin-top: 0;font-size: 18px;color: #add8e6;text-align: center; margin-bottom: 20px;}
|
||
.navigation-links {width: 100%;margin-top: 60px;}
|
||
.sidebar a,
|
||
.sidebar button {display: block;color: #8be9fd;text-decoration: none;margin: 10px 0;font-size: 16px;padding: 15px;border-radius: 4px;background: transparent;
|
||
border: none;cursor: pointer; width: calc(100% - 40px);text-align: left;transition: all 0.2s ease;}
|
||
.sidebar a:hover,
|
||
.sidebar button:hover {color: #ff79c6;background-color: rgba(139, 233, 253, 0.2);}
|
||
/* 主内容区 */
|
||
.main-content {
|
||
margin-left: 200px;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.card {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
box-shadow: 0 10px 24px rgba(31,35,40,0.08);
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #4f46e5;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #ef4444;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #22c55e;
|
||
color: #fff;
|
||
}
|
||
|
||
.table-container {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
th, td {
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
th {
|
||
background-color: #f9fafb;
|
||
font-weight: bold;
|
||
color: #374151;
|
||
}
|
||
|
||
tr:hover {
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.form-group input, .form-group select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-row .form-group {
|
||
flex: 1;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.search-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.search-container input {
|
||
flex: 1;
|
||
padding: 8px 12px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.search-container button {
|
||
padding: 8px 15px;
|
||
background: #4f46e5;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 2000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
margin: 10% auto;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
width: 80%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.close {
|
||
color: #aaa;
|
||
float: right;
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.close:hover {
|
||
color: black;
|
||
}
|
||
|
||
.notification {
|
||
padding: 10px;
|
||
margin-bottom: 15px;
|
||
border-radius: 4px;
|
||
display: none;
|
||
}
|
||
|
||
.notification.success {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.notification.error {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.nav-error {
|
||
color: #ef4444;
|
||
font-size: 12px;
|
||
margin-top: 5px;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 左侧固定栏目 -->
|
||
<div class="sidebar">
|
||
<div class="user-id">
|
||
<h3>你好,{{ username|default:"访客" }}</h3>
|
||
</div>
|
||
<div class="navigation-links">
|
||
<a href="{% url 'main:home' %}" onclick="return handleNavClick(this, '/');">返回主页</a>
|
||
<a id="logoutBtn">退出登录</a>
|
||
<div id="logoutMsg"></div>
|
||
{% csrf_token %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
{% if is_student %}
|
||
<div class="card">
|
||
<div class="header"><h2>修改密码</h2></div>
|
||
<form id="selfPwdForm">
|
||
<input type="hidden" id="selfUserId" name="user_id" value="{{ user_id }}">
|
||
<div class="form-group">
|
||
<label for="password">新密码</label>
|
||
<input type="password" id="password" name="password" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="confirmPassword">确认密码</label>
|
||
<input type="password" id="confirmPassword" name="confirmPassword" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
</form>
|
||
</div>
|
||
{% else %}
|
||
{% if is_tutor %}
|
||
<div class="card">
|
||
<div class="header"><h2>修改本人密码</h2></div>
|
||
<form id="selfPwdForm">
|
||
<input type="hidden" id="selfUserId" name="user_id" value="{{ user_id }}">
|
||
<div class="form-group">
|
||
<label for="password">新密码</label>
|
||
<input type="password" id="password" name="password" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="confirmPassword">确认密码</label>
|
||
<input type="password" id="confirmPassword" name="confirmPassword" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card">
|
||
<div class="header">
|
||
<h2>用户管理</h2>
|
||
{% if is_admin %}<button id="addUserBtn" class="btn btn-primary">添加用户</button>{% endif %}
|
||
</div>
|
||
|
||
<div class="notification success" id="successNotification">操作成功!</div>
|
||
<div class="notification error" id="errorNotification">操作失败!</div>
|
||
|
||
<div class="search-container">
|
||
<input type="text" id="searchInput" placeholder="搜索用户名...">
|
||
<button id="searchBtn">搜索</button>
|
||
<button id="resetBtn">重置</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table id="usersTable">
|
||
<thead>
|
||
<tr>
|
||
<th>用户ID</th>
|
||
<th>用户名</th>
|
||
<th>权限</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="usersTableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 添加/编辑用户模态框 -->
|
||
<div id="userModal" class="modal">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h2 id="modalTitle">添加用户</h2>
|
||
<form id="userForm">
|
||
<input type="hidden" id="userId" name="user_id">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="username">用户名</label>
|
||
<input type="text" id="username" name="username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="permission">权限</label>
|
||
<select id="permission" name="permission" required>
|
||
<option value="0">管理员</option>
|
||
<option value="1">普通用户</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="password">密码</label>
|
||
<input type="password" id="password" name="password" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="confirmPassword">确认密码</label>
|
||
<input type="password" id="confirmPassword" name="confirmPassword" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认删除模态框 -->
|
||
<div id="deleteModal" class="modal">
|
||
<div class="modal-content">
|
||
<span class="close">×</span>
|
||
<h2>确认删除</h2>
|
||
<p>确定要删除用户 <strong id="deleteUserName"></strong> 吗?此操作不可撤销。</p>
|
||
<input type="hidden" id="deleteUserId">
|
||
<button id="confirmDeleteBtn" class="btn btn-danger">确认删除</button>
|
||
<button class="btn">取消</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 获取CSRF令牌的函数
|
||
function getCookie(name) {
|
||
const value = `; ${document.cookie}`;
|
||
const parts = value.split(`; ${name}=`);
|
||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||
}
|
||
|
||
// 导航点击处理函数,提供备用URL
|
||
function handleNavClick(element, fallbackUrl) {
|
||
// 尝试使用Django模板生成的URL,如果失败则使用备用URL
|
||
try {
|
||
// 如果模板渲染正常,直接返回true让默认行为处理
|
||
return true;
|
||
} catch (e) {
|
||
// 如果模板渲染有问题,使用备用URL
|
||
window.location.href = fallbackUrl;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 显示通知
|
||
function showNotification(message, isSuccess = true) {
|
||
const notification = isSuccess ?
|
||
document.getElementById('successNotification') :
|
||
document.getElementById('errorNotification');
|
||
|
||
notification.textContent = message;
|
||
notification.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
notification.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
// 获取所有用户
|
||
async function loadUsers(searchTerm = '') {
|
||
try {
|
||
const url = searchTerm ?
|
||
`/elastic/users/?search=${encodeURIComponent(searchTerm)}` :
|
||
'/elastic/users/';
|
||
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
|
||
if (result.status === 'success') {
|
||
const tbody = document.getElementById('usersTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
// 处理不同格式的API响应
|
||
const users = result.data || result.users || [];
|
||
|
||
users.forEach(user => {
|
||
const row = document.createElement('tr');
|
||
|
||
// 根据权限值显示权限名称
|
||
const permissionText = Number(user.permission) === 0 ? '管理员' : '普通用户';
|
||
|
||
row.innerHTML = `
|
||
<td>${user.user_id}</td>
|
||
<td>${user.username}</td>
|
||
<td>${permissionText}</td>
|
||
<td class="action-buttons">
|
||
<button class="btn btn-success edit-btn" data-user='${JSON.stringify(user)}'>编辑</button>
|
||
<button class="btn btn-danger delete-btn" data-username="${user.username}" data-userid="${user.user_id}">删除</button>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
} else {
|
||
showNotification('获取用户列表失败', false);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户列表失败:', error);
|
||
showNotification('获取用户列表失败', false);
|
||
}
|
||
}
|
||
|
||
// 打开添加用户模态框
|
||
function openAddModal() {
|
||
document.getElementById('modalTitle').textContent = '添加用户';
|
||
document.getElementById('userForm').reset();
|
||
document.getElementById('userId').value = '';
|
||
document.getElementById('password').required = true;
|
||
document.getElementById('confirmPassword').required = true;
|
||
document.getElementById('userModal').style.display = 'block';
|
||
}
|
||
|
||
// 打开编辑用户模态框
|
||
function openEditModal(user) {
|
||
document.getElementById('modalTitle').textContent = '编辑用户';
|
||
document.getElementById('username').value = user.username;
|
||
document.getElementById('userId').value = user.user_id;
|
||
document.getElementById('permission').value = user.permission;
|
||
document.getElementById('password').required = false;
|
||
document.getElementById('confirmPassword').required = false;
|
||
document.getElementById('userModal').style.display = 'block';
|
||
}
|
||
|
||
// 打开删除确认模态框
|
||
function openDeleteModal(username, userId) {
|
||
document.getElementById('deleteUserName').textContent = username;
|
||
document.getElementById('deleteUserId').value = userId;
|
||
document.getElementById('deleteModal').style.display = 'block';
|
||
}
|
||
|
||
// 保存用户(添加或编辑)
|
||
async function saveUser(event) {
|
||
event.preventDefault();
|
||
|
||
const formData = new FormData(event.target);
|
||
const userId = formData.get('user_id');
|
||
const username = formData.get('username');
|
||
const permission = formData.get('permission');
|
||
const password = formData.get('password');
|
||
const confirmPassword = formData.get('confirmPassword');
|
||
|
||
// 验证密码
|
||
if (password !== confirmPassword) {
|
||
showNotification('密码和确认密码不匹配', false);
|
||
return;
|
||
}
|
||
|
||
// 验证密码长度(如果提供了密码)
|
||
if (password && password.length < 6) {
|
||
showNotification('密码长度至少为6位', false);
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
username: username,
|
||
permission: parseInt(permission)
|
||
};
|
||
|
||
if (password) {
|
||
data.password = password;
|
||
}
|
||
|
||
try {
|
||
const csrftoken = getCookie('csrftoken');
|
||
let response;
|
||
|
||
if (userId) {
|
||
response = await fetch(`/elastic/users/${userId}/update/`, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
response = await fetch('/elastic/users/add/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.status === 'success') {
|
||
showNotification(userId ? '用户更新成功' : '用户添加成功');
|
||
document.getElementById('userModal').style.display = 'none';
|
||
loadUsers();
|
||
} else {
|
||
showNotification(result.message || '操作失败', false);
|
||
}
|
||
} catch (error) {
|
||
console.error('保存用户失败:', error);
|
||
showNotification('保存用户失败', false);
|
||
}
|
||
}
|
||
|
||
// 删除用户
|
||
async function deleteUser() {
|
||
const userId = document.getElementById('deleteUserId').value;
|
||
|
||
try {
|
||
const csrftoken = getCookie('csrftoken');
|
||
const response = await fetch(`/elastic/users/${userId}/delete/`, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken
|
||
}
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.status === 'success') {
|
||
showNotification('用户删除成功');
|
||
document.getElementById('deleteModal').style.display = 'none';
|
||
loadUsers();
|
||
} else {
|
||
showNotification(result.message || '删除失败', false);
|
||
}
|
||
} catch (error) {
|
||
console.error('删除用户失败:', error);
|
||
showNotification('删除用户失败', false);
|
||
}
|
||
}
|
||
|
||
// 事件监听器
|
||
const addBtn = document.getElementById('addUserBtn');
|
||
if (addBtn) {
|
||
addBtn.addEventListener('click', openAddModal);
|
||
}
|
||
|
||
document.getElementById('userForm').addEventListener('submit', saveUser);
|
||
|
||
document.getElementById('confirmDeleteBtn').addEventListener('click', deleteUser);
|
||
|
||
document.querySelectorAll('.close').forEach(closeBtn => {
|
||
closeBtn.addEventListener('click', function() {
|
||
this.parentElement.parentElement.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
const searchBtn = document.getElementById('searchBtn');
|
||
if (searchBtn) {
|
||
searchBtn.addEventListener('click', function() {
|
||
const searchTerm = document.getElementById('searchInput').value;
|
||
loadUsers(searchTerm);
|
||
});
|
||
}
|
||
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
if (resetBtn) {
|
||
resetBtn.addEventListener('click', function() {
|
||
document.getElementById('searchInput').value = '';
|
||
loadUsers();
|
||
});
|
||
}
|
||
|
||
// 点击模态框外部关闭模态框
|
||
window.addEventListener('click', function(event) {
|
||
const modals = document.querySelectorAll('.modal');
|
||
modals.forEach(modal => {
|
||
if (event.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// 登出功能
|
||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||
const msg = document.getElementById('logoutMsg');
|
||
msg.textContent = '';
|
||
const csrftoken = getCookie('csrftoken');
|
||
try {
|
||
const resp = await fetch('/accounts/logout/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken || ''
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.ok) {
|
||
throw new Error('登出失败');
|
||
}
|
||
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
document.cookie = 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
window.location.href = data.redirect_url;
|
||
} catch (e) {
|
||
msg.textContent = e.message || '发生错误';
|
||
}
|
||
});
|
||
|
||
// 页面加载时获取用户列表
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const selfForm = document.getElementById('selfPwdForm');
|
||
if (selfForm) {
|
||
selfForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const uid = document.getElementById('selfUserId').value;
|
||
const pwd = document.getElementById('password').value;
|
||
const cpwd = document.getElementById('confirmPassword').value;
|
||
if (pwd !== cpwd) { showNotification('密码和确认密码不匹配', false); return; }
|
||
if ((pwd || '').length < 6) { showNotification('密码长度至少为6位', false); return; }
|
||
try {
|
||
const csrftoken = getCookie('csrftoken');
|
||
const resp = await fetch(`/elastic/users/${uid}/update/`, {
|
||
method: 'POST', credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' },
|
||
body: JSON.stringify({ password: pwd })
|
||
});
|
||
const result = await resp.json();
|
||
if (resp.ok && result.status === 'success') { showNotification('修改成功'); }
|
||
else { showNotification(result.message || '操作失败', false); }
|
||
} catch (error) {
|
||
showNotification('保存失败', false);
|
||
}
|
||
});
|
||
}
|
||
const tbody = document.getElementById('usersTableBody');
|
||
if (tbody) {
|
||
loadUsers();
|
||
}
|
||
});
|
||
|
||
// 为表格中的编辑和删除按钮添加事件监听器
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.classList.contains('edit-btn')) {
|
||
const user = JSON.parse(e.target.getAttribute('data-user'));
|
||
openEditModal(user);
|
||
}
|
||
|
||
if (e.target.classList.contains('delete-btn')) {
|
||
const username = e.target.getAttribute('data-username');
|
||
const userId = e.target.getAttribute('data-userid');
|
||
openDeleteModal(username, userId);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |