feat: 添加管理员用户管理功能,包括创建、更新、删除用户的API和界面支持
This commit is contained in:
@@ -4,9 +4,13 @@ import { API_V1_PREFIX } from "./paths";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AdminPermissionCatalogData,
|
AdminPermissionCatalogData,
|
||||||
|
AdminUserCreatePayload,
|
||||||
|
AdminUserDeleteResult,
|
||||||
AdminUserPermissionListData,
|
AdminUserPermissionListData,
|
||||||
|
AdminUserPermissionRow,
|
||||||
AdminUserPermissionSyncData,
|
AdminUserPermissionSyncData,
|
||||||
AdminUserRoleSyncData,
|
AdminUserRoleSyncData,
|
||||||
|
AdminUserUpdatePayload,
|
||||||
} from "@/types/api/admin-user";
|
} from "@/types/api/admin-user";
|
||||||
|
|
||||||
const A = `${API_V1_PREFIX}/admin`;
|
const A = `${API_V1_PREFIX}/admin`;
|
||||||
@@ -25,6 +29,25 @@ export async function getAdminUserPermissionCatalog(): Promise<AdminPermissionCa
|
|||||||
return adminRequest.get<AdminPermissionCatalogData>(`${A}/admin-user-permission-catalog`);
|
return adminRequest.get<AdminPermissionCatalogData>(`${A}/admin-user-permission-catalog`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAdminUser(adminUserId: number): Promise<AdminUserPermissionRow> {
|
||||||
|
return adminRequest.get<AdminUserPermissionRow>(`${A}/admin-users/${adminUserId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminUser(body: AdminUserCreatePayload): Promise<AdminUserPermissionRow> {
|
||||||
|
return adminRequest.post<AdminUserPermissionRow>(`${A}/admin-users`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putAdminUser(
|
||||||
|
adminUserId: number,
|
||||||
|
body: AdminUserUpdatePayload,
|
||||||
|
): Promise<AdminUserPermissionRow> {
|
||||||
|
return adminRequest.put<AdminUserPermissionRow>(`${A}/admin-users/${adminUserId}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminUser(adminUserId: number): Promise<AdminUserDeleteResult> {
|
||||||
|
return adminRequest.delete<AdminUserDeleteResult>(`${A}/admin-users/${adminUserId}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function putAdminUserPermissions(
|
export async function putAdminUserPermissions(
|
||||||
adminUserId: number,
|
adminUserId: number,
|
||||||
permissionSlugs: string[],
|
permissionSlugs: string[],
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import { ChevronDown } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
deleteAdminUser,
|
||||||
getAdminUserPermissionCatalog,
|
getAdminUserPermissionCatalog,
|
||||||
getAdminUsers,
|
getAdminUsers,
|
||||||
|
postAdminUser,
|
||||||
|
putAdminUser,
|
||||||
putAdminUserPermissions,
|
putAdminUserPermissions,
|
||||||
putAdminUserRoles,
|
putAdminUserRoles,
|
||||||
} from "@/api/admin-users";
|
} from "@/api/admin-users";
|
||||||
@@ -33,9 +36,11 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index";
|
import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index";
|
||||||
|
|
||||||
export function AdminUsersConsole(): React.ReactElement {
|
export function AdminUsersConsole(): React.ReactElement {
|
||||||
|
const profile = useAdminProfile();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(25);
|
const [perPage, setPerPage] = useState(25);
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
@@ -57,6 +62,20 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
/** `false` = 折叠;缺省为展开 */
|
/** `false` = 折叠;缺省为展开 */
|
||||||
const [directMenuExpanded, setDirectMenuExpanded] = useState<Record<string, boolean>>({});
|
const [directMenuExpanded, setDirectMenuExpanded] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const [accountOpen, setAccountOpen] = useState(false);
|
||||||
|
const [accountMode, setAccountMode] = useState<"create" | "edit">("create");
|
||||||
|
const [accountSaving, setAccountSaving] = useState(false);
|
||||||
|
const [editingAccountId, setEditingAccountId] = useState<number | null>(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<string[]>([]);
|
||||||
|
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<AdminUserPermissionRow | null>(null);
|
||||||
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||||
|
|
||||||
const selectedUser = useMemo(
|
const selectedUser = useMemo(
|
||||||
() => items.find((u) => u.id === selectedId) ?? null,
|
() => items.find((u) => u.id === selectedId) ?? null,
|
||||||
[items, selectedId],
|
[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<void> {
|
||||||
|
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<void> {
|
||||||
|
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 directPermissionGroups = useMemo(() => {
|
||||||
const g = catalog?.permission_menu_groups;
|
const g = catalog?.permission_menu_groups;
|
||||||
if (g && g.length > 0) {
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
@@ -213,8 +367,11 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
<div className="flex w-full max-w-none flex-col gap-6">
|
<div className="flex w-full max-w-none flex-col gap-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||||
<CardTitle>管理员用户列表</CardTitle>
|
<CardTitle>管理员用户列表</CardTitle>
|
||||||
|
<Button type="button" size="sm" onClick={() => openCreateAccount()}>
|
||||||
|
新建管理员
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full max-w-lg gap-2">
|
<div className="flex w-full max-w-lg gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -254,16 +411,17 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
<TableHead className="w-16">ID</TableHead>
|
<TableHead className="w-16">ID</TableHead>
|
||||||
<TableHead>账号</TableHead>
|
<TableHead>账号</TableHead>
|
||||||
<TableHead>昵称</TableHead>
|
<TableHead>昵称</TableHead>
|
||||||
|
<TableHead className="w-20 whitespace-nowrap">状态</TableHead>
|
||||||
<TableHead>角色</TableHead>
|
<TableHead>角色</TableHead>
|
||||||
<TableHead>直接权限</TableHead>
|
<TableHead>直接权限</TableHead>
|
||||||
<TableHead>有效权限</TableHead>
|
<TableHead>有效权限</TableHead>
|
||||||
<TableHead className="w-24">操作</TableHead>
|
<TableHead className="min-w-[11rem]">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-muted-foreground">
|
<TableCell colSpan={8} className="text-muted-foreground">
|
||||||
暂无数据
|
暂无数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -278,6 +436,17 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.nickname}</TableCell>
|
<TableCell>{row.nickname}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.status === 0 ? (
|
||||||
|
<Badge variant="secondary" className="font-normal">
|
||||||
|
启用
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="border-amber-600/50 text-amber-800 dark:text-amber-400">
|
||||||
|
禁用
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{row.roles.length === 0 ? (
|
{row.roles.length === 0 ? (
|
||||||
@@ -294,6 +463,7 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
<TableCell className="tabular-nums">{row.direct_permissions.length}</TableCell>
|
<TableCell className="tabular-nums">{row.direct_permissions.length}</TableCell>
|
||||||
<TableCell className="tabular-nums">{row.effective_permissions.length}</TableCell>
|
<TableCell className="tabular-nums">{row.effective_permissions.length}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -304,6 +474,29 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
>
|
>
|
||||||
权限
|
权限
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant={
|
||||||
|
accountOpen && editingAccountId === row.id ? "secondary" : "outline"
|
||||||
|
}
|
||||||
|
onClick={() => openEditAccount(row)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={profile?.id === row.id}
|
||||||
|
title={
|
||||||
|
profile?.id === row.id ? "不能删除当前登录账号" : "删除该管理员"
|
||||||
|
}
|
||||||
|
onClick={() => setDeleteTarget(row)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
@@ -484,6 +677,149 @@ export function AdminUsersConsole(): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={accountOpen} onOpenChange={handleAccountDialogOpenChange}>
|
||||||
|
<DialogContent showCloseButton className="max-h-[90vh] max-w-lg gap-4 overflow-y-auto sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{accountMode === "create" ? "新建管理员" : "编辑账号"}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{accountMode === "create"
|
||||||
|
? "须为账号指定至少一个默认站点角色。登录账号仅可使用字母、数字、点、下划线与连字符,保存后为小写。"
|
||||||
|
: "登录账号不可修改。留空密码表示不修改。"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-sm font-medium leading-none">登录账号</div>
|
||||||
|
<Input
|
||||||
|
value={formUsername}
|
||||||
|
disabled={accountMode === "edit"}
|
||||||
|
placeholder="例如 ops_admin"
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => setFormUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-sm font-medium leading-none">昵称</div>
|
||||||
|
<Input
|
||||||
|
value={formNickname}
|
||||||
|
placeholder="显示名称"
|
||||||
|
onChange={(e) => setFormNickname(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-sm font-medium leading-none">邮箱(可选)</div>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={formEmail}
|
||||||
|
placeholder="留空则不填"
|
||||||
|
onChange={(e) => setFormEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-sm font-medium leading-none">
|
||||||
|
密码{accountMode === "edit" ? "(可选)" : ""}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={formPassword}
|
||||||
|
placeholder={accountMode === "create" ? "至少 8 位" : "不修改请留空"}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setFormPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{accountMode === "create" ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium leading-none">角色(默认站点,至少一项)</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
创建后即可在「权限」中继续调整角色或直接授权。
|
||||||
|
</p>
|
||||||
|
<div className="max-h-52 space-y-2 overflow-y-auto rounded-md border p-2.5 sm:grid sm:max-h-56 sm:grid-cols-2 sm:gap-2 sm:space-y-0">
|
||||||
|
{(catalog?.roles ?? []).length === 0 ? (
|
||||||
|
<p className="col-span-full text-xs text-muted-foreground">
|
||||||
|
暂无角色数据,请等待列表加载完成后重试。
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
(catalog?.roles ?? []).map((r) => {
|
||||||
|
const checked = formCreateRoles.includes(r.slug);
|
||||||
|
return (
|
||||||
|
<label key={r.slug} className="flex items-start gap-2 text-sm">
|
||||||
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(v) => toggleFormCreateRole(r.slug, v === true)}
|
||||||
|
/>
|
||||||
|
<span className="space-y-0.5">
|
||||||
|
<span className="block leading-none font-medium">{r.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{r.slug}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="text-sm font-medium leading-none">状态</div>
|
||||||
|
<select
|
||||||
|
className={selectClassName}
|
||||||
|
value={formStatus}
|
||||||
|
onChange={(e) => setFormStatus(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={0}>启用</option>
|
||||||
|
<option value={1}>禁用</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleAccountDialogOpenChange(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="button" disabled={accountSaving} onClick={() => void submitAccount()}>
|
||||||
|
{accountSaving ? "保存中…" : "保存"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||||
|
<DialogContent showCloseButton className="max-w-md gap-4">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{deleteTarget ? (
|
||||||
|
<>
|
||||||
|
确定删除管理员{" "}
|
||||||
|
<span className="font-medium text-foreground">{deleteTarget.username}</span>
|
||||||
|
?此操作不可撤销。
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={deleteBusy}
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deleteBusy}
|
||||||
|
onClick={() => void confirmDelete()}
|
||||||
|
>
|
||||||
|
{deleteBusy ? "删除中…" : "删除"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,3 +47,25 @@ export type AdminUserPermissionSyncData = {
|
|||||||
|
|
||||||
/** 与 {@link AdminUserPermissionSyncData} 相同(角色同步返回体一致) */
|
/** 与 {@link AdminUserPermissionSyncData} 相同(角色同步返回体一致) */
|
||||||
export type AdminUserRoleSyncData = 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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user