From f90d15bbe9392d8bde472962355c8be37cbe9ee3 Mon Sep 17 00:00:00 2001 From: xiaozhu <2395895331@qq.com> Date: Fri, 7 Aug 2026 18:08:16 +0800 Subject: [PATCH] =?UTF-8?q?update:=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/ProjectManagementPage.tsx | 423 +++++++++++++----- .../components/admin/UserManagementPage.tsx | 175 ++++---- .../app/components/admin/UserMultiSelect.tsx | 68 +-- frontend/app/context/AuthContext.tsx | 5 + frontend/app/services/api.ts | 73 ++- frontend/app/styles/platform.css | 24 +- 6 files changed, 561 insertions(+), 207 deletions(-) diff --git a/frontend/app/components/admin/ProjectManagementPage.tsx b/frontend/app/components/admin/ProjectManagementPage.tsx index 728e8bf..04cf2be 100644 --- a/frontend/app/components/admin/ProjectManagementPage.tsx +++ b/frontend/app/components/admin/ProjectManagementPage.tsx @@ -1,73 +1,67 @@ -import { useEffect, type FormEvent } from "react"; +import { useEffect, useState } from "react"; -import { type Workspace } from "../../services/api"; -import { useAuth } from "../../context/AuthContext"; +import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api"; +import { useApi, useAuth } from "../../context/AuthContext"; import Icon from "../common/Icon"; -import { useAdminStore } from "../../features/admin/state/adminStore"; - -import "../../styles/admin.css"; import { UserMultiSelect } from "./UserMultiSelect"; -type Notice = { - tone: "success" | "error" | "info"; - message: string; +const EMPTY_PROJECT_FORM = { + workspace_code: "", + workspace_name: "", + quota_bytes: 0, + description: "", }; export function ProjectManagementPage({ onNotify, onConnectionChange, }: { - onNotify: (notice: Notice) => void; + onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; onConnectionChange: (online: boolean) => void; }) { - const { user, currentWorkspace } = useAuth(); - - const projects = useAdminStore((s) => s.projects); - const projectLoading = useAdminStore((s) => s.projectsLoading); - const saving = useAdminStore((s) => s.projectsSaving); - const editingProject = useAdminStore((s) => s.editingProject); - const projectDialogOpen = useAdminStore((s) => s.projectDialogOpen); - const projectForm = useAdminStore((s) => s.projectForm); - const importMemberDialogOpen = useAdminStore((s) => s.importMemberDialogOpen); - const selectedProject = useAdminStore((s) => s.selectedProject); - const availableUsers = useAdminStore((s) => s.availableUsers); - const selectedUserIds = useAdminStore((s) => s.selectedUserIds); - const selectedRoleCode = useAdminStore((s) => s.selectedRoleCode); - const projectSearchTerm = useAdminStore((s) => s.projectSearchTerm); - - const setProjectDialogOpen = useAdminStore((s) => s.setProjectDialogOpen); - const setProjectForm = useAdminStore((s) => s.setProjectForm); - const setEditingProject = useAdminStore((s) => s.setEditingProject); - const setImportMemberDialogOpen = useAdminStore((s) => s.setImportMemberDialogOpen); - const setSelectedProject = useAdminStore((s) => s.setSelectedProject); - const setSelectedUserIds = useAdminStore((s) => s.setSelectedUserIds); - const setSelectedRoleCode = useAdminStore((s) => s.setSelectedRoleCode); - const setProjectSearchTerm = useAdminStore((s) => s.setProjectSearchTerm); - - const loadProjects = useAdminStore((s) => s.loadProjects); - const createProject = useAdminStore((s) => s.createProject); - const updateProject = useAdminStore((s) => s.updateProject); - const deleteProject = useAdminStore((s) => s.deleteProject); - const openImportMembers = useAdminStore((s) => s.openImportMembers); - const importMembers = useAdminStore((s) => s.importMembers); - const reset = useAdminStore((s) => s.reset); + const api = useApi(); + const { user } = useAuth(); + const [projects, setProjects] = useState([]); + const [projectLoading, setProjectLoading] = useState(true); + const [projectDialogOpen, setProjectDialogOpen] = useState(false); + const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM); + const [editingProject, setEditingProject] = useState(null); + const [importMemberDialogOpen, setImportMemberDialogOpen] = useState(false); + const [selectedProject, setSelectedProject] = useState(null); + const [availableUsers, setAvailableUsers] = useState([]); + const [selectedUserIds, setSelectedUserIds] = useState([]); + const [projectSearchTerm, setProjectSearchTerm] = useState(""); + const [saving, setSaving] = useState(false); + const [membersDrawerOpen, setMembersDrawerOpen] = useState(false); + const [currentProjectMembers, setCurrentProjectMembers] = useState([]); + const [membersLoading, setMembersLoading] = useState(false); const canManage = user?.role_code === "admin"; - const wsId = currentWorkspace?.workspace_id; + const loadProjects = async (): Promise => { + setProjectLoading(true); + try { + const workspaceList = await api.listWorkspaces(); + setProjects(workspaceList); + onConnectionChange(true); + } catch (error) { + onConnectionChange(false); + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "项目列表加载失败", + }); + } finally { + setProjectLoading(false); + } + }; + useEffect(() => { - reset(); - void loadProjects(onNotify, onConnectionChange); - }, [loadProjects, reset, wsId, onNotify, onConnectionChange]); + void loadProjects(); + }, []); const openCreateProject = (): void => { setEditingProject(null); - setProjectForm({ - workspace_code: "", - workspace_name: "", - quota_bytes: 0, - description: "", - }); + setProjectForm(EMPTY_PROJECT_FORM); setProjectDialogOpen(true); }; @@ -82,7 +76,7 @@ export function ProjectManagementPage({ setProjectDialogOpen(true); }; - const submitProject = async (event: FormEvent): Promise => { + const submitProject = async (event: React.FormEvent): Promise => { event.preventDefault(); if (!projectForm.workspace_name.trim()) { onNotify({ tone: "error", message: "请输入项目名称" }); @@ -92,57 +86,158 @@ export function ProjectManagementPage({ onNotify({ tone: "error", message: "请输入项目编码" }); return; } - if (editingProject) { - await updateProject( - editingProject, - { + setSaving(true); + try { + if (editingProject) { + const updated = await api.updateWorkspace(editingProject.workspace_id, { workspace_name: projectForm.workspace_name.trim(), quota_bytes: projectForm.quota_bytes, - description: projectForm.description.trim() || "", - }, - onNotify, - onConnectionChange, - ); - } else { - const generatedCode = projectForm.workspace_code.trim() - || projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32); - await createProject( - { + description: projectForm.description.trim() || undefined, + }); + setProjects((current) => + current.map((p) => (p.workspace_id === updated.workspace_id ? updated : p)) + ); + onNotify({ tone: "success", message: "项目信息已更新" }); + } else { + const generatedCode = projectForm.workspace_code.trim() || projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32); + const created = await api.createWorkspace({ workspace_code: generatedCode, workspace_name: projectForm.workspace_name.trim(), quota_bytes: projectForm.quota_bytes, - description: projectForm.description.trim() || "", - }, - onNotify, - onConnectionChange, - ); + description: projectForm.description.trim() || undefined, + }); + setProjects((current) => [...current, created]); + onNotify({ tone: "success", message: "项目已创建" }); + } + setProjectDialogOpen(false); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : (editingProject ? "更新项目失败" : "创建项目失败"), + }); + } finally { + setSaving(false); } }; - const handleDelete = async (project: Workspace): Promise => { + const deleteProject = async (project: Workspace): Promise => { if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return; - await deleteProject(project, onNotify, onConnectionChange); + try { + await api.deleteWorkspace(project.workspace_id); + setProjects((current) => current.filter((p) => p.workspace_id !== project.workspace_id)); + onNotify({ tone: "success", message: "项目已删除" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "删除项目失败", + }); + } }; - const openImportMemberDialog = (project: Workspace): void => { + const openImportMemberDialog = async (project: Workspace): Promise => { setSelectedProject(project); setSelectedUserIds([]); - setSelectedRoleCode("developer"); - void openImportMembers(project, onNotify); + setImportMemberDialogOpen(true); + + // 加载可用用户和项目成员 + try { + const [allUsers, currentMembers] = await Promise.all([ + api.listPlatformEmployees(), + api.listWorkspaceMembers(project.workspace_id), + ]); + setAvailableUsers(allUsers); + setCurrentProjectMembers(currentMembers); + } catch (error) { + // 如果加载失败,仍显示所有用户 + const allUsers = await api.listPlatformEmployees(); + setAvailableUsers(allUsers); + setCurrentProjectMembers([]); + } }; - const handleImportMember = async (): Promise => { + const openMembersDrawer = (project: Workspace): void => { + setSelectedProject(project); + setMembersDrawerOpen(true); + void loadProjectMembers(project.workspace_id); + }; + + const loadProjectMembers = async (workspaceId: string): Promise => { + setMembersLoading(true); + try { + const members = await api.listWorkspaceMembers(workspaceId); + setCurrentProjectMembers(members); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "成员列表加载失败", + }); + } finally { + setMembersLoading(false); + } + }; + + const removeMember = async (userId: string): Promise => { + if (!selectedProject) return; + // 管理员不能被移除 + const targetMember = currentProjectMembers.find((m) => m.user_id === userId); + if (targetMember?.role_code === "admin") { + onNotify({ tone: "error", message: "管理员不能被移除" }); + return; + } + try { + await api.deleteWorkspaceMember(selectedProject.workspace_id, userId); + setCurrentProjectMembers((current) => current.filter((m) => m.user_id !== userId)); + onNotify({ tone: "success", message: "成员已移除" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "移除成员失败", + }); + } + }; + + const updateMemberRole = async (userId: string, roleCode: "admin" | "developer"): Promise => { + if (!selectedProject) return; + try { + const updated = await api.updateWorkspaceMember(selectedProject.workspace_id, userId, { + role_code: roleCode, + }); + setCurrentProjectMembers((current) => + current.map((m) => (m.user_id === userId ? { ...m, role_code: roleCode, role_name: roleCode === "admin" ? "管理员" : "开发人员" } : m)) + ); + onNotify({ tone: "success", message: "成员角色已更新" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "更新成员角色失败", + }); + } + }; + + const importMember = async (): Promise => { if (!selectedProject || selectedUserIds.length === 0) { onNotify({ tone: "error", message: "请选择要添加的用户" }); return; } - if (!selectedProject) return; - await importMembers( - selectedProject, - selectedUserIds, - selectedRoleCode, - onNotify, - ); + setSaving(true); + try { + await Promise.all( + selectedUserIds.map((userId) => + api.addWorkspaceMember(selectedProject.workspace_id, { + user_id: userId, + }) + ) + ); + setImportMemberDialogOpen(false); + onNotify({ tone: "success", message: `已添加 ${selectedUserIds.length} 名成员` }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "添加成员失败", + }); + } finally { + setSaving(false); + } }; return ( @@ -210,14 +305,15 @@ export function ProjectManagementPage({ {project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"} + - + {/* */} @@ -295,36 +391,163 @@ export function ProjectManagementPage({ -
+
-
-
)} + + {membersDrawerOpen && selectedProject && ( + <> +
setMembersDrawerOpen(false)} style={{ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0,0,0,0.3)", + zIndex: 99, + }} /> + + + )} ); -} \ No newline at end of file +} diff --git a/frontend/app/components/admin/UserManagementPage.tsx b/frontend/app/components/admin/UserManagementPage.tsx index f69bbf9..df5233d 100644 --- a/frontend/app/components/admin/UserManagementPage.tsx +++ b/frontend/app/components/admin/UserManagementPage.tsx @@ -1,69 +1,66 @@ -import { useEffect, type FormEvent } from "react"; +import { useEffect, useState } from "react"; -import { type Employee } from "../../services/api"; -import { useAuth } from "../../context/AuthContext"; +import { ApiRequestError, type Employee } from "../../services/api"; +import { useApi, useAuth } from "../../context/AuthContext"; import Icon from "../../components/common/Icon"; -import { useAdminStore } from "../../features/admin/state/adminStore"; -import "../../styles/admin.css"; - -type Notice = { - tone: "success" | "error" | "info"; - message: string; +const EMPTY_FORM = { + username: "", + display_name: "", + email: "", + role_code: "developer" as "admin" | "developer", + password: "", + status: "active" as "active" | "disabled" | "locked", }; export function UserManagementPage({ onNotify, onConnectionChange, }: { - onNotify: (notice: Notice) => void; + onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; onConnectionChange: (online: boolean) => void; }) { - const { user, currentWorkspace } = useAuth(); - - const employees = useAdminStore((s) => s.employees); - const loading = useAdminStore((s) => s.usersLoading); - const saving = useAdminStore((s) => s.usersSaving); - const editing = useAdminStore((s) => s.editingUser); - const dialogOpen = useAdminStore((s) => s.userDialogOpen); - const form = useAdminStore((s) => s.userForm); - const userSearchTerm = useAdminStore((s) => s.userSearchTerm); - - const setEditingUser = useAdminStore((s) => s.setEditingUser); - const setUserDialogOpen = useAdminStore((s) => s.setUserDialogOpen); - const setUserForm = useAdminStore((s) => s.setUserForm); - const setUserSearchTerm = useAdminStore((s) => s.setUserSearchTerm); - - const loadEmployees = useAdminStore((s) => s.loadEmployees); - const createEmployee = useAdminStore((s) => s.createEmployee); - const updateEmployee = useAdminStore((s) => s.updateEmployee); - const deleteEmployee = useAdminStore((s) => s.deleteEmployee); - const reset = useAdminStore((s) => s.reset); + const api = useApi(); + const { user } = useAuth(); + const [employees, setEmployees] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [editing, setEditing] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [form, setForm] = useState(EMPTY_FORM); + const [userSearchTerm, setUserSearchTerm] = useState(""); const canManage = user?.role_code === "admin"; - const wsId = currentWorkspace?.workspace_id; + const load = async (): Promise => { + setLoading(true); + try { + setEmployees(await api.listPlatformEmployees()); + onConnectionChange(true); + } catch (error) { + onConnectionChange(false); + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "用户列表加载失败", + }); + } finally { + setLoading(false); + } + }; + useEffect(() => { - reset(); - void loadEmployees(); - }, [loadEmployees, reset, wsId]); + void load(); + }, []); const openCreate = (): void => { - setEditingUser(null); - setUserForm({ - username: "", - display_name: "", - email: "", - role_code: "developer", - password: "", - status: "active", - }); - setUserDialogOpen(true); + setEditing(null); + setForm(EMPTY_FORM); + setDialogOpen(true); }; const openEdit = (employee: Employee): void => { - setEditingUser(employee); - setUserForm({ + setEditing(employee); + setForm({ username: employee.username, display_name: employee.display_name, email: employee.email ?? "", @@ -71,16 +68,18 @@ export function UserManagementPage({ password: "", status: employee.status, }); - setUserDialogOpen(true); + setDialogOpen(true); }; - const submit = async (event: FormEvent): Promise => { + const submit = async (event: React.FormEvent): Promise => { event.preventDefault(); if (!form.display_name.trim() || (!editing && !form.username.trim())) return; + // 新建用户时密码必填 if (!editing && !form.password.trim()) { onNotify({ tone: "error", message: "请输入密码" }); return; } + // 编辑自身或管理员时,前端二次拦截禁用 status / role_code 的修改 if (editing) { const editingSelf = editing.user_id === user?.user_id; const targetIsAdmin = editing.role_code === "admin"; @@ -101,40 +100,58 @@ export function UserManagementPage({ return; } } + // 密码长度校验 8~72 字符 if (form.password && (form.password.length < 8 || form.password.length > 72)) { onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" }); return; } - if (editing) { - await updateEmployee( - editing, - { + setSaving(true); + try { + if (editing) { + const updated = await api.updatePlatformEmployee(editing.user_id, { display_name: form.display_name.trim(), email: form.email.trim() || null, role_code: form.role_code, status: form.status, - }, - onNotify, - onConnectionChange, - ); - } else { - await createEmployee( - { + }); + setEmployees((current) => current.map( + (item) => item.user_id === updated.user_id ? updated : item, + )); + onNotify({ tone: "success", message: "用户信息已更新" }); + } else { + const created = await api.createPlatformEmployee({ username: form.username.trim(), display_name: form.display_name.trim(), - email: form.email.trim() || null, - role_code: form.role_code, + email: form.email.trim() || undefined, password: form.password, - }, - onNotify, - onConnectionChange, - ); + role_code: "developer", + }); + setEmployees((current) => [...current, created]); + onNotify({ tone: "success", message: "用户已添加" }); + } + setDialogOpen(false); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "保存用户失败", + }); + } finally { + setSaving(false); } }; const remove = async (employee: Employee): Promise => { - if (!window.confirm(`确定从当前 Workspace 删除用户"${employee.display_name}"吗?`)) return; - await deleteEmployee(employee, onNotify, onConnectionChange); + if (!window.confirm(`确定从平台删除用户"${employee.display_name}"吗?`)) return; + try { + await api.deletePlatformEmployee(employee.user_id); + setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id)); + onNotify({ tone: "success", message: "用户已删除" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "删除用户失败", + }); + } }; return ( @@ -183,7 +200,7 @@ export function UserManagementPage({
{employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} {employee.username} - {employee.role_name} + {employee.role_code ? ({employee.role_name}) : (-)} {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} @@ -203,7 +220,7 @@ export function UserManagementPage({ EMPLOYEE

{editing ? "编辑用户" : "添加用户"}

- +
void submit(event)}> @@ -220,7 +237,7 @@ export function UserManagementPage({ setUserForm({ ...form, username: event.target.value })} + onChange={(event) => setForm({ ...form, username: event.target.value })} placeholder="请输入登录账号" autoComplete="username" /> @@ -232,7 +249,7 @@ export function UserManagementPage({ type="password" autoComplete="new-password" value={form.password} - onChange={(event) => setUserForm({ ...form, password: event.target.value })} + onChange={(event) => setForm({ ...form, password: event.target.value })} placeholder="请输入密码(8~72 字符)" /> @@ -243,21 +260,21 @@ export function UserManagementPage({ type="email" autoComplete="email" value={form.email} - onChange={(event) => setUserForm({ ...form, email: event.target.value })} + onChange={(event) => setForm({ ...form, email: event.target.value })} placeholder="请输入邮箱(可选)" /> - */} {editing && ( )}
- +
@@ -274,4 +291,4 @@ export function UserManagementPage({ )} ); -} \ No newline at end of file +} diff --git a/frontend/app/components/admin/UserMultiSelect.tsx b/frontend/app/components/admin/UserMultiSelect.tsx index 4af0927..6ecd606 100644 --- a/frontend/app/components/admin/UserMultiSelect.tsx +++ b/frontend/app/components/admin/UserMultiSelect.tsx @@ -7,10 +7,12 @@ export function UserMultiSelect({ users, selectedUserIds, onChange, + existingMemberIds, }: { users: Employee[]; selectedUserIds: string[]; onChange: (ids: string[]) => void; + existingMemberIds?: string[]; }) { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); @@ -114,39 +116,55 @@ export function UserMultiSelect({ top: "100%", left: 0, right: 0, - maxHeight: "200px", + maxHeight: "136px", overflowY: "auto", border: "1px solid #1890ff", borderRadius: "4px", backgroundColor: "#fff", - zIndex: 1000, + zIndex: 100, marginTop: "2px", boxShadow: "0 2px 8px rgba(0,0,0,0.15)", }} > - {users.map((user) => { - const isSelected = selectedUserIds.includes(user.user_id); - return ( -
toggleUser(user.user_id)} - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "8px 12px", - cursor: "pointer", - backgroundColor: isSelected ? "#e6f7ff" : "transparent", - borderBottom: "1px solid #f0f0f0", - }} - > - - {user.display_name} ({user.username}) - - {isSelected && } -
- ); - })} + {users.length === 0 ? ( +
+ 所有可选用户已在项目中 +
+ ) : ( + users.map((user) => { + const isSelected = selectedUserIds.includes(user.user_id); + const isExisting = existingMemberIds?.includes(user.user_id); + const isDisabled = isExisting; + return ( +
!isDisabled && toggleUser(user.user_id)} + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "6px 12px", + cursor: isDisabled ? "not-allowed" : "pointer", + backgroundColor: isSelected ? "#e6f7ff" : isExisting ? "#f5f5f5" : "transparent", + borderBottom: "1px solid #f0f0f0", + opacity: isDisabled ? 0.6 : 1, + }} + > + + {user.display_name} ({user.username}) + +
+ {isExisting && ( + + 已在项目中 + + )} + {isSelected && } +
+
+ ); + }) + )} )} diff --git a/frontend/app/context/AuthContext.tsx b/frontend/app/context/AuthContext.tsx index 4c0a1e0..abbb329 100644 --- a/frontend/app/context/AuthContext.tsx +++ b/frontend/app/context/AuthContext.tsx @@ -235,10 +235,15 @@ export function useApi(): WorkspaceBoundApi { hideScheduleArtifact: (versionsId) => rawApi.hideScheduleArtifact(workspaceId, versionsId), listEmployees: () => rawApi.listEmployees(workspaceId), + listPlatformEmployees: () => rawApi.listPlatformEmployees(), createEmployee: (input) => rawApi.createEmployee(workspaceId, input), + createPlatformEmployee: (input) => rawApi.createPlatformEmployee(input), updateEmployee: (userId, input) => rawApi.updateEmployee(workspaceId, userId, input), + updatePlatformEmployee: (userId, input) => + rawApi.updatePlatformEmployee(userId, input), deleteEmployee: (userId) => rawApi.deleteEmployee(workspaceId, userId), + deletePlatformEmployee: (userId) => rawApi.deletePlatformEmployee(userId), createScheduleNode: (scheduleId, input) => rawApi.createScheduleNode(workspaceId, scheduleId, input), updateScheduleNode: (scheduleId, nodeId, input) => diff --git a/frontend/app/services/api.ts b/frontend/app/services/api.ts index 87bb3e7..bef60ae 100644 --- a/frontend/app/services/api.ts +++ b/frontend/app/services/api.ts @@ -160,7 +160,7 @@ export type WorkspaceUpdatePayload = { export type WorkspaceMemberAddPayload = { user_id: string; - role_code: "admin" | "developer"; + role_code?: "admin" | "developer"; }; export type WorkspaceMemberUpdatePayload = { @@ -902,10 +902,57 @@ export async function hideScheduleArtifact( ); } +// 系统管理级别接口 - 不区分 workspace +export async function listPlatformEmployees(): Promise { + return apiRequest("/api/v1/platform/employees"); +} + +export async function createPlatformEmployee( + input: { + username: string; + display_name: string; + email?: string | undefined; + password: string; + role_code?: "admin" | "developer"; + }, +): Promise { + return apiRequest( + "/api/v1/platform/employees", + { method: "POST", body: JSON.stringify({ ...input, role_code: input.role_code ?? "developer" }) }, + ); +} + +export async function updatePlatformEmployee( + userId: string, + input: { + display_name?: string; + email?: string | null; + role_code?: "admin" | "developer"; + status?: "active" | "disabled" | "locked"; + }, +): Promise { + return apiRequest( + `/api/v1/platform/employees/${userId}`, + { method: "PATCH", body: JSON.stringify(input) }, + ); +} + +export async function deletePlatformEmployee( + userId: string, +): Promise<{ user_id: string; deleted: boolean }> { + return apiRequest( + `/api/v1/platform/employees/${userId}`, + { method: "DELETE" }, + ); +} + +// 兼容旧的 workspace 级别接口(已废弃,建议使用 system-level 接口) +/** @deprecated 使用 listPlatformEmployees 代替 */ export async function listEmployees(workspaceId: string): Promise { return apiRequest("/api/v1/admin/employees", {}, workspaceId); } +/** @deprecated 使用 createPlatformEmployee 代替 */ export async function createEmployee( workspaceId: string, input: { @@ -923,6 +970,7 @@ export async function createEmployee( ); } +/** @deprecated 使用 updatePlatformEmployee 代替 */ export async function updateEmployee( workspaceId: string, userId: string, @@ -940,6 +988,7 @@ export async function updateEmployee( ); } +/** @deprecated 使用 deletePlatformEmployee 代替 */ export async function deleteEmployee( workspaceId: string, userId: string, @@ -1287,16 +1336,38 @@ export type WorkspaceBoundApi = { artifact_preserved: boolean; }>; listEmployees: () => Promise; + listPlatformEmployees: () => Promise; createEmployee: ( input: Parameters[1], ) => Promise; + createPlatformEmployee: ( + input: { + username: string; + display_name: string; + email?: string | undefined; + password: string; + role_code?: "admin" | "developer"; + }, + ) => Promise; updateEmployee: ( userId: string, input: Parameters[2], ) => Promise; + updatePlatformEmployee: ( + userId: string, + input: { + display_name?: string; + email?: string | null; + role_code?: "admin" | "developer"; + status?: "active" | "disabled" | "locked"; + }, + ) => Promise; deleteEmployee: ( userId: string, ) => Promise<{ user_id: string; deleted: boolean }>; + deletePlatformEmployee: ( + userId: string, + ) => Promise<{ user_id: string; deleted: boolean }>; createScheduleNode: ( scheduleId: string, input: Parameters[2], diff --git a/frontend/app/styles/platform.css b/frontend/app/styles/platform.css index be1f662..8576e02 100644 --- a/frontend/app/styles/platform.css +++ b/frontend/app/styles/platform.css @@ -1541,7 +1541,7 @@ button { .modal { width: min(520px, calc(100vw - 40px)); - overflow: hidden; + overflow: visible; border: 1px solid #dce4eb; border-radius: 11px; background: #fff; @@ -1550,7 +1550,25 @@ button { } .modal--compact { - width: min(430px, calc(100vw - 40px)); + width: min(480px, calc(100vw - 40px)); + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.modal--compact .modal__header { + flex-shrink: 0; +} + +.modal--compact form { + padding: 19px 22px 0; + flex: 1; + overflow-y: auto; +} + +.modal--compact .modal__footer { + flex-shrink: 0; + margin: 0; } .modal__header { @@ -1576,6 +1594,8 @@ button { .modal form { padding: 19px 22px 0; + flex: 1; + overflow-y: auto; } .destination-chip {