From 394081b613856cead420693e2d0202ab8486348e Mon Sep 17 00:00:00 2001 From: xiaozhu <2395895331@qq.com> Date: Wed, 5 Aug 2026 13:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=A1=B9=E7=9B=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/context/AuthContext.tsx | 9 + frontend/app/features/admin/AdminPages.tsx | 610 +++++++++++++++++++-- frontend/app/services/api.ts | 150 +++++ 3 files changed, 730 insertions(+), 39 deletions(-) diff --git a/frontend/app/context/AuthContext.tsx b/frontend/app/context/AuthContext.tsx index 81b45c6..3268e6b 100644 --- a/frontend/app/context/AuthContext.tsx +++ b/frontend/app/context/AuthContext.tsx @@ -253,5 +253,14 @@ export function useApi(): WorkspaceBoundApi { runScheduleNow: (scheduleId) => rawApi.runScheduleNow(workspaceId, scheduleId), listScheduleRuns: (input) => rawApi.listScheduleRuns(workspaceId, input), getScheduleRun: (runId) => rawApi.getScheduleRun(workspaceId, runId), + // Workspace (Project) Management - 系统管理接口(跨 workspace,不需要传入 workspaceId) + listWorkspaces: () => rawApi.listWorkspaces(), + createWorkspace: (input) => rawApi.createWorkspace(input), + updateWorkspace: (workspaceId, input) => rawApi.updateWorkspace(workspaceId, input), + deleteWorkspace: (workspaceId) => rawApi.deleteWorkspace(workspaceId), + listWorkspaceMembers: (workspaceId) => rawApi.listWorkspaceMembers(workspaceId), + addWorkspaceMember: (workspaceId, input) => rawApi.addWorkspaceMember(workspaceId, input), + updateWorkspaceMember: (workspaceId, userId, input) => rawApi.updateWorkspaceMember(workspaceId, userId, input), + deleteWorkspaceMember: (workspaceId, userId) => rawApi.deleteWorkspaceMember(workspaceId, userId), }), [workspaceId]); } diff --git a/frontend/app/features/admin/AdminPages.tsx b/frontend/app/features/admin/AdminPages.tsx index 006dd69..421d111 100644 --- a/frontend/app/features/admin/AdminPages.tsx +++ b/frontend/app/features/admin/AdminPages.tsx @@ -1,8 +1,10 @@ -import { type FormEvent, useEffect, useState } from "react"; +import { type FormEvent, useEffect, useState, useRef } from "react"; import { ApiRequestError, type Employee, + type Workspace, + type WorkspaceMember, } from "../../services/api"; import { useApi, useAuth } from "../../context/AuthContext"; import Icon from "../../components/common/Icon"; @@ -103,14 +105,22 @@ const EMPTY_FORM = { display_name: "", email: "", role_code: "developer" as "admin" | "developer", + password: "", status: "active" as "active" | "disabled" | "locked", }; const EMPTY_PROJECT_FORM = { - project_name: "", + workspace_code: "", + workspace_name: "", + quota_bytes: 0, description: "", }; +type ProjectWithMembers = Workspace & { + member_count: number; + members?: WorkspaceMember[]; +}; + export function SystemAdminPage({ onNotify, onConnectionChange, @@ -127,8 +137,21 @@ export function SystemAdminPage({ const [editing, setEditing] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); + const [userSearchTerm, setUserSearchTerm] = useState(""); + + // 项目管理状态 + 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 [selectedRoleCode, setSelectedRoleCode] = useState<"admin" | "developer">("developer"); + const [projectSearchTerm, setProjectSearchTerm] = useState(""); + const canManage = user?.role_code === "admin"; const load = async (): Promise => { @@ -147,10 +170,45 @@ export function SystemAdminPage({ } }; + 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); + } + }; + + const loadAvailableUsers = async (): Promise => { + try { + const list = await api.listEmployees(); + setAvailableUsers(list); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof Error ? error.message : "用户列表加载失败", + }); + } + }; + useEffect(() => { void load(); }, []); + useEffect(() => { + if (activeTab === "projects") { + void loadProjects(); + } + }, [activeTab]); + const openCreate = (): void => { setEditing(null); setForm(EMPTY_FORM); @@ -164,6 +222,7 @@ export function SystemAdminPage({ display_name: employee.display_name, email: employee.email ?? "", role_code: employee.role_code, + password: "", status: employee.status, }); setDialogOpen(true); @@ -172,6 +231,16 @@ export function SystemAdminPage({ 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; + } + // 密码长度校验 8~72 字符 + if (form.password && (form.password.length < 8 || form.password.length > 72)) { + onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" }); + return; + } setSaving(true); try { if (editing) { @@ -184,17 +253,19 @@ export function SystemAdminPage({ setEmployees((current) => current.map( (item) => item.user_id === updated.user_id ? updated : item, )); + onNotify({ tone: "success", message: "用户信息已更新" }); } else { const created = await api.createEmployee({ username: form.username.trim(), display_name: form.display_name.trim(), email: form.email.trim() || null, role_code: form.role_code, + password: form.password, }); setEmployees((current) => [...current, created]); + onNotify({ tone: "success", message: "用户已添加" }); } setDialogOpen(false); - onNotify({ tone: "success", message: editing ? "用户信息已更新" : "用户已添加" }); } catch (error) { onNotify({ tone: "error", @@ -220,23 +291,112 @@ export function SystemAdminPage({ }; const openCreateProject = (): void => { + setEditingProject(null); setProjectForm(EMPTY_PROJECT_FORM); setProjectDialogOpen(true); }; + const openEditProject = (project: Workspace): void => { + setEditingProject(project); + setProjectForm({ + workspace_code: project.workspace_code, + workspace_name: project.workspace_name, + quota_bytes: project.quota_bytes, + description: project.description ?? "", + }); + setProjectDialogOpen(true); + }; + const submitProject = async (event: React.FormEvent): Promise => { event.preventDefault(); - if (!projectForm.project_name.trim()) return; + if (!projectForm.workspace_name.trim()) { + onNotify({ tone: "error", message: "请输入项目名称" }); + return; + } + if (!projectForm.workspace_code.trim() && !editingProject) { + onNotify({ tone: "error", message: "请输入项目编码" }); + return; + } setSaving(true); try { - // TODO: 调用实际的项目创建 API - // await api.createProject({ ... }) + 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() || 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() || undefined, + }); + setProjects((current) => [...current, created]); + onNotify({ tone: "success", message: "项目已创建" }); + } setProjectDialogOpen(false); - onNotify({ tone: "success", message: "项目已创建" }); } catch (error) { onNotify({ tone: "error", - message: error instanceof ApiRequestError ? error.message : "创建项目失败", + message: error instanceof ApiRequestError ? error.message : (editingProject ? "更新项目失败" : "创建项目失败"), + }); + } finally { + setSaving(false); + } + }; + + const deleteProject = async (project: Workspace): Promise => { + if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return; + 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 => { + setSelectedProject(project); + setSelectedUserIds([]); + setSelectedRoleCode("developer"); + void loadAvailableUsers(); + setImportMemberDialogOpen(true); + }; + + const importMember = async (): Promise => { + if (!selectedProject || selectedUserIds.length === 0) { + onNotify({ tone: "error", message: "请选择要添加的用户" }); + return; + } + setSaving(true); + try { + // 批量添加选中的用户 + await Promise.all( + selectedUserIds.map((userId) => + api.addWorkspaceMember(selectedProject.workspace_id, { + user_id: userId, + role_code: selectedRoleCode, + }) + ) + ); + setImportMemberDialogOpen(false); + onNotify({ tone: "success", message: `已添加 ${selectedUserIds.length} 名成员` }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "添加成员失败", }); } finally { setSaving(false); @@ -288,6 +448,8 @@ export function SystemAdminPage({ type="text" placeholder="请输入用户名" className="admin-toolbar__input" + value={userSearchTerm} + onChange={(event) => setUserSearchTerm(event.target.value)} /> - - - - ); - })} + {loading ? ( +

正在加载用户…

+ ) : ( + employees + .filter((employee) => { + const term = userSearchTerm.toLowerCase().trim(); + if (!term) return true; + return ( + employee.display_name.toLowerCase().includes(term) || + employee.username.toLowerCase().includes(term) || + (employee.email && employee.email.toLowerCase().includes(term)) + ); + }) + .map((employee) => { + const isProtectedAdmin = employee.role_code === "admin"; + return ( +
+ {employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} + {employee.username} + {employee.role_name} + {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} + + + + +
+ ); + }) + )} {dialogOpen && (
-
EMPLOYEE

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

+
+
+ EMPLOYEE +

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

+
+ +
void submit(event)}> - - - - - {editing && } -
+ + + {!editing && ( + + )} + + + {editing && ( + + )} +
+ + +
)} ) : ( -
- -

项目管理

-

项目管理功能开发中…

-
+ <> + {!canManage &&
当前为开发人员,只能查看项目列表。
} +
+
+ 项目名称 + 项目编码 + 状态 + 配额 + 操作 +
+ {projectLoading ? ( +

正在加载项目…

+ ) : projects.length === 0 ? ( +

暂无项目

+ ) : ( + projects + .filter((project) => { + const term = projectSearchTerm.toLowerCase().trim(); + if (!term) return true; + return ( + project.workspace_name.toLowerCase().includes(term) || + project.workspace_code.toLowerCase().includes(term) || + (project.description && project.description.toLowerCase().includes(term)) + ); + }) + .map((project) => ( +
+ + {project.workspace_name.slice(0, 1)} + + {project.workspace_name} + {project.description ?? "无描述"} + + + {project.workspace_code} + + + {project.status === "active" ? "正常" : project.status === "archived" ? "已归档" : "已删除"} + + + {project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"} + + + + + +
+ )) + )} +
+ )} {projectDialogOpen && (
-
PROJECT

新建项目

+
+
+ PROJECT +

{editingProject ? "编辑项目" : "新建项目"}

+
+ +
void submitProject(event)}> - -