Files
smart-go/web/components/layout/TenantSwitcher.tsx
T
2026-04-23 18:58:13 +08:00

102 lines
3.6 KiB
TypeScript

'use client';
import * as Select from '@radix-ui/react-select';
import { useCallback, useEffect, useState } from 'react';
import { iamTenant } from '@/lib/api/iam';
import type { IamTenant } from '@/lib/api/types/tenant';
import { useTenantStore } from '@/stores/tenant-store';
import { useTabStore } from '@/stores/tab-store';
/** Radix Select 不允许空字符串作 value,用哨兵表示「默认租户」 */
const DEFAULT_TENANT_VALUE = '__tenant_default__';
export function TenantSwitcher() {
const tenantId = useTenantStore((s) => s.tenantId);
const setTenantId = useTenantStore((s) => s.setTenantId);
const resetTabsForTenantSwitch = useTabStore((s) => s.resetForTenantSwitch);
const [rows, setRows] = useState<IamTenant[] | null>(null);
const [loadErr, setLoadErr] = useState(false);
const load = useCallback(async () => {
try {
const data = await iamTenant.list();
setRows(data.items ?? []);
setLoadErr(false);
} catch {
setLoadErr(true);
setRows([]);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const short = tenantId ? (tenantId.length > 10 ? `${tenantId.slice(0, 8)}` : tenantId) : '默认';
if (loadErr || !rows || rows.length === 0) {
return (
<div
className="hidden max-w-[160px] truncate text-xs text-neutral-500 md:block"
title={tenantId ?? ''}
>
{short}
</div>
);
}
return (
<div className="hidden items-center gap-1 md:flex">
<span className="text-xs text-neutral-500" id="tenant-switcher-label">
</span>
<Select.Root
value={tenantId ?? DEFAULT_TENANT_VALUE}
onValueChange={(v) => {
setTenantId(v === DEFAULT_TENANT_VALUE ? null : v);
resetTabsForTenantSwitch();
}}
>
<Select.Trigger
className="inline-flex max-w-[140px] items-center justify-between gap-1 truncate rounded border border-neutral-200 bg-white px-1.5 py-0.5 text-xs text-neutral-800 outline-none hover:bg-neutral-50 focus-visible:ring-2 focus-visible:ring-neutral-400"
aria-labelledby="tenant-switcher-label"
title={tenantId ?? '默认'}
>
<Select.Value placeholder="默认" />
<Select.Icon aria-hidden>
<span className="text-[10px] text-neutral-500"></span>
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content
className="z-[200] max-h-[min(320px,70vh)] overflow-hidden rounded-md border border-neutral-200 bg-white shadow-lg"
position="popper"
sideOffset={4}
align="start"
>
<Select.Viewport className="p-1">
<Select.Item
value={DEFAULT_TENANT_VALUE}
className="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-xs text-neutral-800 outline-none data-highlighted:bg-neutral-100"
>
<Select.ItemText></Select.ItemText>
</Select.Item>
{rows.map((r) => (
<Select.Item
key={r.id}
value={r.id}
className="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-xs text-neutral-800 outline-none data-highlighted:bg-neutral-100"
>
<Select.ItemText className="truncate">
{r.tenant_name || r.tenant_code || r.id}
</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
);
}