refactor: SchedulesPageRoute.tsx, UserManagementPage.tsx, ProjectManagementPage.tsx
This commit is contained in:
@@ -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<Workspace[]>([]);
|
||||
const [projectLoading, setProjectLoading] = useState(true);
|
||||
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||
const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM);
|
||||
const [editingProject, setEditingProject] = useState<Workspace | null>(null);
|
||||
const [importMemberDialogOpen, setImportMemberDialogOpen] = useState(false);
|
||||
const [selectedProject, setSelectedProject] = useState<Workspace | null>(null);
|
||||
const [availableUsers, setAvailableUsers] = useState<Employee[]>([]);
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
const submitProject = async (event: FormEvent): Promise<void> => {
|
||||
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<void> => {
|
||||
const handleDelete = async (project: Workspace): Promise<void> => {
|
||||
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<void> => {
|
||||
const handleImportMember = async (): Promise<void> => {
|
||||
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)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
@@ -354,7 +318,7 @@ export function ProjectManagementPage({
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", padding: "16px 24px", borderTop: "1px solid #e8e8e8" }}>
|
||||
<button className="secondary-button" type="button" onClick={() => setImportMemberDialogOpen(false)}>取消</button>
|
||||
<button className="primary-button" type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void importMember()}>
|
||||
<button className="primary-button" type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void handleImportMember()}>
|
||||
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||
</button>
|
||||
</div>
|
||||
@@ -363,4 +327,4 @@ export function ProjectManagementPage({
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user