refactor: SchedulesPageRoute.tsx, UserManagementPage.tsx, ProjectManagementPage.tsx

This commit is contained in:
tao.chen
2026-08-07 14:40:46 +08:00
parent ec57fb6c7e
commit 984894d797
8 changed files with 1831 additions and 240 deletions
@@ -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>
);
}
}
@@ -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<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState<Employee | null>(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<void> => {
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<void> => {
const submit = async (event: FormEvent): Promise<void> => {
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<void> => {
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({
<span className="modal__eyebrow">EMPLOYEE</span>
<h2>{editing ? "编辑用户" : "添加用户"}</h2>
</div>
<button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button>
<button className="icon-button" type="button" onClick={() => setUserDialogOpen(false)}><Icon name="close" /></button>
</div>
<form onSubmit={(event) => void submit(event)}>
<label className="form-field">
@@ -230,7 +211,7 @@ export function UserManagementPage({
<input
autoFocus={!editing}
value={form.display_name}
onChange={(event) => setForm({ ...form, display_name: event.target.value })}
onChange={(event) => setUserForm({ ...form, display_name: event.target.value })}
placeholder="请输入姓名"
/>
</label>
@@ -239,7 +220,7 @@ export function UserManagementPage({
<input
disabled={Boolean(editing)}
value={form.username}
onChange={(event) => 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 字符)"
/>
</label>
@@ -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="请输入邮箱(可选)"
/>
</label>
<label className="form-field">
<span></span>
<select value={form.role_code} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
<select value={form.role_code} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setUserForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
<option value="developer"></option>
<option value="admin"></option>
</select>
@@ -276,7 +257,7 @@ export function UserManagementPage({
{editing && (
<label className="form-field">
<span></span>
<select value={form.status} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}>
<select value={form.status} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setUserForm({ ...form, status: event.target.value as typeof form.status })}>
<option value="active"></option>
<option value="disabled"></option>
<option value="locked"></option>
@@ -284,7 +265,7 @@ export function UserManagementPage({
</label>
)}
<div className="modal__footer">
<button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}></button>
<button className="secondary-button" type="button" onClick={() => setUserDialogOpen(false)}></button>
<button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
</div>
</form>
@@ -293,4 +274,4 @@ export function UserManagementPage({
)}
</section>
);
}
}