Merge remote-tracking branch 'aliyun/develop' into develop
This commit is contained in:
@@ -1,73 +1,67 @@
|
|||||||
import { useEffect, type FormEvent } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { type Workspace } from "../../services/api";
|
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import Icon from "../common/Icon";
|
import Icon from "../common/Icon";
|
||||||
import { useAdminStore } from "../../features/admin/state/adminStore";
|
|
||||||
|
|
||||||
import "../../styles/admin.css";
|
|
||||||
import { UserMultiSelect } from "./UserMultiSelect";
|
import { UserMultiSelect } from "./UserMultiSelect";
|
||||||
|
|
||||||
type Notice = {
|
const EMPTY_PROJECT_FORM = {
|
||||||
tone: "success" | "error" | "info";
|
workspace_code: "",
|
||||||
message: string;
|
workspace_name: "",
|
||||||
|
quota_bytes: 0,
|
||||||
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProjectManagementPage({
|
export function ProjectManagementPage({
|
||||||
onNotify,
|
onNotify,
|
||||||
onConnectionChange,
|
onConnectionChange,
|
||||||
}: {
|
}: {
|
||||||
onNotify: (notice: Notice) => void;
|
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
|
||||||
onConnectionChange: (online: boolean) => void;
|
onConnectionChange: (online: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { user, currentWorkspace } = useAuth();
|
const api = useApi();
|
||||||
|
const { user } = useAuth();
|
||||||
const projects = useAdminStore((s) => s.projects);
|
const [projects, setProjects] = useState<Workspace[]>([]);
|
||||||
const projectLoading = useAdminStore((s) => s.projectsLoading);
|
const [projectLoading, setProjectLoading] = useState(true);
|
||||||
const saving = useAdminStore((s) => s.projectsSaving);
|
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||||
const editingProject = useAdminStore((s) => s.editingProject);
|
const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM);
|
||||||
const projectDialogOpen = useAdminStore((s) => s.projectDialogOpen);
|
const [editingProject, setEditingProject] = useState<Workspace | null>(null);
|
||||||
const projectForm = useAdminStore((s) => s.projectForm);
|
const [importMemberDialogOpen, setImportMemberDialogOpen] = useState(false);
|
||||||
const importMemberDialogOpen = useAdminStore((s) => s.importMemberDialogOpen);
|
const [selectedProject, setSelectedProject] = useState<Workspace | null>(null);
|
||||||
const selectedProject = useAdminStore((s) => s.selectedProject);
|
const [availableUsers, setAvailableUsers] = useState<Employee[]>([]);
|
||||||
const availableUsers = useAdminStore((s) => s.availableUsers);
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||||
const selectedUserIds = useAdminStore((s) => s.selectedUserIds);
|
const [projectSearchTerm, setProjectSearchTerm] = useState("");
|
||||||
const selectedRoleCode = useAdminStore((s) => s.selectedRoleCode);
|
const [saving, setSaving] = useState(false);
|
||||||
const projectSearchTerm = useAdminStore((s) => s.projectSearchTerm);
|
const [membersDrawerOpen, setMembersDrawerOpen] = useState(false);
|
||||||
|
const [currentProjectMembers, setCurrentProjectMembers] = useState<WorkspaceMember[]>([]);
|
||||||
const setProjectDialogOpen = useAdminStore((s) => s.setProjectDialogOpen);
|
const [membersLoading, setMembersLoading] = useState(false);
|
||||||
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 canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
const wsId = currentWorkspace?.workspace_id;
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
void loadProjects();
|
||||||
void loadProjects(onNotify, onConnectionChange);
|
}, []);
|
||||||
}, [loadProjects, reset, wsId, onNotify, onConnectionChange]);
|
|
||||||
|
|
||||||
const openCreateProject = (): void => {
|
const openCreateProject = (): void => {
|
||||||
setEditingProject(null);
|
setEditingProject(null);
|
||||||
setProjectForm({
|
setProjectForm(EMPTY_PROJECT_FORM);
|
||||||
workspace_code: "",
|
|
||||||
workspace_name: "",
|
|
||||||
quota_bytes: 0,
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
setProjectDialogOpen(true);
|
setProjectDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +76,7 @@ export function ProjectManagementPage({
|
|||||||
setProjectDialogOpen(true);
|
setProjectDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitProject = async (event: FormEvent): Promise<void> => {
|
const submitProject = async (event: React.FormEvent): Promise<void> => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!projectForm.workspace_name.trim()) {
|
if (!projectForm.workspace_name.trim()) {
|
||||||
onNotify({ tone: "error", message: "请输入项目名称" });
|
onNotify({ tone: "error", message: "请输入项目名称" });
|
||||||
@@ -92,57 +86,158 @@ export function ProjectManagementPage({
|
|||||||
onNotify({ tone: "error", message: "请输入项目编码" });
|
onNotify({ tone: "error", message: "请输入项目编码" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
if (editingProject) {
|
if (editingProject) {
|
||||||
await updateProject(
|
const updated = await api.updateWorkspace(editingProject.workspace_id, {
|
||||||
editingProject,
|
|
||||||
{
|
|
||||||
workspace_name: projectForm.workspace_name.trim(),
|
workspace_name: projectForm.workspace_name.trim(),
|
||||||
quota_bytes: projectForm.quota_bytes,
|
quota_bytes: projectForm.quota_bytes,
|
||||||
description: projectForm.description.trim() || "",
|
description: projectForm.description.trim() || undefined,
|
||||||
},
|
});
|
||||||
onNotify,
|
setProjects((current) =>
|
||||||
onConnectionChange,
|
current.map((p) => (p.workspace_id === updated.workspace_id ? updated : p))
|
||||||
);
|
);
|
||||||
|
onNotify({ tone: "success", message: "项目信息已更新" });
|
||||||
} else {
|
} else {
|
||||||
const generatedCode = projectForm.workspace_code.trim()
|
const generatedCode = projectForm.workspace_code.trim() || projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32);
|
||||||
|| projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32);
|
const created = await api.createWorkspace({
|
||||||
await createProject(
|
|
||||||
{
|
|
||||||
workspace_code: generatedCode,
|
workspace_code: generatedCode,
|
||||||
workspace_name: projectForm.workspace_name.trim(),
|
workspace_name: projectForm.workspace_name.trim(),
|
||||||
quota_bytes: projectForm.quota_bytes,
|
quota_bytes: projectForm.quota_bytes,
|
||||||
description: projectForm.description.trim() || "",
|
description: projectForm.description.trim() || undefined,
|
||||||
},
|
});
|
||||||
onNotify,
|
setProjects((current) => [...current, created]);
|
||||||
onConnectionChange,
|
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<void> => {
|
const deleteProject = async (project: Workspace): Promise<void> => {
|
||||||
if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return;
|
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<void> => {
|
||||||
setSelectedProject(project);
|
setSelectedProject(project);
|
||||||
setSelectedUserIds([]);
|
setSelectedUserIds([]);
|
||||||
setSelectedRoleCode("developer");
|
setImportMemberDialogOpen(true);
|
||||||
void openImportMembers(project, onNotify);
|
|
||||||
|
// 加载可用用户和项目成员
|
||||||
|
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<void> => {
|
const openMembersDrawer = (project: Workspace): void => {
|
||||||
|
setSelectedProject(project);
|
||||||
|
setMembersDrawerOpen(true);
|
||||||
|
void loadProjectMembers(project.workspace_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProjectMembers = async (workspaceId: string): Promise<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
if (!selectedProject || selectedUserIds.length === 0) {
|
if (!selectedProject || selectedUserIds.length === 0) {
|
||||||
onNotify({ tone: "error", message: "请选择要添加的用户" });
|
onNotify({ tone: "error", message: "请选择要添加的用户" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedProject) return;
|
setSaving(true);
|
||||||
await importMembers(
|
try {
|
||||||
selectedProject,
|
await Promise.all(
|
||||||
selectedUserIds,
|
selectedUserIds.map((userId) =>
|
||||||
selectedRoleCode,
|
api.addWorkspaceMember(selectedProject.workspace_id, {
|
||||||
onNotify,
|
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 (
|
return (
|
||||||
@@ -210,14 +305,15 @@ export function ProjectManagementPage({
|
|||||||
</span>
|
</span>
|
||||||
<span>{project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"}</span>
|
<span>{project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"}</span>
|
||||||
<span className="employee-actions">
|
<span className="employee-actions">
|
||||||
|
<button type="button" disabled={!canManage} onClick={() => openMembersDrawer(project)}>成员</button>
|
||||||
<button type="button" disabled={!canManage} onClick={() => openEditProject(project)}>编辑</button>
|
<button type="button" disabled={!canManage} onClick={() => openEditProject(project)}>编辑</button>
|
||||||
<button type="button" disabled={!canManage} onClick={() => openImportMemberDialog(project)}>导入成员</button>
|
{/* <button type="button" disabled={!canManage} onClick={() => openImportMemberDialog(project)}>导入成员</button> */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="is-danger"
|
className="is-danger"
|
||||||
disabled={!canManage || project.status === "disabled"}
|
disabled={!canManage || project.status === "disabled"}
|
||||||
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
||||||
onClick={() => void handleDelete(project)}
|
onClick={() => void deleteProject(project)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
@@ -295,36 +391,163 @@ export function ProjectManagementPage({
|
|||||||
</div>
|
</div>
|
||||||
<button className="icon-button" type="button" onClick={() => setImportMemberDialogOpen(false)}><Icon name="close" /></button>
|
<button className="icon-button" type="button" onClick={() => setImportMemberDialogOpen(false)}><Icon name="close" /></button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: "16px 24px 16px" }}>
|
<div style={{ padding: "20px 24px 110px" }}>
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>选择用户<span className="required">*</span></span>
|
<span>选择用户<span className="required">*</span></span>
|
||||||
<UserMultiSelect
|
<UserMultiSelect
|
||||||
users={availableUsers}
|
users={availableUsers}
|
||||||
selectedUserIds={selectedUserIds}
|
selectedUserIds={selectedUserIds}
|
||||||
onChange={setSelectedUserIds}
|
onChange={setSelectedUserIds}
|
||||||
|
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-field" style={{ marginTop: "12px" }}>
|
|
||||||
<span>角色<span className="required">*</span></span>
|
|
||||||
<select
|
|
||||||
value={selectedRoleCode}
|
|
||||||
onChange={(event) => setSelectedRoleCode(event.target.value as "admin" | "developer")}
|
|
||||||
style={{ marginTop: "4px" }}
|
|
||||||
>
|
|
||||||
<option value="developer">开发人员</option>
|
|
||||||
<option value="admin">管理员</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", padding: "16px 24px", borderTop: "1px solid #e8e8e8" }}>
|
<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="secondary-button" type="button" onClick={() => setImportMemberDialogOpen(false)}>取消</button>
|
||||||
<button className="primary-button" type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void handleImportMember()}>
|
<button className="primary-button" type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void importMember()}>
|
||||||
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{membersDrawerOpen && selectedProject && (
|
||||||
|
<>
|
||||||
|
<div className="drawer-overlay" onClick={() => setMembersDrawerOpen(false)} style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.3)",
|
||||||
|
zIndex: 99,
|
||||||
|
}} />
|
||||||
|
<aside className="drawer" style={{
|
||||||
|
position: "fixed",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 480,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
boxShadow: "-2px 0 8px rgba(0,0,0,0.1)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
zIndex: 100,
|
||||||
|
}}>
|
||||||
|
<div className="drawer__header" style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "16px 20px",
|
||||||
|
borderBottom: "1px solid #e8e8e8",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<span className="modal__eyebrow" style={{ fontSize: 12, color: "#666" }}>MEMBERS</span>
|
||||||
|
<h3 style={{ margin: 0, fontSize: 18 }}>项目成员</h3>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: 13, color: "#888" }}>{selectedProject.workspace_name}</p>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" type="button" onClick={() => setMembersDrawerOpen(false)}>
|
||||||
|
<Icon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
padding: "12px 20px",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 14, color: "#666" }}>
|
||||||
|
共 {currentProjectMembers.length} 名成员
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setMembersDrawerOpen(false);
|
||||||
|
openImportMemberDialog(selectedProject);
|
||||||
|
}}
|
||||||
|
style={{ fontSize: 13, padding: "6px 12px" }}
|
||||||
|
>
|
||||||
|
<Icon name="plus" size={14} /> 添加成员
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflow: "auto", padding: "12px 20px" }}>
|
||||||
|
{membersLoading ? (
|
||||||
|
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>加载中…</p>
|
||||||
|
) : currentProjectMembers.length === 0 ? (
|
||||||
|
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>暂无成员</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{currentProjectMembers.map((member) => (
|
||||||
|
<div
|
||||||
|
key={member.user_id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px",
|
||||||
|
border: "1px solid #e8e8e8",
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="avatar"
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#1890ff",
|
||||||
|
color: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{member.display_name.slice(0, 1)}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 2, fontSize: 13 }}>{member.display_name}</div>
|
||||||
|
<div style={{ fontSize: 11, color: "#888" }}>{member.username}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
{(() => {
|
||||||
|
const isAdmin = member.role_code === "admin";
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void removeMember(member.user_id)}
|
||||||
|
style={{
|
||||||
|
padding: "3px 8px",
|
||||||
|
fontSize: 11,
|
||||||
|
color: isAdmin ? "#999" : "#ff4d4f",
|
||||||
|
border: "1px solid " + (isAdmin ? "#ddd" : "#ff4d4f"),
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
cursor: isAdmin ? "not-allowed" : canManage ? "pointer" : "not-allowed",
|
||||||
|
}}
|
||||||
|
disabled={!canManage || isAdmin}
|
||||||
|
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,69 +1,66 @@
|
|||||||
import { useEffect, type FormEvent } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { type Employee } from "../../services/api";
|
import { ApiRequestError, type Employee } from "../../services/api";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
import { useAdminStore } from "../../features/admin/state/adminStore";
|
|
||||||
|
|
||||||
import "../../styles/admin.css";
|
const EMPTY_FORM = {
|
||||||
|
username: "",
|
||||||
type Notice = {
|
display_name: "",
|
||||||
tone: "success" | "error" | "info";
|
email: "",
|
||||||
message: string;
|
role_code: "developer" as "admin" | "developer",
|
||||||
|
password: "",
|
||||||
|
status: "active" as "active" | "disabled" | "locked",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UserManagementPage({
|
export function UserManagementPage({
|
||||||
onNotify,
|
onNotify,
|
||||||
onConnectionChange,
|
onConnectionChange,
|
||||||
}: {
|
}: {
|
||||||
onNotify: (notice: Notice) => void;
|
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
|
||||||
onConnectionChange: (online: boolean) => void;
|
onConnectionChange: (online: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { user, currentWorkspace } = useAuth();
|
const api = useApi();
|
||||||
|
const { user } = useAuth();
|
||||||
const employees = useAdminStore((s) => s.employees);
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||||
const loading = useAdminStore((s) => s.usersLoading);
|
const [loading, setLoading] = useState(true);
|
||||||
const saving = useAdminStore((s) => s.usersSaving);
|
const [saving, setSaving] = useState(false);
|
||||||
const editing = useAdminStore((s) => s.editingUser);
|
const [editing, setEditing] = useState<Employee | null>(null);
|
||||||
const dialogOpen = useAdminStore((s) => s.userDialogOpen);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const form = useAdminStore((s) => s.userForm);
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
const userSearchTerm = useAdminStore((s) => s.userSearchTerm);
|
const [userSearchTerm, setUserSearchTerm] = useState("");
|
||||||
|
|
||||||
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 canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
const wsId = currentWorkspace?.workspace_id;
|
const load = async (): Promise<void> => {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
reset();
|
void load();
|
||||||
void loadEmployees();
|
}, []);
|
||||||
}, [loadEmployees, reset, wsId]);
|
|
||||||
|
|
||||||
const openCreate = (): void => {
|
const openCreate = (): void => {
|
||||||
setEditingUser(null);
|
setEditing(null);
|
||||||
setUserForm({
|
setForm(EMPTY_FORM);
|
||||||
username: "",
|
setDialogOpen(true);
|
||||||
display_name: "",
|
|
||||||
email: "",
|
|
||||||
role_code: "developer",
|
|
||||||
password: "",
|
|
||||||
status: "active",
|
|
||||||
});
|
|
||||||
setUserDialogOpen(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (employee: Employee): void => {
|
const openEdit = (employee: Employee): void => {
|
||||||
setEditingUser(employee);
|
setEditing(employee);
|
||||||
setUserForm({
|
setForm({
|
||||||
username: employee.username,
|
username: employee.username,
|
||||||
display_name: employee.display_name,
|
display_name: employee.display_name,
|
||||||
email: employee.email ?? "",
|
email: employee.email ?? "",
|
||||||
@@ -71,16 +68,18 @@ export function UserManagementPage({
|
|||||||
password: "",
|
password: "",
|
||||||
status: employee.status,
|
status: employee.status,
|
||||||
});
|
});
|
||||||
setUserDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async (event: FormEvent): Promise<void> => {
|
const submit = async (event: React.FormEvent): Promise<void> => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
|
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
|
||||||
|
// 新建用户时密码必填
|
||||||
if (!editing && !form.password.trim()) {
|
if (!editing && !form.password.trim()) {
|
||||||
onNotify({ tone: "error", message: "请输入密码" });
|
onNotify({ tone: "error", message: "请输入密码" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 编辑自身或管理员时,前端二次拦截禁用 status / role_code 的修改
|
||||||
if (editing) {
|
if (editing) {
|
||||||
const editingSelf = editing.user_id === user?.user_id;
|
const editingSelf = editing.user_id === user?.user_id;
|
||||||
const targetIsAdmin = editing.role_code === "admin";
|
const targetIsAdmin = editing.role_code === "admin";
|
||||||
@@ -101,40 +100,58 @@ export function UserManagementPage({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 密码长度校验 8~72 字符
|
||||||
if (form.password && (form.password.length < 8 || form.password.length > 72)) {
|
if (form.password && (form.password.length < 8 || form.password.length > 72)) {
|
||||||
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
|
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await updateEmployee(
|
const updated = await api.updatePlatformEmployee(editing.user_id, {
|
||||||
editing,
|
|
||||||
{
|
|
||||||
display_name: form.display_name.trim(),
|
display_name: form.display_name.trim(),
|
||||||
email: form.email.trim() || null,
|
email: form.email.trim() || null,
|
||||||
role_code: form.role_code,
|
role_code: form.role_code,
|
||||||
status: form.status,
|
status: form.status,
|
||||||
},
|
});
|
||||||
onNotify,
|
setEmployees((current) => current.map(
|
||||||
onConnectionChange,
|
(item) => item.user_id === updated.user_id ? updated : item,
|
||||||
);
|
));
|
||||||
|
onNotify({ tone: "success", message: "用户信息已更新" });
|
||||||
} else {
|
} else {
|
||||||
await createEmployee(
|
const created = await api.createPlatformEmployee({
|
||||||
{
|
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
display_name: form.display_name.trim(),
|
display_name: form.display_name.trim(),
|
||||||
email: form.email.trim() || null,
|
email: form.email.trim() || undefined,
|
||||||
role_code: form.role_code,
|
|
||||||
password: form.password,
|
password: form.password,
|
||||||
},
|
role_code: "developer",
|
||||||
onNotify,
|
});
|
||||||
onConnectionChange,
|
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<void> => {
|
const remove = async (employee: Employee): Promise<void> => {
|
||||||
if (!window.confirm(`确定从当前 Workspace 删除用户"${employee.display_name}"吗?`)) return;
|
if (!window.confirm(`确定从平台删除用户"${employee.display_name}"吗?`)) return;
|
||||||
await deleteEmployee(employee, onNotify, onConnectionChange);
|
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 (
|
return (
|
||||||
@@ -183,7 +200,7 @@ export function UserManagementPage({
|
|||||||
<div className="employee-row" key={employee.user_id}>
|
<div className="employee-row" key={employee.user_id}>
|
||||||
<span className="employee-name"><b className="avatar">{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span>
|
<span className="employee-name"><b className="avatar">{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span>
|
||||||
<code>{employee.username}</code>
|
<code>{employee.username}</code>
|
||||||
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
|
{employee.role_code ? (<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>) : (<span>-</span>)}
|
||||||
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
||||||
<span className="employee-actions">
|
<span className="employee-actions">
|
||||||
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
||||||
@@ -203,7 +220,7 @@ export function UserManagementPage({
|
|||||||
<span className="modal__eyebrow">EMPLOYEE</span>
|
<span className="modal__eyebrow">EMPLOYEE</span>
|
||||||
<h2>{editing ? "编辑用户" : "添加用户"}</h2>
|
<h2>{editing ? "编辑用户" : "添加用户"}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button" type="button" onClick={() => setUserDialogOpen(false)}><Icon name="close" /></button>
|
<button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={(event) => void submit(event)}>
|
<form onSubmit={(event) => void submit(event)}>
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
@@ -211,7 +228,7 @@ export function UserManagementPage({
|
|||||||
<input
|
<input
|
||||||
autoFocus={!editing}
|
autoFocus={!editing}
|
||||||
value={form.display_name}
|
value={form.display_name}
|
||||||
onChange={(event) => setUserForm({ ...form, display_name: event.target.value })}
|
onChange={(event) => setForm({ ...form, display_name: event.target.value })}
|
||||||
placeholder="请输入姓名"
|
placeholder="请输入姓名"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -220,7 +237,7 @@ export function UserManagementPage({
|
|||||||
<input
|
<input
|
||||||
disabled={Boolean(editing)}
|
disabled={Boolean(editing)}
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(event) => setUserForm({ ...form, username: event.target.value })}
|
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||||
placeholder="请输入登录账号"
|
placeholder="请输入登录账号"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
@@ -232,7 +249,7 @@ export function UserManagementPage({
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(event) => setUserForm({ ...form, password: event.target.value })}
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||||
placeholder="请输入密码(8~72 字符)"
|
placeholder="请输入密码(8~72 字符)"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -243,21 +260,21 @@ export function UserManagementPage({
|
|||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={(event) => setUserForm({ ...form, email: event.target.value })}
|
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
||||||
placeholder="请输入邮箱(可选)"
|
placeholder="请输入邮箱(可选)"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-field">
|
{/* <label className="form-field">
|
||||||
<span>角色</span>
|
<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) => setUserForm({ ...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) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
|
||||||
<option value="developer">开发人员</option>
|
<option value="developer">开发人员</option>
|
||||||
<option value="admin">管理员</option>
|
<option value="admin">管理员</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label> */}
|
||||||
{editing && (
|
{editing && (
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
<span>状态</span>
|
<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) => setUserForm({ ...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) => setForm({ ...form, status: event.target.value as typeof form.status })}>
|
||||||
<option value="active">正常</option>
|
<option value="active">正常</option>
|
||||||
<option value="disabled">停用</option>
|
<option value="disabled">停用</option>
|
||||||
<option value="locked">锁定</option>
|
<option value="locked">锁定</option>
|
||||||
@@ -265,7 +282,7 @@ export function UserManagementPage({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<div className="modal__footer">
|
<div className="modal__footer">
|
||||||
<button className="secondary-button" type="button" onClick={() => setUserDialogOpen(false)}>取消</button>
|
<button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button>
|
||||||
<button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
<button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ export function UserMultiSelect({
|
|||||||
users,
|
users,
|
||||||
selectedUserIds,
|
selectedUserIds,
|
||||||
onChange,
|
onChange,
|
||||||
|
existingMemberIds,
|
||||||
}: {
|
}: {
|
||||||
users: Employee[];
|
users: Employee[];
|
||||||
selectedUserIds: string[];
|
selectedUserIds: string[];
|
||||||
onChange: (ids: string[]) => void;
|
onChange: (ids: string[]) => void;
|
||||||
|
existingMemberIds?: string[];
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -114,39 +116,55 @@ export function UserMultiSelect({
|
|||||||
top: "100%",
|
top: "100%",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
maxHeight: "200px",
|
maxHeight: "136px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
border: "1px solid #1890ff",
|
border: "1px solid #1890ff",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
zIndex: 1000,
|
zIndex: 100,
|
||||||
marginTop: "2px",
|
marginTop: "2px",
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{users.map((user) => {
|
{users.length === 0 ? (
|
||||||
|
<div style={{ padding: "12px", color: "#999", textAlign: "center", fontSize: "13px" }}>
|
||||||
|
所有可选用户已在项目中
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
users.map((user) => {
|
||||||
const isSelected = selectedUserIds.includes(user.user_id);
|
const isSelected = selectedUserIds.includes(user.user_id);
|
||||||
|
const isExisting = existingMemberIds?.includes(user.user_id);
|
||||||
|
const isDisabled = isExisting;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.user_id}
|
key={user.user_id}
|
||||||
onClick={() => toggleUser(user.user_id)}
|
onClick={() => !isDisabled && toggleUser(user.user_id)}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "8px 12px",
|
padding: "6px 12px",
|
||||||
cursor: "pointer",
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||||
backgroundColor: isSelected ? "#e6f7ff" : "transparent",
|
backgroundColor: isSelected ? "#e6f7ff" : isExisting ? "#f5f5f5" : "transparent",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
opacity: isDisabled ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{user.display_name} <span style={{ color: "#999" }}>({user.username})</span>
|
{user.display_name} <span style={{ color: "#999" }}>({user.username})</span>
|
||||||
</span>
|
</span>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||||
|
{isExisting && (
|
||||||
|
<span style={{ fontSize: "11px", color: "#999", backgroundColor: "#f0f0f0", padding: "1px 6px", borderRadius: "2px" }}>
|
||||||
|
已在项目中
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isSelected && <span style={{ color: "#1890ff" }}>✓</span>}
|
{isSelected && <span style={{ color: "#1890ff" }}>✓</span>}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,10 +235,15 @@ export function useApi(): WorkspaceBoundApi {
|
|||||||
hideScheduleArtifact: (versionsId) =>
|
hideScheduleArtifact: (versionsId) =>
|
||||||
rawApi.hideScheduleArtifact(workspaceId, versionsId),
|
rawApi.hideScheduleArtifact(workspaceId, versionsId),
|
||||||
listEmployees: () => rawApi.listEmployees(workspaceId),
|
listEmployees: () => rawApi.listEmployees(workspaceId),
|
||||||
|
listPlatformEmployees: () => rawApi.listPlatformEmployees(),
|
||||||
createEmployee: (input) => rawApi.createEmployee(workspaceId, input),
|
createEmployee: (input) => rawApi.createEmployee(workspaceId, input),
|
||||||
|
createPlatformEmployee: (input) => rawApi.createPlatformEmployee(input),
|
||||||
updateEmployee: (userId, input) =>
|
updateEmployee: (userId, input) =>
|
||||||
rawApi.updateEmployee(workspaceId, userId, input),
|
rawApi.updateEmployee(workspaceId, userId, input),
|
||||||
|
updatePlatformEmployee: (userId, input) =>
|
||||||
|
rawApi.updatePlatformEmployee(userId, input),
|
||||||
deleteEmployee: (userId) => rawApi.deleteEmployee(workspaceId, userId),
|
deleteEmployee: (userId) => rawApi.deleteEmployee(workspaceId, userId),
|
||||||
|
deletePlatformEmployee: (userId) => rawApi.deletePlatformEmployee(userId),
|
||||||
createScheduleNode: (scheduleId, input) =>
|
createScheduleNode: (scheduleId, input) =>
|
||||||
rawApi.createScheduleNode(workspaceId, scheduleId, input),
|
rawApi.createScheduleNode(workspaceId, scheduleId, input),
|
||||||
updateScheduleNode: (scheduleId, nodeId, input) =>
|
updateScheduleNode: (scheduleId, nodeId, input) =>
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export type WorkspaceUpdatePayload = {
|
|||||||
|
|
||||||
export type WorkspaceMemberAddPayload = {
|
export type WorkspaceMemberAddPayload = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
role_code: "admin" | "developer";
|
role_code?: "admin" | "developer";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceMemberUpdatePayload = {
|
export type WorkspaceMemberUpdatePayload = {
|
||||||
@@ -902,10 +902,57 @@ export async function hideScheduleArtifact(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统管理级别接口 - 不区分 workspace
|
||||||
|
export async function listPlatformEmployees(): Promise<Employee[]> {
|
||||||
|
return apiRequest<Employee[]>("/api/v1/platform/employees");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPlatformEmployee(
|
||||||
|
input: {
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
email?: string | undefined;
|
||||||
|
password: string;
|
||||||
|
role_code?: "admin" | "developer";
|
||||||
|
},
|
||||||
|
): Promise<Employee> {
|
||||||
|
return apiRequest<Employee>(
|
||||||
|
"/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<Employee> {
|
||||||
|
return apiRequest<Employee>(
|
||||||
|
`/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<Employee[]> {
|
export async function listEmployees(workspaceId: string): Promise<Employee[]> {
|
||||||
return apiRequest<Employee[]>("/api/v1/admin/employees", {}, workspaceId);
|
return apiRequest<Employee[]>("/api/v1/admin/employees", {}, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated 使用 createPlatformEmployee 代替 */
|
||||||
export async function createEmployee(
|
export async function createEmployee(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
input: {
|
input: {
|
||||||
@@ -923,6 +970,7 @@ export async function createEmployee(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated 使用 updatePlatformEmployee 代替 */
|
||||||
export async function updateEmployee(
|
export async function updateEmployee(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -940,6 +988,7 @@ export async function updateEmployee(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated 使用 deletePlatformEmployee 代替 */
|
||||||
export async function deleteEmployee(
|
export async function deleteEmployee(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -1287,16 +1336,38 @@ export type WorkspaceBoundApi = {
|
|||||||
artifact_preserved: boolean;
|
artifact_preserved: boolean;
|
||||||
}>;
|
}>;
|
||||||
listEmployees: () => Promise<Employee[]>;
|
listEmployees: () => Promise<Employee[]>;
|
||||||
|
listPlatformEmployees: () => Promise<Employee[]>;
|
||||||
createEmployee: (
|
createEmployee: (
|
||||||
input: Parameters<typeof createEmployee>[1],
|
input: Parameters<typeof createEmployee>[1],
|
||||||
) => Promise<Employee>;
|
) => Promise<Employee>;
|
||||||
|
createPlatformEmployee: (
|
||||||
|
input: {
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
email?: string | undefined;
|
||||||
|
password: string;
|
||||||
|
role_code?: "admin" | "developer";
|
||||||
|
},
|
||||||
|
) => Promise<Employee>;
|
||||||
updateEmployee: (
|
updateEmployee: (
|
||||||
userId: string,
|
userId: string,
|
||||||
input: Parameters<typeof updateEmployee>[2],
|
input: Parameters<typeof updateEmployee>[2],
|
||||||
) => Promise<Employee>;
|
) => Promise<Employee>;
|
||||||
|
updatePlatformEmployee: (
|
||||||
|
userId: string,
|
||||||
|
input: {
|
||||||
|
display_name?: string;
|
||||||
|
email?: string | null;
|
||||||
|
role_code?: "admin" | "developer";
|
||||||
|
status?: "active" | "disabled" | "locked";
|
||||||
|
},
|
||||||
|
) => Promise<Employee>;
|
||||||
deleteEmployee: (
|
deleteEmployee: (
|
||||||
userId: string,
|
userId: string,
|
||||||
) => Promise<{ user_id: string; deleted: boolean }>;
|
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||||
|
deletePlatformEmployee: (
|
||||||
|
userId: string,
|
||||||
|
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||||
createScheduleNode: (
|
createScheduleNode: (
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
input: Parameters<typeof createScheduleNode>[2],
|
input: Parameters<typeof createScheduleNode>[2],
|
||||||
|
|||||||
@@ -1541,7 +1541,7 @@ button {
|
|||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
width: min(520px, calc(100vw - 40px));
|
width: min(520px, calc(100vw - 40px));
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
border: 1px solid #dce4eb;
|
border: 1px solid #dce4eb;
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -1550,7 +1550,25 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal--compact {
|
.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 {
|
.modal__header {
|
||||||
@@ -1576,6 +1594,8 @@ button {
|
|||||||
|
|
||||||
.modal form {
|
.modal form {
|
||||||
padding: 19px 22px 0;
|
padding: 19px 22px 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.destination-chip {
|
.destination-chip {
|
||||||
|
|||||||
Reference in New Issue
Block a user