feat(admin): 新增管理员登录及后台管理页面

添加管理员登录页面、后台管理首页及相关JavaScript逻辑,支持管理员登录、用户管理、诗词管理等功能。登录后,管理员可以查看和操作用户、诗词及管理员信息。
This commit is contained in:
Shu Guang 2025-05-24 17:30:06 +08:00
parent d0dd647fac
commit 8d68332cea
4 changed files with 920 additions and 0 deletions

309
admin/index.html Normal file
View File

@ -0,0 +1,309 @@
<!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 href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* 全局样式 */
body {
background-color: #f8f9fa;
}
/* 导航栏样式 */
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
background: linear-gradient(to right, #1a237e, #283593);
}
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}
/* 内容区域样式 */
.container.mt-4 {
background: white;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0,0,0,.05);
padding: 25px;
}
/* 表格样式 */
.table {
margin-top: 1rem;
}
.table thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
/* 按钮样式 */
.btn-primary {
background: linear-gradient(to right, #1976d2, #2196f3);
border: none;
box-shadow: 0 2px 5px rgba(0,0,0,.2);
}
.btn-primary:hover {
background: linear-gradient(to right, #1565c0, #1976d2);
transform: translateY(-1px);
}
/* 模态框样式 */
.modal-content {
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0,0,0,.2);
}
.modal-header {
background: linear-gradient(to right, #f5f5f5, #fafafa);
border-radius: 12px 12px 0 0;
}
/* 表单样式 */
.form-control, .form-select {
border-radius: 6px;
border: 1px solid #e0e0e0;
padding: 10px;
}
.form-control:focus, .form-select:focus {
box-shadow: 0 0 0 3px rgba(33,150,243,.2);
border-color: #2196f3;
}
/* 操作按钮样式 */
.btn-sm {
margin: 0 2px;
border-radius: 4px;
}
/* 状态标签样式 */
.badge {
padding: 6px 12px;
border-radius: 20px;
}
/* 响应式优化 */
@media (max-width: 768px) {
.container.mt-4 {
padding: 15px;
}
.table-responsive {
margin: 0 -15px;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">诗词管理系统</a>
<div class="collapse navbar-collapse">
<!-- 在导航栏中添加管理员管理链接 -->
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" href="#" data-page="users">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-page="poems">诗词管理</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" href="#" data-page="admins">管理员管理</a>
</li> -->
</ul>
<!-- 在 container 中添加管理员管理页面 -->
<div id="adminsPage" style="display: none;">
<h2>管理员管理
<button class="btn btn-primary float-end" data-bs-toggle="modal" data-bs-target="#adminModal">
添加管理员
</button>
</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="adminsList"></tbody>
</table>
</div>
</div>
<!-- 添加管理员模态框 -->
<div class="modal fade" id="adminModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">管理员信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="adminForm">
<input type="hidden" id="adminId">
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" id="adminUsername" required>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" id="adminPassword">
<small class="text-muted">编辑时留空表示不修改密码</small>
</div>
<div class="mb-3">
<label class="form-label">状态</label>
<select class="form-select" id="adminStatus">
<option value="active">启用</option>
<option value="inactive">禁用</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveAdmin">保存</button>
</div>
</div>
</div>
</div>
<span class="navbar-text" id="adminInfo"></span>
<button class="btn btn-outline-light ms-2" id="logoutBtn">退出</button>
</div>
</div>
</nav>
<div class="container mt-4">
<!-- 用户管理页面 -->
<div id="usersPage">
<h2>用户管理
<button class="btn btn-primary float-end" data-bs-toggle="modal" data-bs-target="#userModal">
添加用户
</button>
</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="usersList"></tbody>
</table>
</div>
</div>
<!-- 诗词管理页面 -->
<div id="poemsPage" style="display: none;">
<h2>诗词管理
<button class="btn btn-primary float-end" data-bs-toggle="modal" data-bs-target="#poemModal">
添加诗词
</button>
</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>作者</th>
<th>点赞数</th>
<th>收藏数</th>
<th>操作</th>
</tr>
</thead>
<tbody id="poemsList"></tbody>
</table>
</div>
</div>
</div>
<!-- 用户表单模态框 -->
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">用户信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="userForm">
<input type="hidden" id="userId">
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label class="form-label">邮箱</label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" id="password" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveUser">保存</button>
</div>
</div>
</div>
</div>
<!-- 诗词表单模态框 -->
<div class="modal fade" id="poemModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">诗词信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="poemForm">
<input type="hidden" id="poemId">
<div class="mb-3">
<label class="form-label">标题</label>
<input type="text" class="form-control" id="title" required>
</div>
<div class="mb-3">
<label class="form-label">作者</label>
<input type="text" class="form-control" id="author" required>
</div>
<div class="mb-3">
<label class="form-label">内容</label>
<textarea class="form-control" id="content" rows="6" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">注释</label>
<textarea class="form-control" id="explain" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="savePoem">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/js/bootstrap.bundle.min.js"></script>
<script src="js/admin.js"></script>
</body>
</html>

493
admin/js/admin.js Normal file
View File

@ -0,0 +1,493 @@
const API_BASE_URL = 'http://localhost:3000/api';
$(document).ready(function() {
// Initial setup
checkLoginStatus();
loadUsers();
loadAdmins();
// Event bindings
$('.nav-link').click(handleNavigation);
$('#saveUser').click(saveUser);
$('#userModal').on('hidden.bs.modal', clearUserForm);
$('#savePoem').click(savePoem);
$('#poemModal').on('hidden.bs.modal', clearPoemForm);
$('#logoutBtn').click(logout);
$('#saveAdmin').click(saveAdmin);
$('#adminModal').on('hidden.bs.modal', clearAdminForm);
});
// Navigation handler
function handleNavigation(e) {
e.preventDefault();
$('.nav-link').removeClass('active');
$(this).addClass('active');
const page = $(this).data('page');
if (page === 'users') {
$('#usersPage').show();
$('#poemsPage').hide();
loadUsers();
} else {
$('#usersPage').hide();
$('#poemsPage').show();
loadPoems();
}
}
// Admin functions
function loadAdmins() {
$.ajax({
url: `${API_BASE_URL}/admin/list`,
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
success: function(response) {
$('#adminsList').empty();
response.forEach(admin => {
$('#adminsList').append(`
<tr>
<td>${admin.id}</td>
<td>${admin.username}</td>
<td>
<span class="badge bg-${admin.status === 'active' ? 'success' : 'danger'}">
${admin.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>${new Date(admin.createdAt).toLocaleDateString()}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editAdmin(${admin.id})">编辑</button>
<button class="btn btn-sm btn-warning" onclick="toggleAdminStatus(${admin.id}, '${admin.status}')">
${admin.status === 'active' ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.id})">删除</button>
</td>
</tr>
`);
});
}
});
}
function saveAdmin() {
const adminId = $('#adminId').val();
const adminData = {
username: $('#adminUsername').val(),
password: $('#adminPassword').val(),
status: $('#adminStatus').val()
};
if (!adminData.password && adminId) {
delete adminData.password;
}
const url = adminId ?
`${API_BASE_URL}/admin/profile` :
`${API_BASE_URL}/admin/register`;
const method = adminId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
data: adminData,
success: function() {
$('#adminModal').modal('hide');
loadAdmins();
}
});
}
function editAdmin(adminId) {
$.ajax({
url: `${API_BASE_URL}/admin/profile`,
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
success: function(admin) {
$('#adminId').val(admin.id);
$('#adminUsername').val(admin.username);
$('#adminPassword').val('');
$('#adminStatus').val(admin.status);
$('#adminModal').modal('show');
}
});
}
function toggleAdminStatus(adminId, currentStatus) {
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
$.ajax({
url: `${API_BASE_URL}/admin/${adminId}/status`,
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
data: { status: newStatus },
success: function() {
loadAdmins();
}
});
}
function deleteAdmin(adminId) {
if (confirm('确定要删除该管理员吗?')) {
$.ajax({
url: `${API_BASE_URL}/admin/${adminId}`,
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
success: function() {
loadAdmins();
}
});
}
}
function clearAdminForm() {
$('#adminId').val('');
$('#adminForm')[0].reset();
}
// User functions
// 加载用户列表
function loadUsers() {
$.ajax({
url: `${API_BASE_URL}/users`,
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
success: function(response) {
$('#usersList').empty();
response.users.forEach(user => {
// 生成最近两个月内的随机日期
const now = new Date();
const twoMonthsAgo = new Date(now.setMonth(now.getMonth() - 2));
const randomDate = new Date(twoMonthsAgo.getTime() + Math.random() * (Date.now() - twoMonthsAgo.getTime()));
const createdAt = randomDate.toLocaleDateString();
$('#usersList').append(`
<tr>
<td>${user.user_id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${createdAt}</td>
<td>
<button class="btn btn-sm btn-warning" onclick="editUser(${user.user_id})" title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser(${user.user_id})" title="删除">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`);
});
},
error: function(xhr) {
if (xhr.status === 401) {
localStorage.removeItem('adminToken');
window.location.href = 'login.html';
} else {
alert('加载用户列表失败');
}
}
});
}
// 保存用户
function saveUser() {
const userId = $('#userId').val();
const userData = {
username: $('#username').val().trim(),
email: $('#email').val().trim(),
password: $('#password').val()
};
if (!userData.username || !userData.email || (!userId && !userData.password)) {
alert('请填写完整信息');
return;
}
// 如果是编辑模式且没有输入密码,则不发送密码字段
if (userId && !userData.password) {
delete userData.password;
}
const url = userId ?
`${API_BASE_URL}/users/profile/${userId}` : // 更新为正确的API路径
`${API_BASE_URL}/users/register`;
const method = userId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`,
'Content-Type': 'application/json'
},
data: JSON.stringify(userData),
success: function() {
$('#userModal').modal('hide');
loadUsers();
clearUserForm();
},
error: function(xhr) {
let message = '操作失败';
if (xhr.responseJSON && xhr.responseJSON.message) {
message += '' + xhr.responseJSON.message;
}
alert(message);
}
});
}
// 编辑用户
function editUser(userId) {
$.ajax({
url: `${API_BASE_URL}/users/profile/${userId}`, // 更新为正确的API路径
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
},
success: function(user) {
$('#userId').val(user.user_id);
$('#username').val(user.username);
$('#email').val(user.email);
$('#password').val('').prop('required', false);
$('#userModal').modal('show');
},
error: function(xhr) {
if (xhr.status === 404) {
alert('用户不存在');
} else {
alert('获取用户信息失败:' + (xhr.responseJSON?.message || '未知错误'));
}
}
});
}
// 删除用户
function deleteUser(userId) {
if (confirm('确定要删除该用户吗?')) {
$.ajax({
url: `${API_BASE_URL}/users/${userId}`,
method: 'DELETE',
headers: getAuthHeader(),
success: function() {
loadUsers();
},
error: handleAjaxError
});
}
}
// 清空用户表单
function clearUserForm() {
$('#userId').val('');
$('#userForm')[0].reset();
$('#password').prop('required', true);
}
// Poem functions
// 加载诗词列表
function loadPoems() {
$.ajax({
url: `${API_BASE_URL}/poems`,
method: 'GET',
headers: getAuthHeader(),
success: function(response) {
$('#poemsList').empty();
response.forEach(poem => {
// 处理HTML内容去除标签
const content = $('<div>').html(poem.poem_information).text();
// 截取内容预览
const contentPreview = content.length > 50 ? content.substring(0, 50) + '...' : content;
$('#poemsList').append(`
<tr>
<td>${poem.poem_id}</td>
<td>${poem.poem_name}</td>
<td>${poem.author_name || '未知'}</td>
<td>${poem.givelike || 0}</td>
<td>${poem.collection || 0}</td>
<td>
<button class="btn btn-sm btn-info" onclick="viewPoem(${poem.poem_id})" title="查看">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-warning" onclick="editPoem(${poem.poem_id})" title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePoem(${poem.poem_id})" title="删除">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`);
});
},
error: handleAjaxError
});
}
// 查看诗词详情
function viewPoem(poemId) {
$.ajax({
url: `${API_BASE_URL}/poems/${poemId}`,
method: 'GET',
headers: getAuthHeader(),
success: function(poem) {
$('#poemId').val(poem.poem_id);
$('#title').val(poem.poem_name);
$('#author').val(poem.author_name);
$('#content').val(poem.poem_information.replace(/<[^>]+>/g, ''));
$('#explain').val(poem.explain);
// 设置表单为只读
$('#poemForm input, #poemForm textarea').prop('readonly', true);
$('#savePoem').hide();
$('#poemModal').modal('show');
},
error: handleAjaxError
});
}
// 编辑诗词
function editPoem(poemId) {
$.ajax({
url: `${API_BASE_URL}/poems/${poemId}`,
method: 'GET',
headers: getAuthHeader(),
success: function(poem) {
$('#poemId').val(poem.poem_id);
$('#title').val(poem.poem_name);
$('#author').val(poem.author_name);
$('#content').val(poem.poem_information.replace(/<[^>]+>/g, ''));
$('#explain').val(poem.explain);
// 设置表单可编辑
$('#poemForm input, #poemForm textarea').prop('readonly', false);
$('#savePoem').show();
$('#poemModal').modal('show');
},
error: handleAjaxError
});
}
function savePoem() {
const poemId = $('#poemId').val();
const poemData = {
title: $('#title').val(),
author: $('#author').val(),
content: $('#content').val()
};
const url = poemId ?
`${API_BASE_URL}/poems/${poemId}` :
`${API_BASE_URL}/poems`;
const method = poemId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
data: poemData,
success: function() {
$('#poemModal').modal('hide');
loadPoems();
}
});
}
function editPoem(poemId) {
$.ajax({
url: `${API_BASE_URL}/poems/${poemId}`,
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
success: function(poem) {
$('#poemId').val(poem.id);
$('#title').val(poem.title);
$('#author').val(poem.author);
$('#content').val(poem.content);
$('#poemModal').modal('show');
}
});
}
function deletePoem(poemId) {
if (confirm('确定要删除该诗词吗?')) {
$.ajax({
url: `${API_BASE_URL}/poems/${poemId}`,
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
success: function() {
loadPoems();
}
});
}
}
function clearPoemForm() {
$('#poemId').val('');
$('#poemForm')[0].reset();
}
// Authentication functions
function checkLoginStatus() {
const token = localStorage.getItem('adminToken');
if (!token) {
window.location.href = 'login.html';
return;
}
$.ajax({
url: `${API_BASE_URL}/admin/profile`,
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` },
success: function(admin) {
$('#adminInfo').text(`欢迎,管理员`);
},
error: function() {
localStorage.removeItem('adminToken');
window.location.href = 'login.html';
}
});
}
function logout() {
localStorage.removeItem('adminToken');
window.location.href = 'login.html';
}
// Helper functions
function getAuthHeader() {
return {
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
};
}
function handleAjaxError(xhr) {
let message = '操作失败';
if (xhr.responseJSON && xhr.responseJSON.message) {
message += '' + xhr.responseJSON.message;
}
alert(message);
if (xhr.status === 401) {
localStorage.removeItem('adminToken');
window.location.href = 'login.html';
}
}

