增加用户管理

This commit is contained in:
2026-07-28 18:02:05 +08:00
parent 21be62fa7c
commit 0b22d4f126
10 changed files with 822 additions and 8 deletions
+203
View File
@@ -43,6 +43,9 @@ document.addEventListener('DOMContentLoaded', () => {
// 模态框表单
document.getElementById('modal-form').addEventListener('submit', handleModalSubmit);
// 用户表单
document.getElementById('user-form').addEventListener('submit', handleUserSubmit);
// 封面图上传
const coverFileInput = document.getElementById('post-cover-file');
if (coverFileInput) {
@@ -104,6 +107,9 @@ function showPage(pageName) {
case 'comments':
loadComments();
break;
case 'users':
loadUsers();
break;
case 'files':
loadFiles();
break;
@@ -709,6 +715,203 @@ function getStatusText(status) {
return map[status] || status;
}
// ==================== 用户管理 ====================
// 加载用户列表
async function loadUsers() {
try {
const res = await fetch(`${API_BASE}/admin/users?page_size=100`, { headers: getHeaders() });
const data = await res.json();
const tbody = document.getElementById('users-list');
tbody.innerHTML = data.data?.map(u => {
const isSelf = currentUser && u.id === currentUser.id;
return `
<tr>
<td>${u.username}${isSelf ? ' <span class="status-badge status-published">当前</span>' : ''}</td>
<td>${u.nickname || '-'}</td>
<td>${u.email || '-'}</td>
<td>${u.role === 'admin' ? '管理员' : '普通用户'}</td>
<td><span class="status-badge ${u.status === 1 ? 'status-published' : 'status-draft'}">${u.status === 1 ? '启用' : '禁用'}</span></td>
<td>${u.last_login_at ? new Date(u.last_login_at).toLocaleString() : '-'}</td>
<td>
<button class="btn-success" onclick="editUser(${u.id})">编辑</button>
<button class="btn-secondary" onclick="resetUserPassword(${u.id}, '${u.username}')">重置密码</button>
${isSelf ? '' : `
${u.status === 1 ? `<button class="btn-secondary" onclick="toggleUserStatus(${u.id}, 0)">禁用</button>` : `<button class="btn-primary" onclick="toggleUserStatus(${u.id}, 1)">启用</button>`}
<button class="btn-danger" onclick="deleteUser(${u.id}, '${u.username}')">删除</button>`}
</td>
</tr>
`;
}).join('') || '<tr><td colspan="7" style="text-align:center">暂无用户</td></tr>';
// 缓存用户数据供编辑使用
usersCache = data.data || [];
} catch (err) {
console.error('加载用户失败:', err);
}
}
let usersCache = [];
// 显示新建用户表单
function showUserForm() {
document.getElementById('user-modal-title').textContent = '新建用户';
document.getElementById('user-form').reset();
document.getElementById('user-id').value = '';
document.getElementById('user-username').disabled = false;
document.getElementById('user-password-group').classList.remove('hidden');
document.getElementById('user-password').required = true;
document.getElementById('user-role').disabled = false;
document.getElementById('user-status').disabled = false;
document.getElementById('user-modal').classList.remove('hidden');
}
// 编辑用户
function editUser(id) {
const user = usersCache.find(u => u.id === id);
if (!user) return;
const isSelf = currentUser && user.id === currentUser.id;
document.getElementById('user-modal-title').textContent = '编辑用户';
document.getElementById('user-id').value = user.id;
document.getElementById('user-username').value = user.username;
document.getElementById('user-username').disabled = true;
// 编辑时不修改密码,隐藏密码输入
document.getElementById('user-password-group').classList.add('hidden');
document.getElementById('user-password').required = false;
document.getElementById('user-password').value = '';
document.getElementById('user-nickname').value = user.nickname || '';
document.getElementById('user-email').value = user.email || '';
document.getElementById('user-role').value = user.role;
document.getElementById('user-status').value = String(user.status);
// 不能修改自己的角色和状态
document.getElementById('user-role').disabled = isSelf;
document.getElementById('user-status').disabled = isSelf;
document.getElementById('user-modal').classList.remove('hidden');
}
// 隐藏用户弹窗
function hideUserModal() {
document.getElementById('user-modal').classList.add('hidden');
}
// 提交用户表单
async function handleUserSubmit(e) {
e.preventDefault();
const id = document.getElementById('user-id').value;
const data = {
nickname: document.getElementById('user-nickname').value,
email: document.getElementById('user-email').value,
role: document.getElementById('user-role').value,
status: parseInt(document.getElementById('user-status').value)
};
if (!id) {
data.username = document.getElementById('user-username').value;
data.password = document.getElementById('user-password').value;
}
try {
const url = id ? `${API_BASE}/admin/users/${id}` : `${API_BASE}/admin/users`;
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: getHeaders(),
body: JSON.stringify(data)
});
if (res.ok) {
hideUserModal();
loadUsers();
} else {
const err = await res.json().catch(() => ({}));
alert(err.error || '操作失败');
}
} catch (err) {
alert('网络错误');
}
}
// 启用/禁用用户
async function toggleUserStatus(id, status) {
const actionText = status === 1 ? '启用' : '禁用';
if (!confirm(`确定要${actionText}这个用户吗?`)) return;
try {
const user = usersCache.find(u => u.id === id);
const res = await fetch(`${API_BASE}/admin/users/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({
nickname: user?.nickname || '',
email: user?.email || '',
status: status
})
});
if (res.ok) {
loadUsers();
} else {
const err = await res.json().catch(() => ({}));
alert(err.error || '操作失败');
}
} catch (err) {
alert('网络错误');
}
}
// 重置用户密码
async function resetUserPassword(id, username) {
const newPassword = prompt(`请输入用户「${username}」的新密码(至少 6 位):`);
if (newPassword === null) return;
if (newPassword.length < 6) {
alert('密码至少 6 位');
return;
}
try {
const res = await fetch(`${API_BASE}/admin/users/${id}/password`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ new_password: newPassword })
});
if (res.ok) {
alert('密码重置成功');
} else {
const err = await res.json().catch(() => ({}));
alert(err.error || '重置失败');
}
} catch (err) {
alert('网络错误');
}
}
// 删除用户
async function deleteUser(id, username) {
if (!confirm(`确定要删除用户「${username}」吗?`)) return;
try {
const res = await fetch(`${API_BASE}/admin/users/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
if (res.ok) {
loadUsers();
} else {
const err = await res.json().catch(() => ({}));
alert(err.error || '删除失败');
}
} catch (err) {
alert('网络错误');
}
}
// ==================== 主题管理 ====================
// 加载主题列表
+76 -5
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理 - GoBlog</title>
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/static/admin/style.css">
<link rel="stylesheet" href="/static/admin/style.css?v=2">
</head>
<body>
<div id="app">
@@ -21,6 +21,7 @@
<a href="#tags" class="menu-item" data-page="tags">标签管理</a>
<a href="#pages" class="menu-item" data-page="pages">页面管理</a>
<a href="#comments" class="menu-item" data-page="comments">评论管理</a>
<a href="#users" class="menu-item" data-page="users">用户管理</a>
<a href="#files" class="menu-item" data-page="files">文件管理</a>
<a href="#themes" class="menu-item" data-page="themes">主题管理</a>
</nav>
@@ -163,8 +164,10 @@
<button type="button" id="md-tab-preview" onclick="mdShowPreview()">预览</button>
</div>
</div>
<textarea id="post-content" rows="15" required placeholder="支持 Markdown 语法,也兼容 HTML 内容"></textarea>
<div id="md-preview" class="md-preview hidden"></div>
<div class="md-body">
<textarea id="post-content" required placeholder="支持 Markdown 语法,也兼容 HTML 内容"></textarea>
<div id="md-preview" class="md-preview hidden"></div>
</div>
</div>
</div>
<div class="form-group">
@@ -172,7 +175,7 @@
<input type="checkbox" id="post-istop"> 置顶文章
</label>
</div>
<button type="submit" class="btn-primary">保存</button>
<button type="submit" class="btn-primary" onclick="mdShowEdit()">保存</button>
</form>
</div>
@@ -254,6 +257,28 @@
</table>
</div>
<!-- 用户管理 -->
<div id="users-page" class="page hidden">
<div class="page-header">
<h1>用户管理</h1>
<button class="btn-primary" onclick="showUserForm()">新建用户</button>
</div>
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>昵称</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>最后登录</th>
<th>操作</th>
</tr>
</thead>
<tbody id="users-list"></tbody>
</table>
</div>
<!-- 文件管理 -->
<div id="files-page" class="page hidden">
<div class="page-header">
@@ -339,6 +364,52 @@
</div>
</div>
<!-- 用户表单弹窗 -->
<div id="user-modal" class="modal hidden">
<div class="modal-content">
<h3 id="user-modal-title">新建用户</h3>
<form id="user-form">
<input type="hidden" id="user-id">
<div class="form-group">
<label>用户名</label>
<input type="text" id="user-username" required minlength="3" maxlength="50">
</div>
<div class="form-group" id="user-password-group">
<label>密码</label>
<input type="password" id="user-password" minlength="6" placeholder="至少 6 位">
</div>
<div class="form-group">
<label>昵称</label>
<input type="text" id="user-nickname">
</div>
<div class="form-group">
<label>邮箱</label>
<input type="email" id="user-email" required>
</div>
<div class="form-row">
<div class="form-group">
<label>角色</label>
<select id="user-role">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="form-group">
<label>状态</label>
<select id="user-status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="hideUserModal()">取消</button>
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 创建主题弹窗 -->
<div id="theme-modal" class="modal hidden">
<div class="modal-content modal-large">
@@ -371,6 +442,6 @@
</div>
</div>
<script src="/static/admin/app.js?v=11"></script>
<script src="/static/admin/app.js?v=12"></script>
</body>
</html>
+13 -3
View File
@@ -841,11 +841,22 @@ body {
color: #fff;
}
/* 编辑/预览共享同一容器,切换时区域尺寸不变 */
.md-body {
position: relative;
height: 460px;
min-height: 240px;
resize: vertical;
overflow: hidden;
}
.md-editor textarea {
display: block;
width: 100%;
height: 100%;
border: none !important;
border-radius: 0 !important;
resize: vertical;
resize: none;
font-family: Consolas, Monaco, 'Courier New', monospace;
line-height: 1.6;
}
@@ -855,8 +866,7 @@ body {
}
.md-preview {
min-height: 320px;
max-height: 600px;
height: 100%;
overflow-y: auto;
padding: 16px;
background: #fff;