From 984894d797ca2b5beb6046cc730584b8564d98b4 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:40:46 +0800 Subject: [PATCH] refactor: SchedulesPageRoute.tsx, UserManagementPage.tsx, ProjectManagementPage.tsx --- .../admin/ProjectManagementPage.tsx | 202 ++- .../components/admin/UserManagementPage.tsx | 161 +-- .../app/features/admin/SystemAdminRoute.tsx | 4 - .../app/features/admin/state/adminStore.ts | 476 +++++++ .../app/features/schedules/SchedulePage.tsx | 58 +- .../features/schedules/SchedulesPageRoute.tsx | 4 - .../schedules/state/schedulesStore.ts | 1160 +++++++++++++++++ frontend/app/routes/platform.tsx | 6 + 8 files changed, 1831 insertions(+), 240 deletions(-) create mode 100644 frontend/app/features/admin/state/adminStore.ts create mode 100644 frontend/app/features/schedules/state/schedulesStore.ts diff --git a/frontend/app/components/admin/ProjectManagementPage.tsx b/frontend/app/components/admin/ProjectManagementPage.tsx index 4560c97..728e8bf 100644 --- a/frontend/app/components/admin/ProjectManagementPage.tsx +++ b/frontend/app/components/admin/ProjectManagementPage.tsx @@ -1,79 +1,73 @@ -import { useEffect, useState } from "react"; +import { useEffect, type FormEvent } from "react"; -import { ApiRequestError, type Employee, type Workspace } from "../../services/api"; -import { useApi, useAuth } from "../../context/AuthContext"; +import { type Workspace } from "../../services/api"; +import { useAuth } from "../../context/AuthContext"; import Icon from "../common/Icon"; +import { useAdminStore } from "../../features/admin/state/adminStore"; import "../../styles/admin.css"; import { UserMultiSelect } from "./UserMultiSelect"; -const EMPTY_PROJECT_FORM = { - workspace_code: "", - workspace_name: "", - quota_bytes: 0, - description: "", +type Notice = { + tone: "success" | "error" | "info"; + message: string; }; export function ProjectManagementPage({ onNotify, onConnectionChange, }: { - onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; + onNotify: (notice: Notice) => void; onConnectionChange: (online: boolean) => void; }) { - 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 [selectedRoleCode, setSelectedRoleCode] = useState<"admin" | "developer">("developer"); - const [projectSearchTerm, setProjectSearchTerm] = useState(""); - const [saving, setSaving] = useState(false); + 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 canManage = user?.role_code === "admin"; - 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 : "用户列表加载失败", - }); - } - }; - + const wsId = currentWorkspace?.workspace_id; useEffect(() => { - void loadProjects(); - }, []); + reset(); + void loadProjects(onNotify, onConnectionChange); + }, [loadProjects, reset, wsId, onNotify, onConnectionChange]); const openCreateProject = (): void => { setEditingProject(null); - setProjectForm(EMPTY_PROJECT_FORM); + setProjectForm({ + workspace_code: "", + workspace_name: "", + quota_bytes: 0, + description: "", + }); setProjectDialogOpen(true); }; @@ -88,7 +82,7 @@ export function ProjectManagementPage({ setProjectDialogOpen(true); }; - const submitProject = async (event: React.FormEvent): Promise => { + const submitProject = async (event: FormEvent): Promise => { event.preventDefault(); if (!projectForm.workspace_name.trim()) { onNotify({ tone: "error", message: "请输入项目名称" }); @@ -98,87 +92,57 @@ export function ProjectManagementPage({ onNotify({ tone: "error", message: "请输入项目编码" }); return; } - setSaving(true); - try { - if (editingProject) { - const updated = await api.updateWorkspace(editingProject.workspace_id, { + if (editingProject) { + await updateProject( + editingProject, + { 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({ + 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( + { 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); - } catch (error) { - onNotify({ - tone: "error", - message: error instanceof ApiRequestError ? error.message : (editingProject ? "更新项目失败" : "创建项目失败"), - }); - } finally { - setSaving(false); + description: projectForm.description.trim() || "", + }, + onNotify, + onConnectionChange, + ); } }; - const deleteProject = async (project: Workspace): Promise => { + const handleDelete = 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 : "删除项目失败", - }); - } + await deleteProject(project, onNotify, onConnectionChange); }; const openImportMemberDialog = (project: Workspace): void => { setSelectedProject(project); setSelectedUserIds([]); setSelectedRoleCode("developer"); - void loadAvailableUsers(); - setImportMemberDialogOpen(true); + void openImportMembers(project, onNotify); }; - const importMember = async (): Promise => { + const handleImportMember = 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); - } + if (!selectedProject) return; + await importMembers( + selectedProject, + selectedUserIds, + selectedRoleCode, + onNotify, + ); }; return ( @@ -253,7 +217,7 @@ export function ProjectManagementPage({ className="is-danger" disabled={!canManage || project.status === "disabled"} title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"} - onClick={() => void deleteProject(project)} + onClick={() => void handleDelete(project)} > 删除 @@ -354,7 +318,7 @@ export function ProjectManagementPage({
-
@@ -363,4 +327,4 @@ export function ProjectManagementPage({ )} ); -} +} \ No newline at end of file diff --git a/frontend/app/components/admin/UserManagementPage.tsx b/frontend/app/components/admin/UserManagementPage.tsx index 42a88cd..f69bbf9 100644 --- a/frontend/app/components/admin/UserManagementPage.tsx +++ b/frontend/app/components/admin/UserManagementPage.tsx @@ -1,68 +1,69 @@ -import { useEffect, useState } from "react"; +import { useEffect, type FormEvent } from "react"; -import { ApiRequestError, type Employee } from "../../services/api"; -import { useApi, useAuth } from "../../context/AuthContext"; +import { type Employee } from "../../services/api"; +import { useAuth } from "../../context/AuthContext"; import Icon from "../../components/common/Icon"; +import { useAdminStore } from "../../features/admin/state/adminStore"; import "../../styles/admin.css"; -const EMPTY_FORM = { - username: "", - display_name: "", - email: "", - role_code: "developer" as "admin" | "developer", - password: "", - status: "active" as "active" | "disabled" | "locked", +type Notice = { + tone: "success" | "error" | "info"; + message: string; }; export function UserManagementPage({ onNotify, onConnectionChange, }: { - onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; + onNotify: (notice: Notice) => void; onConnectionChange: (online: boolean) => void; }) { - 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 { 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 canManage = user?.role_code === "admin"; - const load = async (): Promise => { - setLoading(true); - try { - setEmployees(await api.listEmployees()); - onConnectionChange(true); - } catch (error) { - onConnectionChange(false); - onNotify({ - tone: "error", - message: error instanceof Error ? error.message : "用户列表加载失败", - }); - } finally { - setLoading(false); - } - }; - + const wsId = currentWorkspace?.workspace_id; useEffect(() => { - void load(); - }, []); + reset(); + void loadEmployees(); + }, [loadEmployees, reset, wsId]); const openCreate = (): void => { - setEditing(null); - setForm(EMPTY_FORM); - setDialogOpen(true); + setEditingUser(null); + setUserForm({ + username: "", + display_name: "", + email: "", + role_code: "developer", + password: "", + status: "active", + }); + setUserDialogOpen(true); }; const openEdit = (employee: Employee): void => { - setEditing(employee); - setForm({ + setEditingUser(employee); + setUserForm({ username: employee.username, display_name: employee.display_name, email: employee.email ?? "", @@ -70,18 +71,16 @@ export function UserManagementPage({ password: "", status: employee.status, }); - setDialogOpen(true); + setUserDialogOpen(true); }; - const submit = async (event: React.FormEvent): Promise => { + const submit = async (event: 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"; @@ -102,58 +101,40 @@ export function UserManagementPage({ 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) { - const updated = await api.updateEmployee(editing.user_id, { + if (editing) { + await updateEmployee( + editing, + { display_name: form.display_name.trim(), email: form.email.trim() || null, role_code: form.role_code, status: form.status, - }); - setEmployees((current) => current.map( - (item) => item.user_id === updated.user_id ? updated : item, - )); - onNotify({ tone: "success", message: "用户信息已更新" }); - } else { - const created = await api.createEmployee({ + }, + onNotify, + onConnectionChange, + ); + } else { + await 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); - } catch (error) { - onNotify({ - tone: "error", - message: error instanceof ApiRequestError ? error.message : "保存用户失败", - }); - } finally { - setSaving(false); + }, + onNotify, + onConnectionChange, + ); } }; const remove = async (employee: Employee): Promise => { if (!window.confirm(`确定从当前 Workspace 删除用户"${employee.display_name}"吗?`)) return; - try { - await api.deleteEmployee(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 : "删除用户失败", - }); - } + await deleteEmployee(employee, onNotify, onConnectionChange); }; return ( @@ -222,7 +203,7 @@ export function UserManagementPage({ EMPLOYEE

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

- +
void submit(event)}> @@ -239,7 +220,7 @@ export function UserManagementPage({ setForm({ ...form, username: event.target.value })} + onChange={(event) => setUserForm({ ...form, username: event.target.value })} placeholder="请输入登录账号" autoComplete="username" /> @@ -251,7 +232,7 @@ export function UserManagementPage({ type="password" autoComplete="new-password" value={form.password} - onChange={(event) => setForm({ ...form, password: event.target.value })} + onChange={(event) => setUserForm({ ...form, password: event.target.value })} placeholder="请输入密码(8~72 字符)" /> @@ -262,13 +243,13 @@ export function UserManagementPage({ type="email" autoComplete="email" value={form.email} - onChange={(event) => setForm({ ...form, email: event.target.value })} + onChange={(event) => setUserForm({ ...form, email: event.target.value })} placeholder="请输入邮箱(可选)" />