feat(space): 桌面端空间成员管理界面 —— 补齐"临时组队协作"前端闭环

此前能建项目空间/切换/启用全员空间,但邀请队友进空间只有后端 API、无界面,
协作故事前端断了一半。本次补上成员管理 UI,让 Space 协作端到端可用。

- api.ts: spaceMembers/addSpaceMember/setSpaceMemberRole/removeSpaceMember + SpaceMemberInfo
- SpaceMembers 弹窗: 列成员(名/邮箱/角色 badge); 空间 admin/owner 可邀请(邮箱+角色)/
  下拉改角色/移除, owner 受保护(无下拉/移除); 非管理员只读提示。复用 Dialog/Button/Select
- 顶栏加"管理成员"入口(UserCog): 活跃空间为 project/tenant 时显示; App 管弹窗开关,
  传 spaceId/spaceName/canManage(空间角色 admin/owner)/selfUserId

实机验证(Wails 桌面 + 全栈后端): demospace(owner)在项目空间邀请 teammate → 成员列表
即时出现(切换器 1人→2人) → 下拉改 teammate 成员→管理员(服务端确认 owner/admin) →
owner 自身受保护无改删入口。tsc + 48 前端测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-13 14:29:28 +08:00
parent 9dab25a5f0
commit 8f766a75a7
4 changed files with 191 additions and 3 deletions
+34
View File
@@ -212,6 +212,40 @@ export async function createSpace(name: string, kind = "project"): Promise<{ id:
return (await res.json()) as { id: string };
}
// ---- 空间成员管理(邀请/列表/改角色/移除)----
export interface SpaceMemberInfo {
user_id: string;
email: string;
name: string;
role: string; // owner/admin/member/viewer
status: string;
}
export async function spaceMembers(spaceId: string): Promise<SpaceMemberInfo[]> {
const res = guard401(await fetch(`${GATEWAY}/api/v1/spaces/${spaceId}/members`, { headers: bearer() }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `members failed: ${res.status}`);
}
const d = (await res.json()) as { members?: SpaceMemberInfo[] };
return d.members ?? [];
}
async function spaceMemberWrite(method: string, url: string, body?: unknown): Promise<void> {
const res = guard401(await fetch(`${GATEWAY}${url}`, { method, headers: { "Content-Type": "application/json", ...bearer() }, body: body ? JSON.stringify(body) : undefined }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `${method} ${url} failed: ${res.status}`);
}
}
export const addSpaceMember = (spaceId: string, email: string, role: string) =>
spaceMemberWrite("POST", `/api/v1/spaces/${spaceId}/members`, { email, role });
export const setSpaceMemberRole = (spaceId: string, userId: string, role: string) =>
spaceMemberWrite("PUT", `/api/v1/spaces/${spaceId}/members/${userId}`, { role });
export const removeSpaceMember = (spaceId: string, userId: string) =>
spaceMemberWrite("DELETE", `/api/v1/spaces/${spaceId}/members/${userId}`);
// ---- 我的用量明细(当前用户自己租户:余额 + 按天趋势 + 最近消耗)----
export interface UsageDay {
day: string; // YYYYMMDD