feat: 阶段2 Python后端、用户管理、统计、单词收藏与响应式适配

This commit is contained in:
guoqingshan
2026-08-01 17:46:04 +08:00
parent a2e705aa30
commit 55705683ff
26 changed files with 1929 additions and 42 deletions
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 — Mi Diario</title>
<link rel="stylesheet" href="style.css">
<style>
body { display: block; max-width: 960px; margin: 0 auto; padding: 1rem; }
h1 { font-size: 1.4rem; margin: 0.75rem 0; }
.admin-card {
background: var(--panel-bg); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.25rem; margin-bottom: 1rem;
}
.admin-card h2 { font-size: 1rem; margin-bottom: 0.75rem; }
.admin-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; }
.admin-form label { font-size: 0.8rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.2rem; }
.admin-form input, .admin-form select {
border: 1px solid var(--border); border-radius: 6px; padding: 0.45rem 0.6rem;
background: var(--bg); color: var(--text); font-size: 0.9rem;
}
.admin-form button {
background: var(--accent); color: #fff; border: none; border-radius: 6px;
padding: 0.5rem 1rem; cursor: pointer; align-self: end;
}
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th, td { border: 1px solid var(--border); padding: 0.5rem 0.6rem; text-align: left; }
th { background: var(--bg); }
.user-actions { display: flex; gap: 0.35rem; }
.user-actions button {
border: 1px solid var(--border); background: none; border-radius: 4px;
padding: 0.3rem 0.5rem; cursor: pointer; color: var(--text-muted);
}
.user-actions button:hover { border-color: var(--accent); color: var(--accent); }
.user-actions .danger { color: var(--danger); border-color: var(--danger); }
.status-active { color: var(--success); }
.status-disabled { color: var(--danger); }
#admin-msg { min-height: 1.5em; font-size: 0.85rem; color: var(--text-muted); }
#back-home { display: inline-block; margin-bottom: 0.75rem; color: var(--accent); text-decoration: none; }
</style>
</head>
<body>
<a id="back-home" href="index.html">← 返回日记</a>
<h1>用户管理</h1>
<div id="admin-msg"></div>
<div class="admin-card">
<h2>添加用户</h2>
<div class="admin-form">
<label>用户名<input id="new-username" type="text"></label>
<label>显示名<input id="new-display" type="text"></label>
<label>密码<input id="new-password" type="password"></label>
<label>管理员<select id="new-admin"><option value="0">否</option><option value="1">是</option></select></label>
<label>启用<select id="new-active"><option value="1">是</option><option value="0">否</option></select></label>
<button id="btn-add-user">添加</button>
</div>
</div>
<div class="admin-card">
<h2>用户列表</h2>
<table>
<thead><tr><th>ID</th><th>用户名</th><th>显示名</th><th>角色</th><th>状态</th><th>创建时间</th><th>操作</th></tr></thead>
<tbody id="user-table-body"></tbody>
</table>
</div>
<script src="api.js"></script>
<script>
var adminMsg = document.getElementById('admin-msg');
function esc(s) {
var div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
function loadUsers() {
apiAdminUsers().then(function(users) {
document.getElementById('user-table-body').innerHTML = users.map(function(u) {
var statusClass = u.is_active ? 'status-active' : 'status-disabled';
var statusText = u.is_active ? '启用' : '禁用';
var roleText = u.is_admin ? '管理员' : '普通用户';
return '<tr>' +
'<td>' + u.id + '</td>' +
'<td>' + esc(u.username) + '</td>' +
'<td>' + esc(u.display_name) + '</td>' +
'<td>' + roleText + '</td>' +
'<td class="' + statusClass + '">' + statusText + '</td>' +
'<td>' + esc(u.created_at) + '</td>' +
'<td><div class="user-actions">' +
'<button data-id="' + u.id + '" data-active="' + (u.is_active ? '0' : '1') + '" class="toggle-user">' + (u.is_active ? '禁用' : '启用') + '</button>' +
'<button data-id="' + u.id + '" class="danger delete-user">删除</button>' +
'</div></td>' +
'</tr>';
}).join('') || '<tr><td colspan="7">暂无用户</td></tr>';
}).catch(function(err) {
adminMsg.textContent = err.message;
});
}
document.getElementById('btn-add-user').addEventListener('click', function() {
var username = document.getElementById('new-username').value.trim();
var display = document.getElementById('new-display').value.trim();
var password = document.getElementById('new-password').value;
if (!username || !password) { adminMsg.textContent = '用户名和密码不能为空'; return; }
apiAdminCreateUser({
username: username,
displayName: display || username,
password: password,
isAdmin: document.getElementById('new-admin').value === '1',
isActive: document.getElementById('new-active').value === '1'
}).then(function() {
adminMsg.textContent = '用户已添加';
document.getElementById('new-username').value = '';
document.getElementById('new-display').value = '';
document.getElementById('new-password').value = '';
loadUsers();
}).catch(function(err) { adminMsg.textContent = err.message; });
});
document.getElementById('user-table-body').addEventListener('click', function(e) {
var btn = e.target.closest('button');
if (!btn) return;
var id = btn.dataset.id;
if (btn.classList.contains('toggle-user')) {
apiAdminSetStatus(id, btn.dataset.active === '1').then(function() {
loadUsers();
}).catch(function(err) { adminMsg.textContent = err.message; });
} else if (btn.classList.contains('delete-user')) {
if (!confirm('确定删除该用户及其全部日记、收藏数据吗?')) return;
apiAdminDeleteUser(id).then(function() {
adminMsg.textContent = '用户已删除';
loadUsers();
}).catch(function(err) { adminMsg.textContent = err.message; });
}
});
apiGetMe().then(function(me) {
if (!me.isAdmin) {
location.href = 'index.html';
return;
}
loadUsers();
}).catch(function(err) {
if (err && err.status === 401) location.href = 'login.html';
else adminMsg.textContent = err.message;
});
</script>
</body>
</html>