diff --git a/src/api/admin-users.ts b/src/api/admin-users.ts index 9b8a9c2..0ed5812 100644 --- a/src/api/admin-users.ts +++ b/src/api/admin-users.ts @@ -4,9 +4,13 @@ import { API_V1_PREFIX } from "./paths"; import type { AdminPermissionCatalogData, + AdminUserCreatePayload, + AdminUserDeleteResult, AdminUserPermissionListData, + AdminUserPermissionRow, AdminUserPermissionSyncData, AdminUserRoleSyncData, + AdminUserUpdatePayload, } from "@/types/api/admin-user"; const A = `${API_V1_PREFIX}/admin`; @@ -25,6 +29,25 @@ export async function getAdminUserPermissionCatalog(): Promise(`${A}/admin-user-permission-catalog`); } +export async function getAdminUser(adminUserId: number): Promise { + return adminRequest.get(`${A}/admin-users/${adminUserId}`); +} + +export async function postAdminUser(body: AdminUserCreatePayload): Promise { + return adminRequest.post(`${A}/admin-users`, body); +} + +export async function putAdminUser( + adminUserId: number, + body: AdminUserUpdatePayload, +): Promise { + return adminRequest.put(`${A}/admin-users/${adminUserId}`, body); +} + +export async function deleteAdminUser(adminUserId: number): Promise { + return adminRequest.delete(`${A}/admin-users/${adminUserId}`); +} + export async function putAdminUserPermissions( adminUserId: number, permissionSlugs: string[], diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index e4f57cc..789cb68 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -5,8 +5,11 @@ import { ChevronDown } from "lucide-react"; import { toast } from "sonner"; import { + deleteAdminUser, getAdminUserPermissionCatalog, getAdminUsers, + postAdminUser, + putAdminUser, putAdminUserPermissions, putAdminUserRoles, } from "@/api/admin-users"; @@ -33,9 +36,11 @@ import { } from "@/components/ui/table"; import { LotteryApiBizError } from "@/types/api/errors"; import { cn } from "@/lib/utils"; +import { useAdminProfile } from "@/stores/admin-session"; import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index"; export function AdminUsersConsole(): React.ReactElement { + const profile = useAdminProfile(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(25); const [keyword, setKeyword] = useState(""); @@ -57,6 +62,20 @@ export function AdminUsersConsole(): React.ReactElement { /** `false` = 折叠;缺省为展开 */ const [directMenuExpanded, setDirectMenuExpanded] = useState>({}); + const [accountOpen, setAccountOpen] = useState(false); + const [accountMode, setAccountMode] = useState<"create" | "edit">("create"); + const [accountSaving, setAccountSaving] = useState(false); + const [editingAccountId, setEditingAccountId] = useState(null); + const [formUsername, setFormUsername] = useState(""); + const [formNickname, setFormNickname] = useState(""); + const [formEmail, setFormEmail] = useState(""); + const [formPassword, setFormPassword] = useState(""); + const [formStatus, setFormStatus] = useState(0); + const [formCreateRoles, setFormCreateRoles] = useState([]); + + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteBusy, setDeleteBusy] = useState(false); + const selectedUser = useMemo( () => items.find((u) => u.id === selectedId) ?? null, [items, selectedId], @@ -77,6 +96,132 @@ export function AdminUsersConsole(): React.ReactElement { } } + function openCreateAccount(): void { + setAccountMode("create"); + setEditingAccountId(null); + setFormUsername(""); + setFormNickname(""); + setFormEmail(""); + setFormPassword(""); + setFormStatus(0); + setFormCreateRoles([]); + setAccountOpen(true); + } + + function openEditAccount(row: AdminUserPermissionRow): void { + setAccountMode("edit"); + setEditingAccountId(row.id); + setFormUsername(row.username); + setFormNickname(row.nickname); + setFormEmail(row.email ?? ""); + setFormPassword(""); + setFormStatus(row.status); + setAccountOpen(true); + } + + function handleAccountDialogOpenChange(open: boolean): void { + setAccountOpen(open); + if (!open) { + setEditingAccountId(null); + } + } + + async function submitAccount(): Promise { + const nick = formNickname.trim(); + if (nick === "") { + toast.error("请填写昵称"); + return; + } + if (accountMode === "edit" && formPassword !== "" && formPassword.length < 8) { + toast.error("新密码至少 8 位"); + return; + } + + if (accountMode === "create" && formCreateRoles.length === 0) { + toast.error("请至少选择一个角色"); + return; + } + + setAccountSaving(true); + try { + if (accountMode === "create") { + const u = formUsername.trim(); + if (u === "") { + toast.error("请填写登录账号"); + return; + } + if (formPassword.length < 8) { + toast.error("密码至少 8 位"); + return; + } + const created = await postAdminUser({ + username: u.toLowerCase(), + nickname: nick, + email: formEmail.trim() === "" ? null : formEmail.trim(), + password: formPassword, + status: formStatus, + role_slugs: formCreateRoles, + }); + setItems((prev) => [created, ...prev]); + setTotal((t) => t + 1); + toast.success(`已创建管理员 ${created.username}`); + handleAccountDialogOpenChange(false); + } else { + const id = editingAccountId; + if (id === null) { + return; + } + const body: { + nickname: string; + email: string | null; + status: number; + password?: string; + } = { + nickname: nick, + email: formEmail.trim() === "" ? null : formEmail.trim(), + status: formStatus, + }; + if (formPassword.trim() !== "") { + body.password = formPassword.trim(); + } + const updated = await putAdminUser(id, body); + setItems((prev) => prev.map((row) => (row.id === updated.id ? updated : row))); + toast.success(`已更新 ${updated.username}`); + handleAccountDialogOpenChange(false); + } + } catch (e) { + const msg = e instanceof LotteryApiBizError ? e.message : "保存账号失败"; + toast.error(msg); + } finally { + setAccountSaving(false); + } + } + + async function confirmDelete(): Promise { + if (!deleteTarget) { + return; + } + setDeleteBusy(true); + try { + await deleteAdminUser(deleteTarget.id); + setItems((prev) => prev.filter((r) => r.id !== deleteTarget.id)); + setTotal((t) => Math.max(0, t - 1)); + toast.success(`已删除 ${deleteTarget.username}`); + setDeleteTarget(null); + } catch (e) { + const msg = e instanceof LotteryApiBizError ? e.message : "删除失败"; + toast.error(msg); + } finally { + setDeleteBusy(false); + } + } + + const selectClassName = cn( + "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors", + "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm", + "dark:bg-input/30 disabled:cursor-not-allowed disabled:opacity-50", + ); + const directPermissionGroups = useMemo(() => { const g = catalog?.permission_menu_groups; if (g && g.length > 0) { @@ -100,6 +245,15 @@ export function AdminUsersConsole(): React.ReactElement { }); } + function toggleFormCreateRole(slug: string, checked: boolean): void { + setFormCreateRoles((prev) => { + if (checked) { + return Array.from(new Set([...prev, slug])).sort(); + } + return prev.filter((s) => s !== slug); + }); + } + const load = useCallback(async () => { setLoading(true); setErr(null); @@ -213,8 +367,11 @@ export function AdminUsersConsole(): React.ReactElement {
-
+
管理员用户列表 +
ID 账号 昵称 + 状态 角色 直接权限 有效权限 - 操作 + 操作 {items.length === 0 ? ( - + 暂无数据 @@ -278,6 +436,17 @@ export function AdminUsersConsole(): React.ReactElement {
{row.nickname} + + {row.status === 0 ? ( + + 启用 + + ) : ( + + 禁用 + + )} +
{row.roles.length === 0 ? ( @@ -294,16 +463,40 @@ export function AdminUsersConsole(): React.ReactElement { {row.direct_permissions.length} {row.effective_permissions.length} - +
+ + + +
)) @@ -484,6 +677,149 @@ export function AdminUsersConsole(): React.ReactElement {
+ + + + + {accountMode === "create" ? "新建管理员" : "编辑账号"} + + {accountMode === "create" + ? "须为账号指定至少一个默认站点角色。登录账号仅可使用字母、数字、点、下划线与连字符,保存后为小写。" + : "登录账号不可修改。留空密码表示不修改。"} + + +
+
+
登录账号
+ setFormUsername(e.target.value)} + /> +
+
+
昵称
+ setFormNickname(e.target.value)} + /> +
+
+
邮箱(可选)
+ setFormEmail(e.target.value)} + /> +
+
+
+ 密码{accountMode === "edit" ? "(可选)" : ""} +
+ setFormPassword(e.target.value)} + /> +
+ {accountMode === "create" ? ( +
+
角色(默认站点,至少一项)
+

+ 创建后即可在「权限」中继续调整角色或直接授权。 +

+
+ {(catalog?.roles ?? []).length === 0 ? ( +

+ 暂无角色数据,请等待列表加载完成后重试。 +

+ ) : ( + (catalog?.roles ?? []).map((r) => { + const checked = formCreateRoles.includes(r.slug); + return ( + + ); + }) + )} +
+
+ ) : null} +
+
状态
+ +
+
+
+ + +
+
+
+ + !open && setDeleteTarget(null)}> + + + 确认删除 + + {deleteTarget ? ( + <> + 确定删除管理员{" "} + {deleteTarget.username} + ?此操作不可撤销。 + + ) : null} + + +
+ + +
+
+
); } diff --git a/src/types/api/admin-user.ts b/src/types/api/admin-user.ts index 3034c8e..697dfe4 100644 --- a/src/types/api/admin-user.ts +++ b/src/types/api/admin-user.ts @@ -47,3 +47,25 @@ export type AdminUserPermissionSyncData = { /** 与 {@link AdminUserPermissionSyncData} 相同(角色同步返回体一致) */ export type AdminUserRoleSyncData = AdminUserPermissionSyncData; + +export type AdminUserCreatePayload = { + username: string; + nickname: string; + email?: string | null; + password: string; + status?: number; + /** 默认站点角色,至少一项 */ + role_slugs: string[]; +}; + +export type AdminUserUpdatePayload = { + nickname?: string; + email?: string | null; + password?: string; + status?: number; +}; + +export type AdminUserDeleteResult = { + deleted: boolean; + id: number; +};