增加用户管理
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user