267 lines
9.2 KiB
TypeScript
267 lines
9.2 KiB
TypeScript
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) : "";
|
|
}
|