feat:项目管理
This commit is contained in:
@@ -253,5 +253,14 @@ export function useApi(): WorkspaceBoundApi {
|
|||||||
runScheduleNow: (scheduleId) => rawApi.runScheduleNow(workspaceId, scheduleId),
|
runScheduleNow: (scheduleId) => rawApi.runScheduleNow(workspaceId, scheduleId),
|
||||||
listScheduleRuns: (input) => rawApi.listScheduleRuns(workspaceId, input),
|
listScheduleRuns: (input) => rawApi.listScheduleRuns(workspaceId, input),
|
||||||
getScheduleRun: (runId) => rawApi.getScheduleRun(workspaceId, runId),
|
getScheduleRun: (runId) => rawApi.getScheduleRun(workspaceId, runId),
|
||||||
|
// Workspace (Project) Management - 系统管理接口(跨 workspace,不需要传入 workspaceId)
|
||||||
|
listWorkspaces: () => rawApi.listWorkspaces(),
|
||||||
|
createWorkspace: (input) => rawApi.createWorkspace(input),
|
||||||
|
updateWorkspace: (workspaceId, input) => rawApi.updateWorkspace(workspaceId, input),
|
||||||
|
deleteWorkspace: (workspaceId) => rawApi.deleteWorkspace(workspaceId),
|
||||||
|
listWorkspaceMembers: (workspaceId) => rawApi.listWorkspaceMembers(workspaceId),
|
||||||
|
addWorkspaceMember: (workspaceId, input) => rawApi.addWorkspaceMember(workspaceId, input),
|
||||||
|
updateWorkspaceMember: (workspaceId, userId, input) => rawApi.updateWorkspaceMember(workspaceId, userId, input),
|
||||||
|
deleteWorkspaceMember: (workspaceId, userId) => rawApi.deleteWorkspaceMember(workspaceId, userId),
|
||||||
}), [workspaceId]);
|
}), [workspaceId]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { type FormEvent, useEffect, useState } from "react";
|
import { type FormEvent, useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiRequestError,
|
ApiRequestError,
|
||||||
type Employee,
|
type Employee,
|
||||||
|
type Workspace,
|
||||||
|
type WorkspaceMember,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { useApi, useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
@@ -103,14 +105,22 @@ const EMPTY_FORM = {
|
|||||||
display_name: "",
|
display_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
role_code: "developer" as "admin" | "developer",
|
role_code: "developer" as "admin" | "developer",
|
||||||
|
password: "",
|
||||||
status: "active" as "active" | "disabled" | "locked",
|
status: "active" as "active" | "disabled" | "locked",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EMPTY_PROJECT_FORM = {
|
const EMPTY_PROJECT_FORM = {
|
||||||
project_name: "",
|
workspace_code: "",
|
||||||
|
workspace_name: "",
|
||||||
|
quota_bytes: 0,
|
||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProjectWithMembers = Workspace & {
|
||||||
|
member_count: number;
|
||||||
|
members?: WorkspaceMember[];
|
||||||
|
};
|
||||||
|
|
||||||
export function SystemAdminPage({
|
export function SystemAdminPage({
|
||||||
onNotify,
|
onNotify,
|
||||||
onConnectionChange,
|
onConnectionChange,
|
||||||
@@ -127,8 +137,21 @@ export function SystemAdminPage({
|
|||||||
const [editing, setEditing] = useState<Employee | null>(null);
|
const [editing, setEditing] = useState<Employee | null>(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [form, setForm] = useState(EMPTY_FORM);
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
|
const [userSearchTerm, setUserSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// 项目管理状态
|
||||||
|
const [projects, setProjects] = useState<Workspace[]>([]);
|
||||||
|
const [projectLoading, setProjectLoading] = useState(true);
|
||||||
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||||
const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM);
|
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 canManage = user?.role_code === "admin";
|
const canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
const load = async (): Promise<void> => {
|
const load = async (): Promise<void> => {
|
||||||
@@ -147,10 +170,45 @@ export function SystemAdminPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 : "用户列表加载失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "projects") {
|
||||||
|
void loadProjects();
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
const openCreate = (): void => {
|
const openCreate = (): void => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
setForm(EMPTY_FORM);
|
setForm(EMPTY_FORM);
|
||||||
@@ -164,6 +222,7 @@ export function SystemAdminPage({
|
|||||||
display_name: employee.display_name,
|
display_name: employee.display_name,
|
||||||
email: employee.email ?? "",
|
email: employee.email ?? "",
|
||||||
role_code: employee.role_code,
|
role_code: employee.role_code,
|
||||||
|
password: "",
|
||||||
status: employee.status,
|
status: employee.status,
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
@@ -172,6 +231,16 @@ export function SystemAdminPage({
|
|||||||
const submit = async (event: React.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()) {
|
||||||
|
onNotify({ tone: "error", message: "请输入密码" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 密码长度校验 8~72 字符
|
||||||
|
if (form.password && (form.password.length < 8 || form.password.length > 72)) {
|
||||||
|
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
@@ -184,17 +253,19 @@ export function SystemAdminPage({
|
|||||||
setEmployees((current) => current.map(
|
setEmployees((current) => current.map(
|
||||||
(item) => item.user_id === updated.user_id ? updated : item,
|
(item) => item.user_id === updated.user_id ? updated : item,
|
||||||
));
|
));
|
||||||
|
onNotify({ tone: "success", message: "用户信息已更新" });
|
||||||
} else {
|
} else {
|
||||||
const created = await api.createEmployee({
|
const created = await api.createEmployee({
|
||||||
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() || null,
|
||||||
role_code: form.role_code,
|
role_code: form.role_code,
|
||||||
|
password: form.password,
|
||||||
});
|
});
|
||||||
setEmployees((current) => [...current, created]);
|
setEmployees((current) => [...current, created]);
|
||||||
|
onNotify({ tone: "success", message: "用户已添加" });
|
||||||
}
|
}
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
onNotify({ tone: "success", message: editing ? "用户信息已更新" : "用户已添加" });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify({
|
onNotify({
|
||||||
tone: "error",
|
tone: "error",
|
||||||
@@ -220,23 +291,112 @@ export function SystemAdminPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openCreateProject = (): void => {
|
const openCreateProject = (): void => {
|
||||||
|
setEditingProject(null);
|
||||||
setProjectForm(EMPTY_PROJECT_FORM);
|
setProjectForm(EMPTY_PROJECT_FORM);
|
||||||
setProjectDialogOpen(true);
|
setProjectDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditProject = (project: Workspace): void => {
|
||||||
|
setEditingProject(project);
|
||||||
|
setProjectForm({
|
||||||
|
workspace_code: project.workspace_code,
|
||||||
|
workspace_name: project.workspace_name,
|
||||||
|
quota_bytes: project.quota_bytes,
|
||||||
|
description: project.description ?? "",
|
||||||
|
});
|
||||||
|
setProjectDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const submitProject = async (event: React.FormEvent): Promise<void> => {
|
const submitProject = async (event: React.FormEvent): Promise<void> => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!projectForm.project_name.trim()) return;
|
if (!projectForm.workspace_name.trim()) {
|
||||||
|
onNotify({ tone: "error", message: "请输入项目名称" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!projectForm.workspace_code.trim() && !editingProject) {
|
||||||
|
onNotify({ tone: "error", message: "请输入项目编码" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
// TODO: 调用实际的项目创建 API
|
if (editingProject) {
|
||||||
// await api.createProject({ ... })
|
// 编辑项目
|
||||||
|
const updated = await api.updateWorkspace(editingProject.workspace_id, {
|
||||||
|
workspace_name: projectForm.workspace_name.trim(),
|
||||||
|
quota_bytes: projectForm.quota_bytes,
|
||||||
|
description: projectForm.description.trim() || undefined,
|
||||||
|
});
|
||||||
|
setProjects((current) =>
|
||||||
|
current.map((p) => (p.workspace_id === updated.workspace_id ? updated : p))
|
||||||
|
);
|
||||||
|
onNotify({ tone: "success", message: "项目信息已更新" });
|
||||||
|
} else {
|
||||||
|
// 创建项目
|
||||||
|
const generatedCode = projectForm.workspace_code.trim() || projectForm.workspace_name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 32);
|
||||||
|
const created = await api.createWorkspace({
|
||||||
|
workspace_code: generatedCode,
|
||||||
|
workspace_name: projectForm.workspace_name.trim(),
|
||||||
|
quota_bytes: projectForm.quota_bytes,
|
||||||
|
description: projectForm.description.trim() || undefined,
|
||||||
|
});
|
||||||
|
setProjects((current) => [...current, created]);
|
||||||
|
onNotify({ tone: "success", message: "项目已创建" });
|
||||||
|
}
|
||||||
setProjectDialogOpen(false);
|
setProjectDialogOpen(false);
|
||||||
onNotify({ tone: "success", message: "项目已创建" });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify({
|
onNotify({
|
||||||
tone: "error",
|
tone: "error",
|
||||||
message: error instanceof ApiRequestError ? error.message : "创建项目失败",
|
message: error instanceof ApiRequestError ? error.message : (editingProject ? "更新项目失败" : "创建项目失败"),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProject = 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 : "删除项目失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openImportMemberDialog = (project: Workspace): void => {
|
||||||
|
setSelectedProject(project);
|
||||||
|
setSelectedUserIds([]);
|
||||||
|
setSelectedRoleCode("developer");
|
||||||
|
void loadAvailableUsers();
|
||||||
|
setImportMemberDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importMember = 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 {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
@@ -288,6 +448,8 @@ export function SystemAdminPage({
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
className="admin-toolbar__input"
|
className="admin-toolbar__input"
|
||||||
|
value={userSearchTerm}
|
||||||
|
onChange={(event) => setUserSearchTerm(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -310,6 +472,8 @@ export function SystemAdminPage({
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入项目名称"
|
placeholder="请输入项目名称"
|
||||||
className="admin-toolbar__input"
|
className="admin-toolbar__input"
|
||||||
|
value={projectSearchTerm}
|
||||||
|
onChange={(event) => setProjectSearchTerm(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -329,59 +493,427 @@ export function SystemAdminPage({
|
|||||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
||||||
<div className="employee-table">
|
<div className="employee-table">
|
||||||
<div className="employee-table__head"><span>用户</span><span>账号</span><span>角色</span><span>状态</span><span>操作</span></div>
|
<div className="employee-table__head"><span>用户</span><span>账号</span><span>角色</span><span>状态</span><span>操作</span></div>
|
||||||
{loading ? <p className="admin-empty">正在加载用户…</p> : employees.map((employee) => {
|
{loading ? (
|
||||||
const isProtectedAdmin = employee.role_code === "admin";
|
<p className="admin-empty">正在加载用户…</p>
|
||||||
return (
|
) : (
|
||||||
<div className="employee-row" key={employee.user_id}>
|
employees
|
||||||
<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>
|
.filter((employee) => {
|
||||||
<code>{employee.username}</code>
|
const term = userSearchTerm.toLowerCase().trim();
|
||||||
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
|
if (!term) return true;
|
||||||
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
return (
|
||||||
<span className="employee-actions">
|
employee.display_name.toLowerCase().includes(term) ||
|
||||||
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
employee.username.toLowerCase().includes(term) ||
|
||||||
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}>删除</button>
|
(employee.email && employee.email.toLowerCase().includes(term))
|
||||||
</span>
|
);
|
||||||
</div>
|
})
|
||||||
);
|
.map((employee) => {
|
||||||
})}
|
const isProtectedAdmin = employee.role_code === "admin";
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<code>{employee.username}</code>
|
||||||
|
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
|
||||||
|
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
||||||
|
<span className="employee-actions">
|
||||||
|
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
||||||
|
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}>删除</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dialogOpen && (
|
{dialogOpen && (
|
||||||
<div className="modal-backdrop">
|
<div className="modal-backdrop">
|
||||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||||
<div className="modal__header"><div><span className="modal__eyebrow">EMPLOYEE</span><h2>{editing ? "编辑用户" : "添加用户"}</h2></div><button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button></div>
|
<div className="modal__header">
|
||||||
|
<div>
|
||||||
|
<span className="modal__eyebrow">EMPLOYEE</span>
|
||||||
|
<h2>{editing ? "编辑用户" : "添加用户"}</h2>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button>
|
||||||
|
</div>
|
||||||
<form onSubmit={(event) => void submit(event)}>
|
<form onSubmit={(event) => void submit(event)}>
|
||||||
<label className="form-field"><span>姓名<span className="required">*</span></span><input autoFocus value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /></label>
|
<label className="form-field">
|
||||||
<label className="form-field"><span>登录账号<span className="required">*</span></span><input disabled={Boolean(editing)} value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} /></label>
|
<span>姓名<span className="required">*</span></span>
|
||||||
<label className="form-field"><span>邮箱</span><input type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /></label>
|
<input
|
||||||
<label className="form-field"><span>角色</span><select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}><option value="developer">开发人员</option><option value="admin">管理员</option></select></label>
|
autoFocus={!editing}
|
||||||
{editing && <label className="form-field"><span>状态</span><select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}><option value="active">正常</option><option value="disabled">停用</option><option value="locked">锁定</option></select></label>}
|
value={form.display_name}
|
||||||
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button></div>
|
onChange={(event) => setForm({ ...form, display_name: event.target.value })}
|
||||||
|
placeholder="请输入姓名"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
<span>登录账号<span className="required">*</span></span>
|
||||||
|
<input
|
||||||
|
disabled={Boolean(editing)}
|
||||||
|
value={form.username}
|
||||||
|
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||||
|
placeholder="请输入登录账号"
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{!editing && (
|
||||||
|
<label className="form-field">
|
||||||
|
<span>密码<span className="required">*</span></span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
||||||
|
placeholder="请输入密码(8~72 字符)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<label className="form-field">
|
||||||
|
<span>邮箱</span>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
||||||
|
placeholder="请输入邮箱(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
<span>角色</span>
|
||||||
|
<select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
|
||||||
|
<option value="developer">开发人员</option>
|
||||||
|
<option value="admin">管理员</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{editing && (
|
||||||
|
<label className="form-field">
|
||||||
|
<span>状态</span>
|
||||||
|
<select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}>
|
||||||
|
<option value="active">正常</option>
|
||||||
|
<option value="disabled">停用</option>
|
||||||
|
<option value="locked">锁定</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="modal__footer">
|
||||||
|
<button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button>
|
||||||
|
<button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="project-list-placeholder">
|
<>
|
||||||
<Icon name="folder" size={42} />
|
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看项目列表。</div>}
|
||||||
<h3>项目管理</h3>
|
<div className="employee-table">
|
||||||
<p>项目管理功能开发中…</p>
|
<div className="employee-table__head">
|
||||||
</div>
|
<span>项目名称</span>
|
||||||
|
<span>项目编码</span>
|
||||||
|
<span>状态</span>
|
||||||
|
<span>配额</span>
|
||||||
|
<span>操作</span>
|
||||||
|
</div>
|
||||||
|
{projectLoading ? (
|
||||||
|
<p className="admin-empty">正在加载项目…</p>
|
||||||
|
) : projects.length === 0 ? (
|
||||||
|
<p className="admin-empty">暂无项目</p>
|
||||||
|
) : (
|
||||||
|
projects
|
||||||
|
.filter((project) => {
|
||||||
|
const term = projectSearchTerm.toLowerCase().trim();
|
||||||
|
if (!term) return true;
|
||||||
|
return (
|
||||||
|
project.workspace_name.toLowerCase().includes(term) ||
|
||||||
|
project.workspace_code.toLowerCase().includes(term) ||
|
||||||
|
(project.description && project.description.toLowerCase().includes(term))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((project) => (
|
||||||
|
<div className="employee-row" key={project.workspace_id}>
|
||||||
|
<span className="employee-name">
|
||||||
|
<b className="avatar">{project.workspace_name.slice(0, 1)}</b>
|
||||||
|
<span>
|
||||||
|
<strong>{project.workspace_name}</strong>
|
||||||
|
<small>{project.description ?? "无描述"}</small>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<code>{project.workspace_code}</code>
|
||||||
|
<span>
|
||||||
|
<span className={`status-pill is-${project.status}`}>
|
||||||
|
{project.status === "active" ? "正常" : project.status === "archived" ? "已归档" : "已删除"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"}</span>
|
||||||
|
<span className="employee-actions">
|
||||||
|
<button type="button" disabled={!canManage} onClick={() => openEditProject(project)}>编辑</button>
|
||||||
|
<button type="button" disabled={!canManage} onClick={() => openImportMemberDialog(project)}>导入成员</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="is-danger"
|
||||||
|
disabled={!canManage || project.status === "disabled"}
|
||||||
|
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
||||||
|
onClick={() => void deleteProject(project)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projectDialogOpen && (
|
{projectDialogOpen && (
|
||||||
<div className="modal-backdrop">
|
<div className="modal-backdrop">
|
||||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||||
<div className="modal__header"><div><span className="modal__eyebrow">PROJECT</span><h2>新建项目</h2></div><button className="icon-button" type="button" onClick={() => setProjectDialogOpen(false)}><Icon name="close" /></button></div>
|
<div className="modal__header">
|
||||||
|
<div>
|
||||||
|
<span className="modal__eyebrow">PROJECT</span>
|
||||||
|
<h2>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" type="button" onClick={() => setProjectDialogOpen(false)}><Icon name="close" /></button>
|
||||||
|
</div>
|
||||||
<form onSubmit={(event) => void submitProject(event)}>
|
<form onSubmit={(event) => void submitProject(event)}>
|
||||||
<label className="form-field"><span>项目名称<span className="required">*</span></span><input autoFocus value={projectForm.project_name} onChange={(event) => setProjectForm({ ...projectForm, project_name: event.target.value })} placeholder="请输入项目名称" /></label>
|
{!editingProject && (
|
||||||
<label className="form-field"><span>项目描述</span><textarea value={projectForm.description} onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })} placeholder="请输入项目描述(可选)" rows={4} /></label>
|
<label className="form-field">
|
||||||
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setProjectDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "创建中…" : "创建"}</button></div>
|
<span>项目编码<span className="required">*</span></span>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={projectForm.workspace_code}
|
||||||
|
onChange={(event) => setProjectForm({ ...projectForm, workspace_code: event.target.value })}
|
||||||
|
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<label className="form-field">
|
||||||
|
<span>项目名称<span className="required">*</span></span>
|
||||||
|
<input
|
||||||
|
autoFocus={!editingProject}
|
||||||
|
value={projectForm.workspace_name}
|
||||||
|
onChange={(event) => setProjectForm({ ...projectForm, workspace_name: event.target.value })}
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
<span>配额(字节)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={projectForm.quota_bytes}
|
||||||
|
onChange={(event) => setProjectForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
|
||||||
|
placeholder="0 表示无配额限制"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
<span>项目描述</span>
|
||||||
|
<textarea
|
||||||
|
value={projectForm.description}
|
||||||
|
onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })}
|
||||||
|
placeholder="请输入项目描述(可选)"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="modal__footer">
|
||||||
|
<button className="secondary-button" type="button" onClick={() => setProjectDialogOpen(false)}>取消</button>
|
||||||
|
<button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{importMemberDialogOpen && (
|
||||||
|
<div className="modal-backdrop">
|
||||||
|
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||||
|
<div className="modal__header">
|
||||||
|
<div>
|
||||||
|
<span className="modal__eyebrow">IMPORT MEMBER</span>
|
||||||
|
<h2>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" type="button" onClick={() => setImportMemberDialogOpen(false)}><Icon name="close" /></button>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "16px 24px 16px" }}>
|
||||||
|
<label className="form-field">
|
||||||
|
<span>选择用户<span className="required">*</span></span>
|
||||||
|
{/* 多选下拉框 - 带 tag 显示 */}
|
||||||
|
<UserMultiSelect
|
||||||
|
users={availableUsers}
|
||||||
|
selectedUserIds={selectedUserIds}
|
||||||
|
onChange={setSelectedUserIds}
|
||||||
|
/>
|
||||||
|
</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 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()}>
|
||||||
|
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 多选用户下拉框组件 - 带 tag 显示
|
||||||
|
function UserMultiSelect({
|
||||||
|
users,
|
||||||
|
selectedUserIds,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
users: Employee[];
|
||||||
|
selectedUserIds: string[];
|
||||||
|
onChange: (ids: string[]) => void;
|
||||||
|
}) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 点击外部关闭下拉框
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedUsers = users.filter((user) => selectedUserIds.includes(user.user_id));
|
||||||
|
|
||||||
|
const toggleUser = (userId: string) => {
|
||||||
|
if (selectedUserIds.includes(userId)) {
|
||||||
|
onChange(selectedUserIds.filter((id) => id !== userId));
|
||||||
|
} else {
|
||||||
|
onChange([...selectedUserIds, userId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUser = (userId: string, event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onChange(selectedUserIds.filter((id) => id !== userId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} style={{ position: "relative" }}>
|
||||||
|
{/* 选择框 - 显示 tag */}
|
||||||
|
<div
|
||||||
|
className="admin-toolbar__input"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "4px",
|
||||||
|
padding: "6px 32px 6px 8px",
|
||||||
|
minHeight: "36px",
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedUsers.length === 0 ? (
|
||||||
|
<span style={{ color: "#999" }}>请选择用户</span>
|
||||||
|
) : (
|
||||||
|
selectedUsers.map((user) => (
|
||||||
|
<span
|
||||||
|
key={user.user_id}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#e6f7ff",
|
||||||
|
color: "#1890ff",
|
||||||
|
border: "1px solid #91d5ff",
|
||||||
|
borderRadius: "2px",
|
||||||
|
padding: "2px 6px",
|
||||||
|
fontSize: "13px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.display_name}
|
||||||
|
<span
|
||||||
|
onClick={(e) => removeUser(user.user_id, e)}
|
||||||
|
style={{
|
||||||
|
marginLeft: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "12px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
color: "#999",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isOpen ? "▲" : "▼"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 下拉列表 */}
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
maxHeight: "200px",
|
||||||
|
overflowY: "auto",
|
||||||
|
border: "1px solid #1890ff",
|
||||||
|
borderRadius: "4px",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
zIndex: 1000,
|
||||||
|
marginTop: "2px",
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{users.map((user) => {
|
||||||
|
const isSelected = selectedUserIds.includes(user.user_id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={user.user_id}
|
||||||
|
onClick={() => toggleUser(user.user_id)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "8px 12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: isSelected ? "#e6f7ff" : "transparent",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{user.display_name} <span style={{ color: "#999" }}>({user.username})</span>
|
||||||
|
</span>
|
||||||
|
{isSelected && <span style={{ color: "#1890ff" }}>✓</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,55 @@ export type Employee = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Workspace (Project) types - 对应 API.md 第七部分系统管理接口
|
||||||
|
export type Workspace = {
|
||||||
|
workspace_id: string;
|
||||||
|
workspace_code: string;
|
||||||
|
workspace_name: string;
|
||||||
|
active_root_uri: string;
|
||||||
|
quota_bytes: number;
|
||||||
|
status: "active" | "archived" | "disabled";
|
||||||
|
description: string | null;
|
||||||
|
created_by: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceMember = {
|
||||||
|
user_id: string;
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
email: string | null;
|
||||||
|
role_code: "admin" | "developer";
|
||||||
|
role_name: string;
|
||||||
|
member_status: "active" | "disabled" | "locked";
|
||||||
|
joined_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceCreatePayload = {
|
||||||
|
workspace_code: string;
|
||||||
|
workspace_name: string;
|
||||||
|
quota_bytes?: number;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceUpdatePayload = {
|
||||||
|
workspace_name?: string;
|
||||||
|
quota_bytes?: number;
|
||||||
|
description?: string;
|
||||||
|
status?: "active" | "archived";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceMemberAddPayload = {
|
||||||
|
user_id: string;
|
||||||
|
role_code: "admin" | "developer";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceMemberUpdatePayload = {
|
||||||
|
role_code?: "admin" | "developer";
|
||||||
|
member_status?: "active" | "disabled" | "locked";
|
||||||
|
};
|
||||||
|
|
||||||
export type ScriptItem = {
|
export type ScriptItem = {
|
||||||
script_id: string;
|
script_id: string;
|
||||||
workspace_id: string;
|
workspace_id: string;
|
||||||
@@ -850,6 +899,7 @@ export async function createEmployee(
|
|||||||
display_name: string;
|
display_name: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
role_code: "admin" | "developer";
|
role_code: "admin" | "developer";
|
||||||
|
password: string;
|
||||||
},
|
},
|
||||||
): Promise<Employee> {
|
): Promise<Employee> {
|
||||||
return apiRequest<Employee>(
|
return apiRequest<Employee>(
|
||||||
@@ -887,6 +937,84 @@ export async function deleteEmployee(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Workspace (Project) Management APIs - 对应 API.md 第七部分系统管理接口
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function listWorkspaces(): Promise<Workspace[]> {
|
||||||
|
return apiRequest<Workspace[]>("/api/v1/platform/workspaces");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWorkspace(
|
||||||
|
input: WorkspaceCreatePayload,
|
||||||
|
): Promise<Workspace> {
|
||||||
|
return apiRequest<Workspace>(
|
||||||
|
"/api/v1/platform/workspaces",
|
||||||
|
{ method: "POST", body: JSON.stringify(input) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkspace(workspaceId: string): Promise<Workspace> {
|
||||||
|
return apiRequest<Workspace>(`/api/v1/platform/workspaces/${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWorkspace(
|
||||||
|
workspaceId: string,
|
||||||
|
input: WorkspaceUpdatePayload,
|
||||||
|
): Promise<Workspace> {
|
||||||
|
return apiRequest<Workspace>(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}`,
|
||||||
|
{ method: "PATCH", body: JSON.stringify(input) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWorkspace(workspaceId: string): Promise<{
|
||||||
|
workspace_id: string;
|
||||||
|
deleted: boolean;
|
||||||
|
}> {
|
||||||
|
return apiRequest(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
|
||||||
|
return apiRequest<WorkspaceMember[]>(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}/members`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addWorkspaceMember(
|
||||||
|
workspaceId: string,
|
||||||
|
input: WorkspaceMemberAddPayload,
|
||||||
|
): Promise<WorkspaceMember> {
|
||||||
|
return apiRequest<WorkspaceMember>(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}/members`,
|
||||||
|
{ method: "POST", body: JSON.stringify(input) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWorkspaceMember(
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string,
|
||||||
|
input: WorkspaceMemberUpdatePayload,
|
||||||
|
): Promise<WorkspaceMember> {
|
||||||
|
return apiRequest<WorkspaceMember>(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}/members/${userId}`,
|
||||||
|
{ method: "PATCH", body: JSON.stringify(input) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWorkspaceMember(
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string,
|
||||||
|
): Promise<{ user_id: string; deleted: boolean }> {
|
||||||
|
return apiRequest(
|
||||||
|
`/api/v1/platform/workspaces/${workspaceId}/members/${userId}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function createScheduleNode(
|
export async function createScheduleNode(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
@@ -1177,4 +1305,26 @@ export type WorkspaceBoundApi = {
|
|||||||
input?: Parameters<typeof listScheduleRuns>[1],
|
input?: Parameters<typeof listScheduleRuns>[1],
|
||||||
) => Promise<ScheduleRunSummary[]>;
|
) => Promise<ScheduleRunSummary[]>;
|
||||||
getScheduleRun: (runId: string) => Promise<ScheduleRunDetail>;
|
getScheduleRun: (runId: string) => Promise<ScheduleRunDetail>;
|
||||||
|
// Workspace (Project) Management - 系统管理接口
|
||||||
|
listWorkspaces: () => Promise<Workspace[]>;
|
||||||
|
createWorkspace: (input: WorkspaceCreatePayload) => Promise<Workspace>;
|
||||||
|
updateWorkspace: (
|
||||||
|
workspaceId: string,
|
||||||
|
input: WorkspaceUpdatePayload,
|
||||||
|
) => Promise<Workspace>;
|
||||||
|
deleteWorkspace: (workspaceId: string) => Promise<{ workspace_id: string; deleted: boolean }>;
|
||||||
|
listWorkspaceMembers: (workspaceId: string) => Promise<WorkspaceMember[]>;
|
||||||
|
addWorkspaceMember: (
|
||||||
|
workspaceId: string,
|
||||||
|
input: WorkspaceMemberAddPayload,
|
||||||
|
) => Promise<WorkspaceMember>;
|
||||||
|
updateWorkspaceMember: (
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string,
|
||||||
|
input: WorkspaceMemberUpdatePayload,
|
||||||
|
) => Promise<WorkspaceMember>;
|
||||||
|
deleteWorkspaceMember: (
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string,
|
||||||
|
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user