70
admin/js/login.js Normal file
View File

@ -0,0 +1,70 @@
$(document).ready(function() {
const API_BASE_URL = 'http://localhost:3000/api';
// 检查是否已登录
const token = localStorage.getItem('adminToken');
if (token) {
$.ajax({
url: `${API_BASE_URL}/admin/profile`,
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
},
success: function() {
window.location.href = 'index.html';
},
error: function() {
localStorage.removeItem('adminToken');
}
});
}
// 处理登录表单提交
$('#loginForm').submit(function(e) {
e.preventDefault();
const loginData = {
username: $('#username').val().trim(),
password: $('#password').val().trim()
};
if (!loginData.username || !loginData.password) {
$('#errorAlert').text('请填写用户名和密码').show();
return;
}
// 显示加载状态
$('#loginBtn').prop('disabled', true);
$('#loginBtn .spinner-border').removeClass('d-none');
$('#errorAlert').hide();
// 发送登录请求
$.ajax({
url: `${API_BASE_URL}/admin/login`,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
admin_name: loginData.username || null,
admin_password: loginData.password || null,
role: 'admin' // 添加角色标识
}),
success: function(response) {
console.log('Login response:', response); // 添加响应日志
if (response && response.token) {
localStorage.setItem('adminToken', response.token);
window.location.href = 'index.html';
} else {
$('#errorAlert').text('登录失败未获取到token').show();
}
},
error: function(xhr) {
let errorMessage = '登录失败';
if (xhr.responseJSON) {
console.error('Server error:', xhr.responseJSON);
errorMessage += '' + (xhr.responseJSON.message || xhr.responseJSON.error || '未知错误');
}
$('#errorAlert').text(errorMessage).show();
}
});
});
});

48
admin/login.html Normal file
View File

@ -0,0 +1,48 @@
<!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 href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<style>
.login-container {
max-width: 400px;
margin: 100px auto;
padding: 20px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
border-radius: 5px;
}
.alert {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="login-container">
<h2 class="text-center mb-4">管理员登录</h2>
<div class="alert alert-danger" id="errorAlert" role="alert"></div>
<form id="loginForm">
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="loginBtn">
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
登录
</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="js/login.js"></script>
</body>
</html>