feat(admin): 用户管理 - 支持管理员添加用户 (#104)

* feat(canvas): 绘图工具 - 支持自定义 tldraw License Key

* docs(canvas): 绘图工具 - 添加 License Key 页面截图

* feat(admin): 用户管理 - 支持管理员添加用户
This commit is contained in:
ahyang
2026-08-03 22:40:10 +08:00
committed by GitHub
parent d2741f39f3
commit acaa4bb69f
6 changed files with 280 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
import { App, Button, Drawer, Form, Input, Select } from "antd";
import { useEffect, useState } from "react";
import { updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";
import { createAdminUser, updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";
type UserFormValues = Pick<LocalUser, "displayName" | "email" | "role" | "status">;
@@ -100,3 +100,104 @@ export function AdminUserEditDrawer({
</Drawer>
);
}
type CreateUserFormValues = {
username: string;
displayName: string;
email?: string;
password: string;
role: LocalUser["role"];
status: LocalUser["status"];
};
export function AdminUserCreateDrawer({
open,
onClose,
onCreated,
}: {
open: boolean;
onClose: () => void;
onCreated: (user: AdminUser) => void;
}) {
const { message, modal } = App.useApp();
const [saving, setSaving] = useState(false);
const [form] = Form.useForm<CreateUserFormValues>();
useEffect(() => {
if (!open) return;
form.resetFields();
form.setFieldsValue({ role: "user", status: "active" });
}, [form, open]);
const close = () => {
if (saving) return;
if (!form.isFieldsTouched()) {
onClose();
return;
}
modal.confirm({
title: "\u653e\u5f03\u6dfb\u52a0\u7528\u6237\uff1f",
content: "\u5c1a\u672a\u4fdd\u5b58\u7684\u7528\u6237\u4fe1\u606f\u5c06\u4e22\u5931\u3002",
okText: "\u653e\u5f03\u5e76\u5173\u95ed",
cancelText: "\u7ee7\u7eed\u7f16\u8f91",
okButtonProps: { danger: true },
onOk: onClose,
});
};
const save = async () => {
const values = await form.validateFields();
setSaving(true);
try {
const result = await createAdminUser({
username: values.username.trim(),
displayName: values.displayName.trim(),
email: values.email?.trim() || "",
password: values.password,
role: values.role,
status: values.status,
});
onCreated(result.user);
form.resetFields();
onClose();
message.success("\u7528\u6237\u5df2\u521b\u5efa");
} catch (error) {
message.error(error instanceof Error ? error.message : "\u521b\u5efa\u7528\u6237\u5931\u8d25");
} finally {
setSaving(false);
}
};
return (
<Drawer
title={"\u6dfb\u52a0\u7528\u6237"}
open={open}
width="min(520px, 100vw)"
onClose={close}
maskClosable={!saving}
destroyOnHidden
extra={<Button type="primary" loading={saving} onClick={() => void save()}>{"\u4fdd\u5b58"}</Button>}
>
<Form form={form} layout="vertical" requiredMark={false}>
<Form.Item name="username" label={"\u7528\u6237\u540d"} rules={[{ required: true, whitespace: true, message: "\u8bf7\u8f93\u5165\u7528\u6237\u540d" }]}>
<Input placeholder={"3-32 \u4f4d\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u4e0b\u5212\u7ebf\u6216\u8fde\u5b57\u7b26"} />
</Form.Item>
<Form.Item name="displayName" label={"\u663e\u793a\u540d\u79f0"} rules={[{ required: true, whitespace: true, message: "\u8bf7\u586b\u5199\u663e\u793a\u540d\u79f0" }]}>
<Input placeholder={"\u7528\u6237\u5728\u4ea7\u54c1\u5185\u663e\u793a\u7684\u540d\u79f0"} />
</Form.Item>
<Form.Item name="email" label={"\u90ae\u7bb1"} rules={[{ type: "email", message: "\u8bf7\u8f93\u5165\u6709\u6548\u90ae\u7bb1" }]}>
<Input placeholder="name@example.com" />
</Form.Item>
<Form.Item name="password" label={"\u521d\u59cb\u5bc6\u7801"} rules={[{ required: true, message: "\u8bf7\u8bbe\u7f6e\u521d\u59cb\u5bc6\u7801" }]}>
<Input.Password placeholder={"\u81f3\u5c11 8 \u4f4d"} />
</Form.Item>
<Form.Item name="role" label={"\u89d2\u8272"}>
<Select options={[{ label: "\u7ba1\u7406\u5458", value: "admin" }, { label: "\u666e\u901a\u7528\u6237", value: "user" }]} />
</Form.Item>
<Form.Item name="status" label={"\u8d26\u53f7\u72b6\u6001"}>
<Select options={[{ label: "\u5df2\u542f\u7528", value: "active" }, { label: "\u5df2\u505c\u7528", value: "disabled" }]} />
</Form.Item>
</Form>
</Drawer>
);
}

View File

@@ -1,5 +1,5 @@
import { App, Button, Checkbox, Dropdown, Input, Table, Tag } from "antd";
import { Ban, ChevronDown, Search, Settings2 } from "lucide-react";
import { Ban, ChevronDown, Search, Settings2, UserPlus } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ListToolbar, PaginationBar, TableSurface } from "@/components/layout/workspace-page";
@@ -10,7 +10,7 @@ import { AdminBatchBar, AdminTableEmpty, AdminTableSkeleton } from "../component
import { useTableUrlState } from "../lib/use-table-url-state";
import { AdminUserDetailDrawer } from "../components/admin-user-detail-drawer";
import { createUserColumns, userColumnOptions, type UserColumnKey } from "./users-columns";
import { AdminUserEditDrawer } from "./users-drawer";
import { AdminUserCreateDrawer, AdminUserEditDrawer } from "./users-drawer";
const columnStorageKey = "admin-users-visible-columns";
const allColumnKeys = userColumnOptions.map((item) => item.key);
@@ -25,6 +25,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
const [loading, setLoading] = useState(true);
const [detailUserId, setDetailUserId] = useState<string | null>(null);
const [editingUser, setEditingUser] = useState<AdminUser | null>(null);
const [createUserOpen, setCreateUserOpen] = useState(false);
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
const [bulkDisabling, setBulkDisabling] = useState(false);
const [visibleColumns, setVisibleColumns] = useState<Set<UserColumnKey>>(() => {
@@ -74,6 +75,13 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
onUserChanged?.(nextUser);
}, [onUserChanged]);
const addUser = useCallback((user: AdminUser) => {
setUsers((items) => [user, ...items].slice(0, state.pageSize));
setTotal((value) => value + 1);
onUserChanged?.(user);
setCreateUserOpen(false);
}, [onUserChanged, state.pageSize]);
const toggleStatus = useCallback(async (user: AdminUser) => {
try {
if (user.status === "active") {
@@ -94,7 +102,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
actorId: actor?.id,
visibleColumns,
onView: (user) => setDetailUserId(user.id),
onEdit: setEditingUser,
onEdit: (user) => { setCreateUserOpen(false); setEditingUser(user); },
onToggleStatus: toggleStatus,
}), [actor?.id, toggleStatus, visibleColumns]);
@@ -129,6 +137,8 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
active={hasFilters}
onReset={resetFilters}
trailing={
<div className="flex items-center gap-2">
<Button icon={<UserPlus className="size-4" />} onClick={() => { setEditingUser(null); setCreateUserOpen(true); }}>{"\u6dfb\u52a0\u7528\u6237"}</Button>
<Dropdown
trigger={["click"]}
dropdownRender={() => (
@@ -156,6 +166,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
>
<Button icon={<Settings2 className="size-4" />}>列设置</Button>
</Dropdown>
</div>
}
>
<Input
@@ -217,6 +228,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
</TableSurface>
<AdminUserDetailDrawer userId={detailUserId} onClose={() => setDetailUserId(null)} />
<AdminUserCreateDrawer open={createUserOpen} onClose={() => setCreateUserOpen(false)} onCreated={addUser} />
<AdminUserEditDrawer user={editingUser} actorId={actor?.id} onClose={() => setEditingUser(null)} onSaved={replaceUser} />
</>
);

View File

@@ -345,6 +345,10 @@ export function listAdminUsers(params: AdminListParams = {}) {
return request<{ users: AdminUser[]; total: number; page: number; limit: number }>(api.get("/admin/users", { params }));
}
export function createAdminUser(input: { username: string; displayName: string; email?: string; password: string; role: LocalUser["role"]; status: LocalUser["status"] }) {
return request<{ user: AdminUser }>(api.post("/admin/users", input));
}
export function getAdminReferences() {
return request<AdminReferenceData>(api.get("/admin/references"));
}