diff --git a/frontend/app/features/admin/CreateUserProjectField.tsx b/frontend/app/features/admin/CreateUserProjectField.tsx
new file mode 100644
index 0000000..8395872
--- /dev/null
+++ b/frontend/app/features/admin/CreateUserProjectField.tsx
@@ -0,0 +1,62 @@
+import type { Workspace } from "../../services/api";
+import { Checkbox } from "~/components/ui/checkbox";
+import { formFieldClass } from "../platform/modalUi";
+
+const CHECKBOX_CLASS =
+ "size-[16px] shrink-0 rounded-[3px] border-[#cdd9e4] bg-white data-checked:border-[#1277d3] data-checked:bg-[#1277d3] data-checked:text-white";
+
+/** 新建用户时可选加入的项目列表(多选,非必填)。 */
+export function CreateUserProjectField({
+ projects,
+ loading,
+ selectedIds,
+ onToggle,
+}: {
+ projects: Workspace[];
+ loading: boolean;
+ selectedIds: string[];
+ onToggle: (workspaceId: string) => void;
+}) {
+ return (
+
+
+ 加入项目
+ (可选)
+
+
+ {loading ? (
+
加载项目中…
+ ) : projects.length === 0 ? (
+
暂无可用项目
+ ) : (
+
+ {projects.map((project) => {
+ const checked = selectedIds.includes(project.workspace_id);
+ return (
+
+ );
+ })}
+
+ )}
+
+
+ 未加入任何项目时,该用户将无法登录。
+
+
+ );
+}
diff --git a/frontend/app/features/admin/UserFormDialog.tsx b/frontend/app/features/admin/UserFormDialog.tsx
new file mode 100644
index 0000000..3fac2de
--- /dev/null
+++ b/frontend/app/features/admin/UserFormDialog.tsx
@@ -0,0 +1,215 @@
+import type { Employee, Role, Workspace } from "../../services/api";
+import {
+ AppFormDialog,
+ dialogPrimaryButtonClass,
+ dialogSecondaryButtonClass,
+} from "~/components/common/AppFormDialog";
+import { Button } from "~/components/ui/button";
+import { Input } from "~/components/ui/input";
+import { CreateUserProjectField } from "./CreateUserProjectField";
+import {
+ formFieldClass,
+ formInputClass,
+ modalFormClass,
+} from "../platform/modalUi";
+
+export type UserFormState = {
+ username: string;
+ display_name: string;
+ email: string;
+ role_code: "admin" | "developer";
+ password: string;
+ status: "active" | "disabled" | "locked";
+};
+
+const FORM_INPUT_CLASS =
+ "h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:ring-3 focus:ring-brand/10 disabled:cursor-not-allowed disabled:opacity-50";
+
+export function UserFormDialog({
+ open,
+ editing,
+ form,
+ saving,
+ currentUserId,
+ roles,
+ createRoleOptions,
+ createProjects,
+ createProjectsLoading,
+ selectedWorkspaceIds,
+ onChangeForm,
+ onToggleWorkspace,
+ onSubmit,
+ onClose,
+}: {
+ open: boolean;
+ editing: Employee | null;
+ form: UserFormState;
+ saving: boolean;
+ currentUserId: string | undefined;
+ roles: Role[];
+ createRoleOptions: Role[];
+ createProjects: Workspace[];
+ createProjectsLoading: boolean;
+ selectedWorkspaceIds: string[];
+ onChangeForm: (next: UserFormState) => void;
+ onToggleWorkspace: (workspaceId: string) => void;
+ onSubmit: (event: React.FormEvent) => void;
+ onClose: () => void;
+}) {
+ return (
+ {
+ if (!nextOpen) onClose();
+ }}
+ eyebrow="EMPLOYEE"
+ title={editing ? "编辑用户" : "添加用户"}
+ >
+
+
+ );
+}
diff --git a/frontend/app/features/admin/UserManagementPage.tsx b/frontend/app/features/admin/UserManagementPage.tsx
index fc15a7f..ba5d008 100644
--- a/frontend/app/features/admin/UserManagementPage.tsx
+++ b/frontend/app/features/admin/UserManagementPage.tsx
@@ -1,16 +1,10 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Plus, Search } from "lucide-react";
-import { ApiRequestError, type Employee, type Role } from "../../services/api";
+import { ApiRequestError, type Employee, type Role, type Workspace } from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
-import {
- AppFormDialog,
- dialogPrimaryButtonClass,
- dialogSecondaryButtonClass,
-} from "~/components/common/AppFormDialog";
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import { Button } from "~/components/ui/button";
-import { Input } from "~/components/ui/input";
import {
Table,
TableBody,
@@ -21,12 +15,8 @@ import {
} from "~/components/ui/table";
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
import { AdminPagination } from "./AdminPagination";
+import { UserFormDialog, type UserFormState } from "./UserFormDialog";
import { useCursorPage } from "./useCursorPage";
-import {
- formFieldClass,
- formInputClass,
- modalFormClass,
-} from "../platform/modalUi";
import {
adminEmptyClass,
adminPageClass,
@@ -46,18 +36,15 @@ import {
import { primaryGradientButtonClass } from "~/components/common/buttonClasses";
import { useDebouncedValue } from "./useDebouncedValue";
-const EMPTY_FORM = {
+const EMPTY_FORM: UserFormState = {
username: "",
display_name: "",
email: "",
- role_code: "developer" as "admin" | "developer",
+ role_code: "developer",
password: "",
- status: "active" as "active" | "disabled" | "locked",
+ status: "active",
};
-const FORM_INPUT_CLASS =
- "h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:ring-3 focus:ring-brand/10 disabled:cursor-not-allowed disabled:opacity-50";
-
export function UserManagementPage({
onNotify,
onConnectionChange,
@@ -71,9 +58,12 @@ export function UserManagementPage({
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
- const [form, setForm] = useState(EMPTY_FORM);
+ const [form, setForm] = useState(EMPTY_FORM);
const [userSearchTerm, setUserSearchTerm] = useState("");
const [deleteTarget, setDeleteTarget] = useState(null);
+ const [createProjects, setCreateProjects] = useState([]);
+ const [createProjectsLoading, setCreateProjectsLoading] = useState(false);
+ const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState([]);
const debouncedSearch = useDebouncedValue(userSearchTerm, 300);
const canManage = user?.role_code === "admin";
@@ -113,14 +103,34 @@ export function UserManagementPage({
useEffect(() => {
void api.listPlatformRoles().then(setRoles).catch(() => {
- /* roles only needed for edit dialog */
+ /* roles only needed for dialog */
});
}, [api]);
+ const createRoleOptions = useMemo(
+ () => roles.filter((role) => role.role_code !== "admin"),
+ [roles],
+ );
+
+ const loadCreateProjects = useCallback(async (): Promise => {
+ setCreateProjectsLoading(true);
+ try {
+ const page = await api.listWorkspaces({ limit: 200, cursor: null, q: "" });
+ setCreateProjects(page.items.filter((item) => item.status === "active"));
+ } catch {
+ setCreateProjects([]);
+ onNotify({ tone: "error", message: "项目列表加载失败" });
+ } finally {
+ setCreateProjectsLoading(false);
+ }
+ }, [api, onNotify]);
+
const openCreate = (): void => {
setEditing(null);
setForm(EMPTY_FORM);
+ setSelectedWorkspaceIds([]);
setDialogOpen(true);
+ void loadCreateProjects();
};
const openEdit = (employee: Employee): void => {
@@ -133,9 +143,18 @@ export function UserManagementPage({
password: "",
status: employee.status,
});
+ setSelectedWorkspaceIds([]);
setDialogOpen(true);
};
+ const toggleCreateWorkspace = (workspaceId: string): void => {
+ setSelectedWorkspaceIds((current) =>
+ current.includes(workspaceId)
+ ? current.filter((id) => id !== workspaceId)
+ : [...current, workspaceId],
+ );
+ };
+
const submit = async (event: React.FormEvent): Promise => {
event.preventDefault();
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
@@ -163,6 +182,10 @@ export function UserManagementPage({
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
return;
}
+ if (!editing && form.role_code === "admin") {
+ onNotify({ tone: "error", message: "新建用户不能指定管理员角色" });
+ return;
+ }
setSaving(true);
try {
if (editing) {
@@ -177,14 +200,38 @@ export function UserManagementPage({
);
onNotify({ tone: "success", message: "用户信息已更新" });
} else {
- await api.createPlatformEmployee({
+ const created = await api.createPlatformEmployee({
username: form.username.trim(),
display_name: form.display_name.trim(),
email: form.email.trim() || undefined,
password: form.password,
- role_code: "developer",
+ role_code: form.role_code,
});
- onNotify({ tone: "success", message: "用户已添加" });
+ if (selectedWorkspaceIds.length > 0) {
+ const results = await Promise.allSettled(
+ selectedWorkspaceIds.map((workspaceId) =>
+ api.addWorkspaceMember(workspaceId, { user_id: created.user_id }),
+ ),
+ );
+ const failed = results.filter((item) => item.status === "rejected").length;
+ const joined = results.length - failed;
+ if (failed === 0) {
+ onNotify({
+ tone: "success",
+ message: `用户已添加,并加入 ${joined} 个项目`,
+ });
+ } else {
+ onNotify({
+ tone: "error",
+ message: `用户已创建,但有 ${failed} 个项目加入失败`,
+ });
+ }
+ } else {
+ onNotify({
+ tone: "info",
+ message: "用户已添加",
+ });
+ }
await refreshFromStart();
}
setDialogOpen(false);
@@ -317,118 +364,22 @@ export function UserManagementPage({
onGoToPage={goToPage}
/>
- {
- if (!nextOpen) setDialogOpen(false);
- }}
- eyebrow="EMPLOYEE"
- title={editing ? "编辑用户" : "添加用户"}
- >
-
-
+ editing={editing}
+ form={form}
+ saving={saving}
+ currentUserId={user?.user_id}
+ roles={roles}
+ createRoleOptions={createRoleOptions}
+ createProjects={createProjects}
+ createProjectsLoading={createProjectsLoading}
+ selectedWorkspaceIds={selectedWorkspaceIds}
+ onChangeForm={setForm}
+ onToggleWorkspace={toggleCreateWorkspace}
+ onSubmit={(event) => void submit(event)}
+ onClose={() => setDialogOpen(false)}
+ />