From 248267984c546fba1f4270510be101642788d2c8 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 8 Jul 2026 11:02:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E9=A1=B6=E6=A0=8F=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=88=87=E6=8D=A2=E5=99=A8=EF=BC=88=E5=A4=9A=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=EF=BC=89=E2=80=94=E2=80=94=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=B4=BB=E8=B7=83=E7=A7=9F=E6=88=B7=E5=8D=B3=E6=8D=A2=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA+=E8=AE=A1=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户属多个租户时,顶栏显示租户下拉(Building2 图标):切换即调 POST /me/tenant 换活跃租户,随后刷新余额芯片(显示新计费租户的可花余额)。仅 >1 租户时出现。 api 加 myTenants/switchTenant。 live 验证(preview):demoB(属公司A+公司B) 切换器显两租户;从公司A(共享,余额15.84) 切到公司B(个人,余额6.38)→ 后端 active 持久化、芯片随之更新为 6.4(橙,偏低)。tsc+48 测试全过。 Co-Authored-By: Claude Opus 4.8 --- sundynix-desktop/frontend/src/App.tsx | 20 +++++++++++-- sundynix-desktop/frontend/src/lib/api.ts | 26 ++++++++++++++++ .../frontend/src/shell/TopBar.tsx | 30 +++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/sundynix-desktop/frontend/src/App.tsx b/sundynix-desktop/frontend/src/App.tsx index 9869748..d74ef7e 100644 --- a/sundynix-desktop/frontend/src/App.tsx +++ b/sundynix-desktop/frontend/src/App.tsx @@ -15,7 +15,7 @@ import { Placeholder } from "./views/Placeholder"; import { CommandPalette, type Command } from "./components/CommandPalette"; import { UpdateBanner } from "./components/UpdateBanner"; import { Login } from "./views/Login"; -import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, type Identity, type AuthUser, type TenantCtx } from "./lib/api"; +import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, type Identity, type AuthUser, type TenantCtx, type MyTenant } from "./lib/api"; import type { TaskDsl } from "./lib/dsl"; import { emptyRun, type RunState } from "./lib/run"; import { ToastProvider } from "./ui"; @@ -43,6 +43,7 @@ export default function App() { const [view, setView] = useState("home"); const [user, setUser] = useState(null); const [tenant, setTenant] = useState(null); + const [tenants, setTenants] = useState([]); const [authLoading, setAuthLoading] = useState(true); const identity = useMemo(() => ({ userId: user?.id ?? "", sessionId: getSessionId() }), [user]); const [run, setRun] = useState(emptyRun); @@ -92,10 +93,25 @@ export default function App() { tenantCurrent() .then(setTenant) .catch(() => {}); + myTenants() + .then((r) => setTenants(r.tenants)) + .catch(() => {}); }, []); + const onSwitchTenant = useCallback( + async (id: string) => { + try { + await switchTenant(id); + refreshTenant(); + } catch { + /* ignore */ + } + }, + [refreshTenant], + ); useEffect(() => { if (!user) { setTenant(null); + setTenants([]); return; } refreshTenant(); @@ -239,7 +255,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/lib/api.ts b/sundynix-desktop/frontend/src/lib/api.ts index 2d8b47b..abb2284 100644 --- a/sundynix-desktop/frontend/src/lib/api.ts +++ b/sundynix-desktop/frontend/src/lib/api.ts @@ -139,6 +139,32 @@ export async function tenantCurrent(): Promise { }; } +// ---- 我所属的租户 + 切换活跃租户(多租户)---- +export interface MyTenant { + id: string; + name: string; + slug: string; + plan: string; + credit_balance_micro: number; + shared_billing: boolean; + members: number; +} + +export async function myTenants(): Promise<{ tenants: MyTenant[]; active: string }> { + const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenants`, { headers: bearer() })); + if (!res.ok) return { tenants: [], active: "" }; + const d = (await res.json()) as { tenants?: MyTenant[]; active?: string }; + return { tenants: d.tenants ?? [], active: d.active ?? "" }; +} + +export async function switchTenant(tenantId: string): Promise { + const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenant`, { method: "POST", headers: { "Content-Type": "application/json", ...bearer() }, body: JSON.stringify({ tenant_id: tenantId }) })); + if (!res.ok) { + const d = (await res.json().catch(() => ({}))) as { error?: string }; + throw new Error(d.error ?? `switch failed: ${res.status}`); + } +} + // ---- 我的用量明细(当前用户自己租户:余额 + 按天趋势 + 最近消耗)---- export interface UsageDay { day: string; // YYYYMMDD diff --git a/sundynix-desktop/frontend/src/shell/TopBar.tsx b/sundynix-desktop/frontend/src/shell/TopBar.tsx index 3bfafb0..c9209a0 100644 --- a/sundynix-desktop/frontend/src/shell/TopBar.tsx +++ b/sundynix-desktop/frontend/src/shell/TopBar.tsx @@ -1,6 +1,6 @@ import type { CSSProperties } from "react"; -import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins } from "lucide-react"; -import type { AuthUser, TenantCtx } from "../lib/api"; +import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins, Building2 } from "lucide-react"; +import type { AuthUser, TenantCtx, MyTenant } from "../lib/api"; import { useHealth } from "../lib/health"; import { isMacDesktop } from "../lib/desktop"; import { useTheme } from "../lib/theme"; @@ -42,8 +42,31 @@ function CreditChip({ tenant, onClick }: { tenant: TenantCtx; onClick?: () => vo ); } +// 租户切换器:仅当用户属多个租户时显示;切换即改活跃租户(工作区 + 计费上下文)。 +function TenantSwitcher({ tenants, activeId, onSwitch }: { tenants: MyTenant[]; activeId?: string; onSwitch?: (id: string) => void }) { + if (tenants.length < 2) return null; + return ( +
+ + + +
+ ); +} + // 顶栏:品牌 · 垂直切换 · 健康灯 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。 -export function TopBar({ user, tenant, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) { +export function TopBar({ user, tenant, tenants = [], onSwitchTenant, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; tenants?: MyTenant[]; onSwitchTenant?: (id: string) => void; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) { const h = useHealth(); const { theme, toggle } = useTheme(); return ( @@ -86,6 +109,7 @@ export function TopBar({ user, tenant, onLogout, onCommand, onOpenUsage }: { use
+ {tenant?.tenant && }