From 9dab25a5f0e9f66044cdd38d56ae89ef848b8285 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 13 Jul 2026 13:55:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(space):=20=E5=85=A8=E5=91=98=E7=A9=BA?= =?UTF-8?q?=E9=97=B4(tenant-kind)=20=E2=80=94=E2=80=94=20=E6=95=B4?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E5=85=B1=E4=BA=AB=E7=9A=84"=E5=BC=80?= =?UTF-8?q?=E5=85=B3"=EF=BC=8C=E8=87=AA=E5=8A=A8=E7=BA=B3=E5=85=A5?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=88=90=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 Space 三类型的最后一种:tenant-kind「全员空间」= 用户最初想要的"共享工作区开关"。 与 project 空间的本质区别:自动纳入全租户成员(含之后新加入的),而非手动逐个邀请。 后端: - EnableTenantSpace(幂等):建 kind=tenant 空间 + 把全部 active 租户成员按租户角色纳入 (owner→owner…billing_admin→viewer);tenantSpaceID 查询;spaceRoleFromTenant 角色映射 - AddMemberByEmail 加租户成员时 autoJoinTenantSpace:若已启用全员空间则自动补纳新成员 - SpaceCreate handler:kind=tenant 路由到 EnableTenantSpace,且须租户 admin/owner(inline 403) 桌面端: - 空间切换器加"启用全员空间"入口(Globe 图标):仅租户 admin/owner 且尚未启用时显示, 带二次确认;启用后该按钮自动隐藏(hasTenantSpace),全员空间进切换器 验证(gateway+docker):member 启用 403 / owner 启用纳入全部现有成员(owner·member·viewer 按租户角色映射) / 新加租户成员自动纳入 / 幂等(再启用返回同一个不重复建) / viewer 在全员空间 入库 403(只读) / owner 建库 200;浏览器实机:启用后全员空间进切换器 + 启用按钮消失 至此 Space 三类型(个人/项目/全员)全部落地。 Co-Authored-By: Claude Opus 4.8 --- sundynix-desktop/frontend/src/App.tsx | 12 +++- .../frontend/src/shell/TopBar.tsx | 21 ++++-- sundynix-gateway/internal/handler/space.go | 24 +++++-- sundynix-gateway/internal/store/space.go | 64 +++++++++++++++++++ sundynix-gateway/internal/store/tenant.go | 2 + 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/sundynix-desktop/frontend/src/App.tsx b/sundynix-desktop/frontend/src/App.tsx index c661413..412ca9d 100644 --- a/sundynix-desktop/frontend/src/App.tsx +++ b/sundynix-desktop/frontend/src/App.tsx @@ -139,6 +139,16 @@ export default function App() { }, [refreshTenant], ); + // 启用全员空间(整租户共享;幂等纳入全部成员)——须租户 admin。 + const onEnableTenantSpace = useCallback(async () => { + try { + const { id } = await createSpace("全员空间", "tenant"); + await switchSpace(id); + refreshTenant(); + } catch (e) { + window.alert((e as Error).message); + } + }, [refreshTenant]); useEffect(() => { if (!user) { setTenant(null); @@ -288,7 +298,7 @@ export default function App() { style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }} /> - setCmdOpen(true)} onOpenUsage={() => setView("usage")} /> + setCmdOpen(true)} onOpenUsage={() => setView("usage")} />
diff --git a/sundynix-desktop/frontend/src/shell/TopBar.tsx b/sundynix-desktop/frontend/src/shell/TopBar.tsx index 486c619..9019443 100644 --- a/sundynix-desktop/frontend/src/shell/TopBar.tsx +++ b/sundynix-desktop/frontend/src/shell/TopBar.tsx @@ -1,5 +1,5 @@ import type { CSSProperties } from "react"; -import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins, Building2, Users as UsersIcon, Plus } from "lucide-react"; +import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins, Building2, Users as UsersIcon, Plus, Globe } from "lucide-react"; import type { AuthUser, TenantCtx, MyTenant, SpaceCtx, MySpace } from "../lib/api"; import { useHealth } from "../lib/health"; import { isMacDesktop } from "../lib/desktop"; @@ -67,9 +67,11 @@ function TenantSwitcher({ tenants, activeId, onSwitch }: { tenants: MyTenant[]; // 工作区切换器:切换活跃 Space(共享工作区 — 个人/项目/全员)。含"新建项目空间"。 // 个人空间恒有,故 ≥1 即显示(让用户随时能建/进项目协作空间)。 -function SpaceSwitcher({ spaces, activeId, onSwitch, onCreate }: { spaces: MySpace[]; activeId?: string; onSwitch?: (id: string) => void; onCreate?: (name: string) => void }) { +function SpaceSwitcher({ spaces, activeId, onSwitch, onCreate, onEnableTenantSpace, tenantRole }: { spaces: MySpace[]; activeId?: string; onSwitch?: (id: string) => void; onCreate?: (name: string) => void; onEnableTenantSpace?: () => void; tenantRole?: string }) { if (spaces.length === 0) return null; const label = (s: MySpace) => (s.kind === "personal" ? "个人空间" : s.name) + (s.kind !== "personal" ? ` · ${s.members}人` : ""); + const canManage = tenantRole === "owner" || tenantRole === "admin"; + const hasTenantSpace = spaces.some((s) => s.kind === "tenant"); return (
@@ -98,12 +100,23 @@ function SpaceSwitcher({ spaces, activeId, onSwitch, onCreate }: { spaces: MySpa > + {canManage && !hasTenantSpace && ( + + )}
); } // 顶栏:品牌 · 垂直切换 · 健康灯 · 租户/工作区切换 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。 -export function TopBar({ user, tenant, tenants = [], onSwitchTenant, space, spaces = [], onSwitchSpace, onCreateSpace, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; tenants?: MyTenant[]; onSwitchTenant?: (id: string) => void; space?: SpaceCtx | null; spaces?: MySpace[]; onSwitchSpace?: (id: string) => void; onCreateSpace?: (name: string) => void; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) { +export function TopBar({ user, tenant, tenants = [], onSwitchTenant, space, spaces = [], onSwitchSpace, onCreateSpace, onEnableTenantSpace, tenantRole, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; tenants?: MyTenant[]; onSwitchTenant?: (id: string) => void; space?: SpaceCtx | null; spaces?: MySpace[]; onSwitchSpace?: (id: string) => void; onCreateSpace?: (name: string) => void; onEnableTenantSpace?: () => void; tenantRole?: string; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) { const h = useHealth(); const { theme, toggle } = useTheme(); return ( @@ -147,7 +160,7 @@ export function TopBar({ user, tenant, tenants = [], onSwitchTenant, space, spac
- + {tenant?.tenant && }