initial commit
This commit is contained in:
266
web/src/pages/app/modules/users.ts
Normal file
266
web/src/pages/app/modules/users.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { api } from "../../../services/api";
|
||||
import { getCurrentUser } from "../../../services/authState";
|
||||
import type { ManagedUser, UserListResponse, UserRole } from "../../../types";
|
||||
import { confirmDialog, formDialog, notify, notifyError } from "../../../ui/dialogs";
|
||||
import { escapeHtml } from "../../../utils/format";
|
||||
|
||||
type UserFormMode = "create" | "edit";
|
||||
|
||||
let users: ManagedUser[] = [];
|
||||
let editingUserId: number | null = null;
|
||||
|
||||
export async function openUserManager() {
|
||||
await refreshUsers();
|
||||
editingUserId = null;
|
||||
await formDialog<boolean>({
|
||||
title: "人员权限管理",
|
||||
width: 980,
|
||||
height: 560,
|
||||
body: `
|
||||
<div class="user-manager">
|
||||
<header class="user-manager-head">
|
||||
<div>
|
||||
<strong>账号列表</strong>
|
||||
<span>维护人员角色、启停状态和授权到期时间</span>
|
||||
</div>
|
||||
<button id="addUserBtn" class="primary-btn" type="button">新增人员</button>
|
||||
</header>
|
||||
<div class="user-manager-body">
|
||||
<div class="user-table">
|
||||
<div class="user-table-head">
|
||||
<span>用户</span>
|
||||
<span>角色</span>
|
||||
<span>状态</span>
|
||||
<span>授权到期</span>
|
||||
<span>更新时间</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
<div id="userTableBody" class="user-table-body">
|
||||
${userRowsHtml()}
|
||||
</div>
|
||||
</div>
|
||||
<aside id="userEditorHost" class="user-editor-panel">
|
||||
${userEditorEmptyHtml()}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
onOpen: bindUserManagerEvents,
|
||||
onSubmit: () => true
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshUsers() {
|
||||
const result = await api<UserListResponse>("/api/users");
|
||||
users = result.users;
|
||||
}
|
||||
|
||||
function userRowsHtml() {
|
||||
return users.map(userRowHtml).join("") || `<div class="user-empty">暂无人员</div>`;
|
||||
}
|
||||
|
||||
function userRowHtml(user: ManagedUser) {
|
||||
const currentUser = getCurrentUser();
|
||||
const locked = currentUser?.id === user.id;
|
||||
const expired = isExpired(user.expires_at);
|
||||
const statusClass = user.enabled ? (expired ? "is-expired" : "is-enabled") : "is-disabled";
|
||||
const statusText = user.enabled ? (expired ? "已过期" : "启用") : "禁用";
|
||||
return `
|
||||
<div class="user-row" data-id="${user.id}">
|
||||
<span class="user-name">
|
||||
<strong>${escapeHtml(user.username)}</strong>
|
||||
${locked ? `<em>当前账号</em>` : ""}
|
||||
</span>
|
||||
<span><i class="role-badge ${user.role === "admin" ? "is-admin" : ""}">${roleText(user.role)}</i></span>
|
||||
<span><i class="status-badge ${statusClass}">${statusText}</i></span>
|
||||
<span>${formatDate(user.expires_at) || "长期有效"}</span>
|
||||
<span>${formatDate(user.updated_at)}</span>
|
||||
<span class="user-row-actions">
|
||||
<button type="button" data-action="edit" data-id="${user.id}">编辑</button>
|
||||
<button class="danger-text-btn" type="button" data-action="delete" data-id="${user.id}" ${locked ? "disabled" : ""}>删除</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindUserManagerEvents() {
|
||||
document.querySelector("#addUserBtn")?.addEventListener("click", () => {
|
||||
renderUserEditor("create");
|
||||
});
|
||||
bindUserRowEvents();
|
||||
bindUserEditorEvents();
|
||||
}
|
||||
|
||||
function bindUserRowEvents() {
|
||||
document.querySelectorAll<HTMLButtonElement>(".user-table-body button").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
try {
|
||||
const id = Number(button.dataset.id);
|
||||
if (button.dataset.action === "edit") renderUserEditor("edit", id);
|
||||
if (button.dataset.action === "delete") await deleteUser(id);
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderUserEditor(mode: UserFormMode, id?: number) {
|
||||
editingUserId = mode === "edit" ? id ?? null : null;
|
||||
const user = id ? users.find((item) => item.id === id) : undefined;
|
||||
const isCreate = mode === "create";
|
||||
const host = document.querySelector<HTMLElement>("#userEditorHost");
|
||||
if (!host) return;
|
||||
host.innerHTML = `
|
||||
<div class="user-editor-title">
|
||||
<strong>${isCreate ? "新增人员" : "编辑权限"}</strong>
|
||||
<span>${isCreate ? "创建账号并设置初始权限" : "调整角色、状态和授权期限"}</span>
|
||||
</div>
|
||||
<form id="userEditorForm" class="popup-form user-editor-form" data-mode="${mode}">
|
||||
${isCreate ? `
|
||||
<label>
|
||||
<span>用户名</span>
|
||||
<input name="username" autocomplete="off" />
|
||||
</label>
|
||||
` : `
|
||||
<label>
|
||||
<span>用户名</span>
|
||||
<input value="${escapeHtml(user?.username ?? "")}" disabled />
|
||||
</label>
|
||||
`}
|
||||
<div class="popup-form-grid">
|
||||
<label>
|
||||
<span>角色</span>
|
||||
<select name="role">
|
||||
<option value="user" ${user?.role === "user" ? "selected" : ""}>普通用户</option>
|
||||
<option value="admin" ${user?.role === "admin" ? "selected" : ""}>管理员</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>状态</span>
|
||||
<select name="enabled">
|
||||
<option value="true" ${user?.enabled === false ? "" : "selected"}>启用</option>
|
||||
<option value="false" ${user?.enabled === false ? "selected" : ""}>禁用</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>授权到期</span>
|
||||
<input name="expiresAt" type="date" value="${dateInputValue(user?.expires_at)}" />
|
||||
</label>
|
||||
<label>
|
||||
<span>${isCreate ? "初始密码" : "重置密码"}</span>
|
||||
<input name="password" type="password" autocomplete="new-password" placeholder="${isCreate ? "至少 6 位" : "不填写则保持原密码"}" />
|
||||
</label>
|
||||
<div class="user-editor-actions">
|
||||
<button id="cancelUserEditBtn" class="ghost-btn" type="button">取消</button>
|
||||
<button id="saveUserBtn" class="primary-btn" type="button">${isCreate ? "新增" : "保存"}</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
bindUserEditorEvents();
|
||||
}
|
||||
|
||||
function userEditorEmptyHtml() {
|
||||
return `
|
||||
<div class="user-editor-empty">
|
||||
<strong>选择人员</strong>
|
||||
<span>点击新增或编辑后,在这里维护账号权限。</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindUserEditorEvents() {
|
||||
document.querySelector("#saveUserBtn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await saveUserEditor();
|
||||
} catch (error) {
|
||||
notifyError(error);
|
||||
}
|
||||
});
|
||||
document.querySelector("#cancelUserEditBtn")?.addEventListener("click", () => {
|
||||
editingUserId = null;
|
||||
const host = document.querySelector<HTMLElement>("#userEditorHost");
|
||||
if (host) host.innerHTML = userEditorEmptyHtml();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveUserEditor() {
|
||||
const formElement = document.querySelector<HTMLFormElement>("#userEditorForm");
|
||||
if (!formElement) return;
|
||||
const form = new FormData(formElement);
|
||||
const isCreate = formElement.dataset.mode === "create";
|
||||
const password = String(form.get("password") ?? "").trim();
|
||||
const result = {
|
||||
username: String(form.get("username") ?? "").trim(),
|
||||
password,
|
||||
role: String(form.get("role") ?? "user") as UserRole,
|
||||
enabled: String(form.get("enabled") ?? "true") === "true",
|
||||
expiresAt: String(form.get("expiresAt") ?? "") || null
|
||||
};
|
||||
if (isCreate && !result.username) throw new Error("用户名不能为空");
|
||||
if (isCreate && result.password.length < 6) throw new Error("初始密码至少 6 位");
|
||||
if (!isCreate && result.password && result.password.length < 6) throw new Error("重置密码至少 6 位");
|
||||
if (isCreate) {
|
||||
await api("/api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(result)
|
||||
});
|
||||
notify("新增成功");
|
||||
} else {
|
||||
if (!editingUserId) throw new Error("请选择要编辑的用户");
|
||||
await api(`/api/users/${editingUserId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
role: result.role,
|
||||
enabled: result.enabled,
|
||||
expiresAt: result.expiresAt,
|
||||
...(result.password ? { password: result.password } : {})
|
||||
})
|
||||
});
|
||||
notify("保存成功");
|
||||
}
|
||||
await rerenderUserRows();
|
||||
if (isCreate) {
|
||||
const host = document.querySelector<HTMLElement>("#userEditorHost");
|
||||
if (host) host.innerHTML = userEditorEmptyHtml();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id: number) {
|
||||
const user = users.find((item) => item.id === id);
|
||||
if (!user) return;
|
||||
const confirmed = await confirmDialog(`确认删除用户「${user.username}」?`);
|
||||
if (!confirmed) return;
|
||||
await api(`/api/users/${id}`, { method: "DELETE" });
|
||||
notify("删除成功");
|
||||
await rerenderUserRows();
|
||||
}
|
||||
|
||||
async function rerenderUserRows() {
|
||||
await refreshUsers();
|
||||
const body = document.querySelector<HTMLDivElement>("#userTableBody");
|
||||
if (!body) return;
|
||||
body.innerHTML = userRowsHtml();
|
||||
bindUserRowEvents();
|
||||
if (editingUserId) {
|
||||
renderUserEditor("edit", editingUserId);
|
||||
}
|
||||
}
|
||||
|
||||
function roleText(role: UserRole) {
|
||||
return role === "admin" ? "管理员" : "普通用户";
|
||||
}
|
||||
|
||||
function isExpired(value: string | null) {
|
||||
return Boolean(value && new Date(value).getTime() < Date.now());
|
||||
}
|
||||
|
||||
function formatDate(value: string | null) {
|
||||
if (!value) return "";
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
|
||||
function dateInputValue(value: string | null | undefined) {
|
||||
return value ? value.slice(0, 10) : "";
|
||||
}
|
||||
Reference in New Issue
Block a user