chore: 完成全量功能迭代与部署准备

- 移除冗余依赖包
- 新增账号禁用校验与用户管理能力
- 新增文档下载与管理页面文件展示
- 新增API文档页面与用户管理前端页面
- 重构时区处理与docker-compose部署配置
- 完善测试用例与项目文档
This commit is contained in:
2026-08-01 00:02:42 +08:00
parent f92eff6f65
commit 6c6f690788
19 changed files with 2000 additions and 485 deletions
+591 -56
View File
@@ -107,6 +107,10 @@
.tag { display: inline-block; background: #eff6ff; color: #1d4ed8; border-radius: 3px; padding: 1px 6px; font-size: 12px; margin-right: 4px; }
.fallback-flag { color: #b45309; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 4px; padding: 6px 10px; font-size: 13px; margin-bottom: 10px; }
.toolbar { margin: 12px 0; }
.toolbar.users-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.users-toolbar input[type="text"], .users-toolbar select { width: auto; }
.users-toolbar input[type="text"] { min-width: 200px; }
.user-role-select { width: auto; display: inline-block; margin-right: 4px; }
.muted { color: #6b7280; font-size: 12px; }
.status-badge {
display: inline-block; border-radius: 3px; padding: 1px 8px;
@@ -133,6 +137,20 @@
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
}
.login-box h2 { font-size: 16px; margin: 0 0 16px; }
/* 通用 overlay 弹窗组件:固定全屏遮罩 + 居中卡片 */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.45);
display: flex; align-items: center; justify-content: center; z-index: 200;
}
.modal-card {
background: #fff; border-radius: 8px; padding: 20px 24px; width: 380px;
max-width: calc(100vw - 32px); box-shadow: 0 6px 20px rgba(0,0,0,0.25);
}
.modal-card .modal-title { font-size: 16px; margin: 0 0 14px; }
.modal-card .modal-content { font-size: 13px; }
.modal-card .modal-actions { margin-top: 14px; display: flex; gap: 8px; justify-content: flex-end; }
.modal-card .modal-strength-hint { margin-top: -6px; margin-bottom: 12px; font-size: 12px; }
.modal-card .modal-confirm-text { margin-bottom: 10px; }
.extracted-info {
background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 6px;
padding: 10px 12px; margin-bottom: 12px; font-size: 13px;
@@ -227,6 +245,8 @@
</form>
</div>
</div>
<!-- 通用弹窗挂载点:openModal 动态注入 .modal-overlay/.modal-card -->
<div id="modal-root"></div>
<header>
<h1>知识库管理后台</h1>
<div id="user-area" class="user-area hidden">
@@ -242,6 +262,7 @@
<button type="button" data-target="section-search">检索测试台</button>
<button type="button" data-target="section-categories">类目列表</button>
<button type="button" data-target="section-api-guide">API 指南</button>
<button type="button" data-target="section-profile" id="nav-profile">个人中心</button>
</nav>
</header>
<main>
@@ -256,6 +277,32 @@
<div id="overview-categories"></div>
</section>
<section id="section-profile" class="hidden">
<h2>个人中心</h2>
<div class="error-bar hidden" id="error-profile"></div>
<h3 style="font-size:14px;">账号信息</h3>
<div class="cards" id="profile-cards"></div>
<h3 style="font-size:14px; margin-top:24px;">修改密码</h3>
<form id="profile-password-form">
<div class="field">
<label for="profile-old-password">旧密码</label>
<input type="password" id="profile-old-password" name="old_password" required autocomplete="current-password">
</div>
<div class="field">
<label for="profile-new-password">新密码(至少 8 位)</label>
<input type="password" id="profile-new-password" name="new_password" required autocomplete="new-password">
</div>
<div class="field">
<label for="profile-confirm-password">确认新密码</label>
<input type="password" id="profile-confirm-password" name="confirm_password" required autocomplete="new-password">
</div>
<div class="muted" id="profile-strength-hint"></div>
<button type="submit" class="primary" id="btn-profile-password-submit">确认修改</button>
</form>
<h3 style="font-size:14px; margin-top:24px;">会话</h3>
<button type="button" class="action danger" id="btn-profile-logout">退出登录</button>
</section>
<section id="section-docs" class="hidden">
<h2>文档管理</h2>
<div class="error-bar hidden" id="error-docs"></div>
@@ -351,12 +398,19 @@
<section id="section-users" class="hidden">
<h2>用户管理</h2>
<div class="error-bar hidden" id="error-users"></div>
<div class="toolbar">
<div class="toolbar users-toolbar">
<input type="text" id="user-search" placeholder="按用户名搜索" autocomplete="off">
<select id="user-role-filter">
<option value="all" selected>全部角色</option>
<option value="admin">admin</option>
<option value="user">user</option>
</select>
<button type="button" class="action" id="btn-refresh-users">刷新</button>
<span class="muted" id="users-status"></span>
</div>
<table>
<thead>
<tr><th>用户名</th><th>角色</th><th>须改密</th><th>创建时间</th><th>操作</th></tr>
<tr><th>用户名</th><th>角色</th><th>须改密</th><th>创建时间</th><th>启用</th><th>操作</th></tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
@@ -454,7 +508,8 @@ function showLogin() {
unmountUsersSection();
}
/* 渲染主界面:顶栏用户区 + 角色门禁(仅 admin 挂载用户管理区块) */
/* 渲染主界面:顶栏用户区 + 角色门禁(仅 admin 挂载用户管理区块)
admin 默认进概览,user 角色默认进个人中心(user 无需访问概览统计) */
function enterApp(user) {
document.getElementById("login-overlay").classList.add("hidden");
document.getElementById("password-overlay").classList.add("hidden");
@@ -466,10 +521,11 @@ function enterApp(user) {
loadedOnce = {};
if (user.role === "admin") {
mountUsersSection();
activateSection("section-overview");
} else {
unmountUsersSection();
activateSection("section-profile");
}
activateSection("section-overview");
}
/* 登录/会话校验成功后的统一入口:must_change_password 用户先强制改密 */
@@ -507,14 +563,17 @@ document.getElementById("login-form").addEventListener("submit", function (event
});
});
document.getElementById("btn-logout").addEventListener("click", function () {
/* 退出登录:调用后端 logout 端点,失败不阻塞本地登出(顶栏 + 个人中心共用) */
function doLogout() {
api("/api/v1/auth/logout", { method: "POST" }).catch(function () {
/* 服务端退出失败不阻塞本地登出 */
}).finally(function () {
clearAuth();
showLogin();
});
});
}
document.getElementById("btn-logout").addEventListener("click", doLogout);
/* ---------- 修改密码 ---------- */
@@ -575,6 +634,191 @@ document.getElementById("password-form").addEventListener("submit", function (ev
});
});
/* ---------- 个人中心 ---------- */
/* 进入区块时加载当前账号信息:GET /auth/me 返回 username/role/must_change_password/enabled/created_at */
function loadProfile() {
hideError("error-profile");
api("/api/v1/auth/me").then(function (user) {
renderProfile(user);
}).catch(function (err) { showError("error-profile", err); });
}
/* 账号信息卡片:用户名 / 角色徽章 / 创建时间 / 须改密 / 启用状态 */
function profileCard(label, value, valueClassName) {
var card = el("div", null, "card");
var num = el("div", null, "num");
if (valueClassName) {
var span = el("span", value, valueClassName);
num.appendChild(span);
} else {
num.textContent = value;
}
card.appendChild(num);
card.appendChild(el("div", label, "label"));
return card;
}
function renderProfile(user) {
var container = document.getElementById("profile-cards");
clearChildren(container);
container.appendChild(profileCard("用户名", user.username || ""));
var roleBadgeClass = "role-badge" + (user.role === "admin" ? " role-admin" : "");
container.appendChild(profileCard("角色", user.role || "", roleBadgeClass));
container.appendChild(profileCard("创建时间", user.created_at || ""));
container.appendChild(profileCard("须改密", user.must_change_password ? "是" : "否"));
container.appendChild(profileCard("启用状态", user.enabled ? "启用" : "禁用"));
}
/* 强度提示:复用 Task 3 文案(<8 弱 / ≥8 中 / ≥12 强,纯文案) */
function updateProfileStrength() {
var pwd = document.getElementById("profile-new-password").value || "";
var level = "";
if (pwd.length > 0) {
if (pwd.length < 8) { level = "弱"; }
else if (pwd.length >= 12) { level = "强"; }
else { level = "中"; }
}
document.getElementById("profile-strength-hint").textContent = level ? "密码强度:" + level : "";
}
document.getElementById("profile-new-password").addEventListener("input", updateProfileStrength);
/* 个人中心改密表单:与顶栏改密弹窗同一端点 POST /auth/password
- 两次不一致本地拦截
- 旧密码错误 1005、新密码 <8 位 1001 由后端返回,统一进错误条
- 成功后:更新本地 user 状态(清除 must_change_password)、清空表单、刷新账号信息、成功提示 */
document.getElementById("profile-password-form").addEventListener("submit", function (event) {
event.preventDefault();
hideError("error-profile");
var newPwd = document.getElementById("profile-new-password").value;
if (newPwd !== document.getElementById("profile-confirm-password").value) {
showError("error-profile", { code: "VALIDATION", message: "两次输入的新密码不一致" });
return;
}
var submitBtn = document.getElementById("btn-profile-password-submit");
var origText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = "提交中…";
api("/api/v1/auth/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
old_password: document.getElementById("profile-old-password").value,
new_password: newPwd
})
}).then(function () {
/* 改密成功:更新本地 user 状态(清除 must_change_password 标志) */
var user = getStoredUser();
if (user && user.must_change_password) {
user.must_change_password = false;
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
/* 清空表单与强度提示 */
document.getElementById("profile-password-form").reset();
document.getElementById("profile-strength-hint").textContent = "";
/* 刷新账号信息展示 */
loadProfile();
alert("密码修改成功");
}).catch(function (err) {
showError("error-profile", err);
}).finally(function () {
submitBtn.disabled = false;
submitBtn.textContent = origText;
});
});
/* 个人中心退出按钮:复用顶栏 doLogout 逻辑 */
document.getElementById("btn-profile-logout").addEventListener("click", doLogout);
/* ---------- 通用 overlay 弹窗组件 ----------
openModal(id, title, contentBuilder):在 #modal-root 下动态构造 .modal-overlay>.modal-card
- 关闭路径:点遮罩 / 点取消按钮 / ESC(提交中 data-submitting=true 时全部禁用)
- 提交 loading 态:setModalSubmitting 禁用按钮 + 文案改「提交中…」
- 动态内容全 textContent,无 innerHTML */
function openModal(id, title, contentBuilder) {
closeModal(id);
var overlay = el("div", null, "modal-overlay");
overlay.id = "modal-" + id;
overlay.setAttribute("data-modal-id", id);
overlay.setAttribute("data-submitting", "false");
var card = el("div", null, "modal-card");
card.appendChild(el("h3", title, "modal-title"));
var content = el("div", null, "modal-content");
contentBuilder(content);
card.appendChild(content);
overlay.appendChild(card);
/* 点遮罩关闭(提交中不可关闭) */
overlay.addEventListener("click", function (e) {
if (e.target === overlay && overlay.getAttribute("data-submitting") !== "true") {
closeModal(id);
}
});
document.getElementById("modal-root").appendChild(overlay);
}
function closeModal(id) {
var existing = document.getElementById("modal-" + id);
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}
}
function isModalSubmitting(id) {
var overlay = document.getElementById("modal-" + id);
return !!(overlay && overlay.getAttribute("data-submitting") === "true");
}
/* 提交 loading 态:data-submitting 标记 + 按钮禁用 + 文案改「提交中…」;
失败回滚时恢复按钮原始文案与可点状态 */
function setModalSubmitting(id, submitting, submitBtn) {
var overlay = document.getElementById("modal-" + id);
if (!overlay) { return; }
overlay.setAttribute("data-submitting", submitting ? "true" : "false");
if (submitBtn) {
if (submitting) {
if (!submitBtn.getAttribute("data-original-text")) {
submitBtn.setAttribute("data-original-text", submitBtn.textContent);
}
submitBtn.disabled = true;
submitBtn.textContent = "提交中…";
} else {
submitBtn.disabled = false;
var orig = submitBtn.getAttribute("data-original-text");
submitBtn.textContent = orig || submitBtn.textContent;
}
}
}
function showModelError(id, message) {
var overlay = document.getElementById("modal-" + id);
if (!overlay) { return; }
var box = overlay.querySelector(".error-bar");
if (!box) { return; }
box.textContent = message || "";
box.classList.remove("hidden");
}
function hideModelError(id) {
var overlay = document.getElementById("modal-" + id);
if (!overlay) { return; }
var box = overlay.querySelector(".error-bar");
if (!box) { return; }
box.classList.add("hidden");
box.textContent = "";
}
/* ESC 关闭最顶层弹窗(提交中不可关闭) */
document.addEventListener("keydown", function (e) {
if (e.key !== "Escape") { return; }
var modals = document.querySelectorAll(".modal-overlay");
if (modals.length === 0) { return; }
var top = modals[modals.length - 1];
if (top.getAttribute("data-submitting") === "true") { return; }
var mid = top.getAttribute("data-modal-id");
if (mid) { closeModal(mid); }
});
/* 统一 API 封装:自动注入 Authorization: Bearer <token>(登录接口除外),code !== 0 抛错;
1005 未认证/凭证无效 → 清除本地凭证并回登录卡片;1006 权限不足 → 抛给调用方在错误条展示 */
function api(path, options) {
@@ -629,6 +873,7 @@ function activateSection(targetId) {
if (targetId === "section-categories") { loadCategories(); }
if (targetId === "section-users") { loadUsers(); }
if (targetId === "section-api-guide") { renderApiGuide(); }
if (targetId === "section-profile") { loadProfile(); }
}
}
@@ -814,18 +1059,72 @@ function renderNodes(panel, nodes) {
});
}
/* 删除文档弹窗:复用通用弹窗组件,输入 doc_id 匹配后才可提交;
与用户管理删除保持一致的二次确认体验(不再使用原生弹窗) */
function deleteDocument(docId, title) {
if (!confirm("确定删除文档「" + (title || docId) + "」(" + docId + ") 吗?该操作将删除四层集合中的全部数据,不可恢复。")) {
return;
}
hideError("error-docs");
api("/api/v1/documents/" + encodeURIComponent(docId), { method: "DELETE" }).then(function (data) {
document.getElementById("doc-detail").classList.add("hidden");
loadDocuments(true);
loadOverview();
var status = document.getElementById("docs-status");
status.textContent = "已删除 " + (data.deleted_total || 0) + " 条数据";
}).catch(function (err) { showError("error-docs", err); });
openModal("delete-doc", "删除文档", function (content) {
var warn = el("div", null, "fallback-flag");
warn.textContent = "此操作不可恢复,将删除四层集合中的全部数据";
content.appendChild(warn);
var metaRow = el("div", null, "modal-confirm-text");
metaRow.textContent = "文档:" + (title || docId) + "doc_id=" + docId + "";
content.appendChild(metaRow);
var confirmText = el("div", null, "modal-confirm-text");
confirmText.textContent = "请输入 doc_id " + docId + " 以确认";
content.appendChild(confirmText);
var field = el("div", null, "field");
var input = el("input");
input.type = "text";
input.id = "modal-delete-doc-input";
input.setAttribute("autocomplete", "off");
field.appendChild(input);
content.appendChild(field);
var errBox = el("div", null, "error-bar hidden");
errBox.id = "modal-delete-doc-error";
content.appendChild(errBox);
var actions = el("div", null, "modal-actions");
var submitBtn = el("button", "删除", "action danger");
submitBtn.type = "button";
submitBtn.disabled = true;
var cancelBtn = el("button", "取消", "action");
cancelBtn.type = "button";
cancelBtn.addEventListener("click", function () {
if (!isModalSubmitting("delete-doc")) { closeModal("delete-doc"); }
});
input.addEventListener("input", function () {
submitBtn.disabled = (input.value !== docId);
});
submitBtn.addEventListener("click", function () {
if (input.value !== docId) {
showModelError("delete-doc", "输入的 doc_id 不匹配");
return;
}
hideModelError("delete-doc");
setModalSubmitting("delete-doc", true, submitBtn);
api("/api/v1/documents/" + encodeURIComponent(docId), { method: "DELETE" }).then(function (data) {
closeModal("delete-doc");
document.getElementById("doc-detail").classList.add("hidden");
loadDocuments(true);
loadOverview();
var status = document.getElementById("docs-status");
status.textContent = "已删除 " + (data.deleted_total || 0) + " 条数据";
}).catch(function (err) {
showModelError("delete-doc", (err && err.message) ? err.message : String(err));
setModalSubmitting("delete-doc", false, submitBtn);
});
});
actions.appendChild(submitBtn);
actions.appendChild(cancelBtn);
content.appendChild(actions);
setTimeout(function () { input.focus(); }, 0);
});
}
/* ---------- 3. 文档入库(异步任务轮询) ---------- */
@@ -1125,6 +1424,9 @@ function mountUsersSection() {
document.getElementById("nav").appendChild(navBtn);
document.getElementById("btn-refresh-users").addEventListener("click", loadUsers);
document.getElementById("user-create-form").addEventListener("submit", createUser);
/* 搜索框 input + 角色筛选 change:仅本地重新过滤渲染,不发后端请求 */
document.getElementById("user-search").addEventListener("input", applyUsersFilter);
document.getElementById("user-role-filter").addEventListener("change", applyUsersFilter);
}
function unmountUsersSection() {
@@ -1141,33 +1443,139 @@ function loadUsers() {
}).catch(function (err) { showError("error-users", err); });
}
var usersCache = [];
function renderUsers(users) {
usersCache = users || [];
applyUsersFilter();
}
/* 列表搜索筛选:按用户名(包含、不区分大小写)+ 角色下拉过滤,仅本地重新渲染不发请求 */
function applyUsersFilter() {
var tbody = document.getElementById("users-tbody");
clearChildren(tbody);
users.forEach(function (user) {
var tr = el("tr");
tr.appendChild(el("td", user.username));
var roleTd = el("td");
roleTd.appendChild(el("span", user.role, "role-badge" + (user.role === "admin" ? " role-admin" : "")));
tr.appendChild(roleTd);
tr.appendChild(el("td", user.must_change_password ? "是" : "否"));
tr.appendChild(el("td", user.created_at || ""));
var opsTd = el("td");
var resetBtn = el("button", "重置密码", "action");
resetBtn.type = "button";
resetBtn.addEventListener("click", function () { resetUserPassword(user.username); });
var deleteBtn = el("button", "删除", "action danger");
deleteBtn.type = "button";
deleteBtn.addEventListener("click", function () { deleteUser(user.username, user.role); });
opsTd.appendChild(resetBtn);
opsTd.appendChild(deleteBtn);
tr.appendChild(opsTd);
tbody.appendChild(tr);
var keyword = (document.getElementById("user-search").value || "").trim().toLowerCase();
var roleFilter = document.getElementById("user-role-filter").value;
var currentUser = getStoredUser();
usersCache.forEach(function (user) {
if (keyword && (user.username || "").toLowerCase().indexOf(keyword) === -1) { return; }
if (roleFilter !== "all" && user.role !== roleFilter) { return; }
tbody.appendChild(buildUserRow(user, currentUser));
});
}
/* 单行渲染:行内角色 select + 保存按钮、启用开关;当前登录 admin 自身行禁用防自锁降级 */
function buildUserRow(user, currentUser) {
var isSelf = !!(currentUser && currentUser.username === user.username);
var tr = el("tr");
tr.appendChild(el("td", user.username));
/* 角色:行内 select + 保存按钮(自身行 select disabled + 无保存按钮) */
var roleTd = el("td");
var roleSelect = el("select", null, "user-role-select");
var adminOpt = el("option", "admin");
adminOpt.value = "admin";
var userOpt = el("option", "user");
userOpt.value = "user";
roleSelect.appendChild(userOpt);
roleSelect.appendChild(adminOpt);
roleSelect.value = user.role;
if (isSelf) {
roleSelect.disabled = true;
} else {
var saveRoleBtn = el("button", "保存", "action user-role-save");
saveRoleBtn.type = "button";
saveRoleBtn.disabled = true;
saveRoleBtn.addEventListener("click", function () {
updateUserRole(user.username, roleSelect.value, saveRoleBtn);
});
roleSelect.addEventListener("change", function () {
saveRoleBtn.disabled = (roleSelect.value === user.role);
});
roleTd.appendChild(saveRoleBtn);
}
roleTd.appendChild(roleSelect);
tr.appendChild(roleTd);
tr.appendChild(el("td", user.must_change_password ? "是" : "否"));
tr.appendChild(el("td", user.created_at || ""));
/* 启用开关:当前状态文字 + 样式区分;自身行 disabled 不可点 */
var enabledTd = el("td");
var isEnabled = !!user.enabled;
var toggleBtn = el(
"button",
isEnabled ? "启用" : "禁用",
"action user-enabled-toggle" + (isEnabled ? "" : " danger")
);
toggleBtn.type = "button";
if (isSelf) {
toggleBtn.disabled = true;
toggleBtn.title = "不能禁用当前登录账号";
} else {
toggleBtn.addEventListener("click", function () {
toggleUserEnabled(user.username, isEnabled);
});
}
enabledTd.appendChild(toggleBtn);
tr.appendChild(enabledTd);
/* 操作:重置密码 + 删除(均走通用弹窗组件,无 prompt/confirm 调用) */
var opsTd = el("td");
var resetBtn = el("button", "重置密码", "action");
resetBtn.type = "button";
resetBtn.addEventListener("click", function () { resetUserPassword(user.username); });
var deleteBtn = el("button", "删除", "action danger");
deleteBtn.type = "button";
deleteBtn.addEventListener("click", function () { deleteUser(user.username, user.role); });
opsTd.appendChild(resetBtn);
opsTd.appendChild(deleteBtn);
tr.appendChild(opsTd);
return tr;
}
function showUsersStatus(text) {
var span = document.getElementById("users-status");
if (!span) { return; }
span.textContent = text || "";
if (text) {
setTimeout(function () {
if (span.textContent === text) { span.textContent = ""; }
}, 2500);
}
}
/* 行内角色保存:PATCH {role},成功刷新列表 + 状态提示;失败错误条 */
function updateUserRole(username, newRole, btn) {
hideError("error-users");
if (btn) { btn.disabled = true; }
api("/api/v1/auth/users/" + encodeURIComponent(username), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: newRole })
}).then(function () {
showUsersStatus("已更新 " + username + " 的角色");
loadUsers();
}).catch(function (err) {
showError("error-users", err);
if (btn) { btn.disabled = false; }
});
}
/* 行内启用/禁用开关:PATCH {enabled: !当前},成功刷新列表 + 状态提示;失败错误条 */
function toggleUserEnabled(username, currentEnabled) {
hideError("error-users");
api("/api/v1/auth/users/" + encodeURIComponent(username), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: !currentEnabled })
}).then(function () {
showUsersStatus("已" + (!currentEnabled ? "启用" : "禁用") + " 用户 " + username);
loadUsers();
}).catch(function (err) { showError("error-users", err); });
}
function createUser(event) {
event.preventDefault();
hideError("error-users");
@@ -1186,31 +1594,158 @@ function createUser(event) {
}).catch(function (err) { showError("error-users", err); });
}
/* 重置密码弹窗:新密码 + 确认密码 + 强度提示(<8 弱 / ≥8 中 / ≥12 强,纯文案)+
两次不一致本地拦截;提交按钮 loading 态;成功关闭弹窗 + 刷新列表 + 状态提示 */
function resetUserPassword(username) {
var newPwd = prompt("为用户「" + username + "」设置新密码(至少 8 位):");
if (newPwd === null) { return; }
if (!newPwd) {
showError("error-users", { code: "VALIDATION", message: "新密码不能为空" });
return;
}
hideError("error-users");
api("/api/v1/auth/users/" + encodeURIComponent(username) + "/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_password: newPwd })
}).then(function () {
loadUsers();
}).catch(function (err) { showError("error-users", err); });
openModal("reset-password", "重置用户「" + username + "」密码", function (content) {
var newField = el("div", null, "field");
newField.appendChild(el("label", "新密码(至少 8 位)"));
var newInput = el("input");
newInput.type = "password";
newInput.id = "modal-reset-new";
newInput.setAttribute("autocomplete", "new-password");
newField.appendChild(newInput);
content.appendChild(newField);
var confirmField = el("div", null, "field");
confirmField.appendChild(el("label", "确认新密码"));
var confirmInput = el("input");
confirmInput.type = "password";
confirmInput.id = "modal-reset-confirm";
confirmInput.setAttribute("autocomplete", "new-password");
confirmField.appendChild(confirmInput);
content.appendChild(confirmField);
var hint = el("div", null, "modal-strength-hint muted");
hint.id = "modal-reset-strength";
content.appendChild(hint);
var errBox = el("div", null, "error-bar hidden");
errBox.id = "modal-reset-error";
content.appendChild(errBox);
/* 强度提示:纯文案,<8 弱 / ≥8 中 / ≥12 强 */
function updateStrength() {
var pwd = newInput.value || "";
var level = "";
if (pwd.length > 0) {
if (pwd.length < 8) { level = "弱"; }
else if (pwd.length >= 12) { level = "强"; }
else { level = "中"; }
}
hint.textContent = level ? "密码强度:" + level : "";
}
newInput.addEventListener("input", updateStrength);
var actions = el("div", null, "modal-actions");
var submitBtn = el("button", "确认重置", "primary");
submitBtn.type = "button";
var cancelBtn = el("button", "取消", "action");
cancelBtn.type = "button";
cancelBtn.addEventListener("click", function () {
if (!isModalSubmitting("reset-password")) { closeModal("reset-password"); }
});
submitBtn.addEventListener("click", function () {
var newPwd = newInput.value;
var confirmPwd = confirmInput.value;
if (!newPwd) {
showModelError("reset-password", "新密码不能为空");
return;
}
/* 两次不一致本地拦截 */
if (newPwd !== confirmPwd) {
showModelError("reset-password", "两次输入的新密码不一致");
return;
}
hideModelError("reset-password");
setModalSubmitting("reset-password", true, submitBtn);
api("/api/v1/auth/users/" + encodeURIComponent(username) + "/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_password: newPwd })
}).then(function () {
closeModal("reset-password");
loadUsers();
showUsersStatus("已重置 " + username + " 的密码");
}).catch(function (err) {
showModelError("reset-password", (err && err.message) ? err.message : String(err));
setModalSubmitting("reset-password", false, submitBtn);
});
});
actions.appendChild(submitBtn);
actions.appendChild(cancelBtn);
content.appendChild(actions);
setTimeout(function () { newInput.focus(); }, 0);
});
}
/* 删除用户弹窗:警告文案 + 「请输入用户名 {username} 以确认」+ 文本输入框;
输入 !== username 时删除按钮禁用;匹配后启用;提交按钮 loading 态 */
function deleteUser(username, role) {
if (!confirm("确定删除用户「" + username + "」(角色 " + role + ")吗?该操作不可恢复。")) {
return;
}
hideError("error-users");
api("/api/v1/auth/users/" + encodeURIComponent(username), { method: "DELETE" }).then(function () {
loadUsers();
}).catch(function (err) { showError("error-users", err); });
openModal("delete-user", "删除用户", function (content) {
var warn = el("div", null, "fallback-flag");
warn.textContent = "此操作不可恢复,将清除该用户及其全部会话";
content.appendChild(warn);
var metaRow = el("div", null, "modal-confirm-text");
metaRow.textContent = "目标用户:" + username + "(角色 " + role + "";
content.appendChild(metaRow);
var confirmText = el("div", null, "modal-confirm-text");
confirmText.textContent = "请输入用户名 " + username + " 以确认";
content.appendChild(confirmText);
var field = el("div", null, "field");
var input = el("input");
input.type = "text";
input.id = "modal-delete-input";
input.setAttribute("autocomplete", "off");
field.appendChild(input);
content.appendChild(field);
var errBox = el("div", null, "error-bar hidden");
errBox.id = "modal-delete-error";
content.appendChild(errBox);
var actions = el("div", null, "modal-actions");
var submitBtn = el("button", "删除", "action danger");
submitBtn.type = "button";
/* 初始禁用:输入匹配后才启用 */
submitBtn.disabled = true;
var cancelBtn = el("button", "取消", "action");
cancelBtn.type = "button";
cancelBtn.addEventListener("click", function () {
if (!isModalSubmitting("delete-user")) { closeModal("delete-user"); }
});
/* 输入 !== username 时删除按钮禁用;匹配后启用 */
input.addEventListener("input", function () {
submitBtn.disabled = (input.value !== username);
});
submitBtn.addEventListener("click", function () {
if (input.value !== username) {
showModelError("delete-user", "输入的用户名不匹配");
return;
}
hideModelError("delete-user");
setModalSubmitting("delete-user", true, submitBtn);
api("/api/v1/auth/users/" + encodeURIComponent(username), { method: "DELETE" }).then(function () {
closeModal("delete-user");
loadUsers();
showUsersStatus("已删除用户 " + username);
}).catch(function (err) {
showModelError("delete-user", (err && err.message) ? err.message : String(err));
setModalSubmitting("delete-user", false, submitBtn);
});
});
actions.appendChild(submitBtn);
actions.appendChild(cancelBtn);
content.appendChild(actions);
setTimeout(function () { input.focus(); }, 0);
});
}
/* ---------- 7. API 指南 ---------- */