收口 Makelore 客户端变更
This commit is contained in:
1 parent
253bad8b40
commit
f4113a872f
232 files changed
+4617
-20121
No files matched your search
+112
-135
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { DisclosureContent } from '@/components/ui/disclosure';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
@@ -28,6 +30,7 @@ import { fetchWorksTokenUsage, type WorksTokenUsage } from '@/lib/works-square';
|
||||
import { isWorksTokenUsageExhausted } from '@/lib/works-square-token-usage';
|
||||
import { openWorksSquareSubscriptionUpgrade } from '@/lib/subscription-upgrade';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SidebarPeekSource } from './sidebar-peek';
|
||||
import { ModuleSwitcher } from './ModuleSwitcher';
|
||||
import { ImageWorkspaceSidebar } from './ImageWorkspaceSidebar';
|
||||
import { SidebarUpdateButton } from './SidebarUpdateButton';
|
||||
@@ -36,7 +39,6 @@ import { useAuthStore, type AuthUser } from '@/stores/auth';
|
||||
import { useOpencodeStore, type ProjectDirectoryMode } from '@/stores/opencode';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import type { OpencodeProject } from '@/types/opencode';
|
||||
import { buildConfiguredModelOptions } from '@/lib/model-options';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { EMPTY_AGENT_USER_PROFILE, getProfileAccountKey, isUserProfileComplete, useUserProfileStore, type UserGender } from '@/stores/user-profile';
|
||||
import type { ProjectType } from '../../../shared/project-config';
|
||||
@@ -119,11 +121,12 @@ function getSubscriptionMembershipLabel(user: AuthUser | null, state: TokenUsage
|
||||
}
|
||||
|
||||
type SidebarProps = {
|
||||
workspaceLayout?: boolean;
|
||||
sidebarPeekOpen?: boolean;
|
||||
onSidebarPeekChange?: (open: boolean) => void;
|
||||
onSidebarPeekChange?: (open: boolean, source: SidebarPeekSource) => void;
|
||||
};
|
||||
|
||||
export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: SidebarProps = {}) {
|
||||
export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSidebarPeekChange }: SidebarProps = {}) {
|
||||
const { t } = useTranslation('common');
|
||||
const sidebarPinnedCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const sidebarCollapsed = sidebarPinnedCollapsed && !sidebarPeekOpen;
|
||||
@@ -145,9 +148,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
const saveUserProfile = useUserProfileStore((state) => state.saveProfile);
|
||||
const syncUserProfile = useUserProfileStore((state) => state.syncProfile);
|
||||
const saveUserProfileToServer = useUserProfileStore((state) => state.saveProfileToServer);
|
||||
const providerAccounts = useProviderStore((state) => state.accounts);
|
||||
const providerStatuses = useProviderStore((state) => state.statuses);
|
||||
const providerDefaultAccountId = useProviderStore((state) => state.defaultAccountId);
|
||||
const refreshProviderSnapshot = useProviderStore((state) => state.refreshProviderSnapshot);
|
||||
const [projectsOpen, setProjectsOpen] = useState(true);
|
||||
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
||||
@@ -164,7 +164,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
const [newProjectSelectedPath, setNewProjectSelectedPath] = useState('');
|
||||
const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState<ProjectDirectoryMode>('use-selected-directory');
|
||||
const [newProjectType, setNewProjectType] = useState<ProjectType>('mini_game');
|
||||
const [newProjectDefaultModel, setNewProjectDefaultModel] = useState('');
|
||||
const [createProjectError, setCreateProjectError] = useState<string | null>(null);
|
||||
const [creatingProject, setCreatingProject] = useState(false);
|
||||
const [projectEntryError, setProjectEntryError] = useState<ProjectEntryError | null>(null);
|
||||
@@ -176,10 +175,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
const location = useLocation();
|
||||
const isPaintingModule = getAiModuleForPath(location.pathname) === 'painting';
|
||||
const projectConfigPath = '/project-config';
|
||||
const configuredModelOptions = useMemo(
|
||||
() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId),
|
||||
[providerAccounts, providerDefaultAccountId, providerStatuses],
|
||||
);
|
||||
const visibleProjects = opencodeProjects;
|
||||
const selectedProjectFolderName = getFolderName(newProjectSelectedPath);
|
||||
const accountName = getAuthUserDisplayName(authUser) || '未登录用户';
|
||||
@@ -394,7 +389,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
setNewProjectSelectedPath('');
|
||||
setNewProjectDirectoryMode('use-selected-directory');
|
||||
setNewProjectType('mini_game');
|
||||
setNewProjectDefaultModel('');
|
||||
setCreateProjectError(null);
|
||||
setCreateDialogOpen(true);
|
||||
};
|
||||
@@ -403,7 +397,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
if (creatingProject) return;
|
||||
setCreateDialogOpen(false);
|
||||
setNewProjectType('mini_game');
|
||||
setNewProjectDefaultModel('');
|
||||
setCreateProjectError(null);
|
||||
};
|
||||
|
||||
@@ -466,10 +459,8 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
selectedPath,
|
||||
projectType: newProjectType,
|
||||
...(newProjectDirectoryMode === 'create-child-directory' ? { projectName } : {}),
|
||||
...(newProjectDefaultModel ? { defaultModel: newProjectDefaultModel } : {}),
|
||||
});
|
||||
setCreateDialogOpen(false);
|
||||
setNewProjectDefaultModel('');
|
||||
await enterProject(project);
|
||||
} catch (error) {
|
||||
setCreateProjectError(error instanceof Error ? error.message : String(error));
|
||||
@@ -521,19 +512,29 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const sidebarPeekPreview = sidebarPinnedCollapsed && sidebarPeekOpen;
|
||||
|
||||
return (
|
||||
<aside
|
||||
data-testid="sidebar"
|
||||
aria-hidden={sidebarCollapsed}
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true)}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false)}
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
||||
className={cn(
|
||||
'flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 bg-surface-sidebar text-foreground transition-[width,opacity,box-shadow] duration-200 ease-spring motion-reduce:transition-none',
|
||||
sidebarCollapsed ? 'pointer-events-none w-0 border-r-0 opacity-0' : 'w-64 opacity-100',
|
||||
sidebarPinnedCollapsed && sidebarPeekOpen && 'absolute inset-y-0 left-0 z-40 shadow-float',
|
||||
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground transition-[width,opacity,box-shadow] duration-300 ease-spring motion-reduce:transition-none',
|
||||
sidebarCollapsed
|
||||
? 'pointer-events-none w-0 border-r-0 opacity-0'
|
||||
: workspaceLayout
|
||||
? 'w-[256px] min-w-[256px] basis-[256px] opacity-100'
|
||||
: 'w-64 opacity-100',
|
||||
// Keep the collapsed preview in the same fixed layer while it opens
|
||||
// and closes. This makes the hover state cover the titlebar and the
|
||||
// full window height without reflowing the page underneath it.
|
||||
sidebarPinnedCollapsed && 'fixed inset-y-0 left-0 z-[350]',
|
||||
sidebarPeekPreview && 'sidebar-peek-surface pt-10 shadow-float',
|
||||
)}
|
||||
>
|
||||
<div className={cn('bg-surface-sidebar px-3 py-2 text-foreground', sidebarCollapsed && 'px-2')}>
|
||||
<div className={cn('bg-transparent px-3 py-2 text-foreground', sidebarCollapsed && 'px-2')}>
|
||||
<div className="min-w-0 w-full">
|
||||
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />
|
||||
</div>
|
||||
@@ -575,8 +576,8 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
)}
|
||||
</button>
|
||||
|
||||
{projectsOpen && !sidebarCollapsed ? (
|
||||
<div className="mt-1 space-y-0.5">
|
||||
{!sidebarCollapsed ? (
|
||||
<DisclosureContent open={projectsOpen} className="mt-1" innerClassName="space-y-1.5 px-1 py-1">
|
||||
{visibleProjects.length === 0 ? (
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground">
|
||||
暂无真实项目。添加本地项目后即可开始配置。
|
||||
@@ -587,49 +588,42 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
const projectConfig = projectConfigs[project.id];
|
||||
const active = activeProject?.id === project.id;
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={project.id}
|
||||
data-testid={`sidebar-course-project-${project.id}`}
|
||||
aria-label={`进入项目 ${project.name}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={() => void openProject(project)}
|
||||
className={cn(
|
||||
'motion-press w-full rounded-lg px-2 py-1.5 text-left transition-[background-color,color]',
|
||||
'motion-press group/project flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left transition-[background-color,color,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1 active:scale-[0.985]',
|
||||
active
|
||||
? 'bg-brand-soft text-foreground'
|
||||
: 'bg-transparent text-foreground hover:bg-surface-subtle',
|
||||
? 'bg-brand-selected text-foreground shadow-soft ring-1 ring-brand/25'
|
||||
: 'bg-transparent text-foreground hover:bg-surface-tertiary',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">{project.name}</span>
|
||||
<div className="flex shrink-0 -space-x-1.5" aria-label="项目 Agent">
|
||||
{projectConfig?.agents.map((agent) => (
|
||||
<span
|
||||
key={agent.id}
|
||||
title={agent.name.trim() || agent.roleName}
|
||||
className="h-5 w-5 overflow-hidden rounded-full border border-border bg-white"
|
||||
>
|
||||
<img
|
||||
src={getAgentAvatarSrc(agent.avatarId)}
|
||||
alt=""
|
||||
className="h-full w-full object-cover [image-rendering:pixelated]"
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{project.name}</span>
|
||||
<div className="flex shrink-0 -space-x-1.5" aria-label="项目 Agent">
|
||||
{projectConfig?.agents.map((agent) => (
|
||||
<span
|
||||
key={agent.id}
|
||||
title={agent.name.trim() || agent.roleName}
|
||||
className="h-5 w-5 overflow-hidden rounded-full border border-background bg-white"
|
||||
>
|
||||
<img
|
||||
src={getAgentAvatarSrc(agent.avatarId)}
|
||||
alt=""
|
||||
className="h-full w-full object-cover [image-rendering:pixelated]"
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`进入项目 ${project.name}`}
|
||||
onClick={() => void openProject(project)}
|
||||
className="motion-press flex min-w-0 flex-1 items-center justify-center rounded-md bg-transparent px-1.5 py-1 text-left text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground"
|
||||
>
|
||||
进入项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight aria-hidden="true" className="h-3.5 w-3.5 shrink-0 -translate-x-0.5 text-muted-foreground opacity-50 transition-[opacity,transform,color] duration-200 ease-out group-hover/project:translate-x-0 group-hover/project:text-foreground group-hover/project:opacity-100 group-focus-visible/project:translate-x-0 group-focus-visible/project:text-foreground group-focus-visible/project:opacity-100" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
</div>
|
||||
</DisclosureContent>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
@@ -804,30 +798,6 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
: '确认后会在所选位置下创建与项目名称相同的文件夹。'}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-semibold" htmlFor="new-project-model">项目默认模型</label>
|
||||
<select
|
||||
id="new-project-model"
|
||||
aria-label="项目默认模型"
|
||||
value={newProjectDefaultModel}
|
||||
onChange={(event) => setNewProjectDefaultModel(event.target.value)}
|
||||
disabled={creatingProject}
|
||||
className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20"
|
||||
>
|
||||
<option value="">使用运行时默认模型</option>
|
||||
{configuredModelOptions.map((option) => (
|
||||
<option key={option.modelRef} value={option.modelRef}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[11px] font-medium text-muted-foreground">
|
||||
{configuredModelOptions.length > 0
|
||||
? '模型由服务端配置提供,项目只保存选择,不保存 Key。'
|
||||
: '暂时没有可选模型,创建后会使用运行时默认模型。'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{createProjectError ? (
|
||||
<div className="rounded-md border border-foreground/15 bg-accent-soft px-3 py-2 text-xs font-semibold">
|
||||
{createProjectError}
|
||||
@@ -859,40 +829,47 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
</Dialog>
|
||||
) : null}
|
||||
|
||||
{!isPaintingModule && projectEntryError ? (
|
||||
<div className="glass-surface fixed inset-0 z-50 flex items-center justify-center bg-foreground/15 px-4 backdrop-blur-[3px]">
|
||||
<div
|
||||
role="alertdialog"
|
||||
data-testid="project-config-error"
|
||||
className="glass-surface w-full max-w-md rounded-2xl border border-border/80 bg-background p-5 text-foreground shadow-float"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">项目配置不可用</h2>
|
||||
<p className="mt-2 text-sm font-medium leading-6 text-muted-foreground">
|
||||
无法进入项目,请检查 .niancode/project.json。
|
||||
</p>
|
||||
<pre className="mt-3 max-h-32 overflow-auto rounded-md border border-foreground/15 bg-white p-2 text-xs font-medium">
|
||||
{projectEntryError.message}
|
||||
</pre>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border border-foreground/15 bg-accent-soft text-foreground"
|
||||
onClick={requestRemoveUnavailableProject}
|
||||
{!isPaintingModule && projectEntryError && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div
|
||||
data-testid="project-config-error-overlay"
|
||||
className="fixed inset-0 z-[1000] flex min-h-screen items-center justify-center bg-foreground/15 p-4 backdrop-blur-md sm:p-6"
|
||||
>
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
data-testid="project-config-error"
|
||||
className="glass-surface w-full max-w-md rounded-2xl border border-border/80 bg-background p-5 text-foreground shadow-float sm:p-6"
|
||||
>
|
||||
移除项目卡片
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="border border-foreground/15 bg-brand-soft text-foreground"
|
||||
onClick={() => setProjectEntryError(null)}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<h2 className="text-xl font-semibold">项目配置不可用</h2>
|
||||
<p className="mt-2 text-sm font-medium leading-6 text-muted-foreground">
|
||||
无法进入项目,请检查 .niancode/project.json。
|
||||
</p>
|
||||
<pre className="mt-3 max-h-32 overflow-auto rounded-md border border-foreground/15 bg-white p-2 text-xs font-medium">
|
||||
{projectEntryError.message}
|
||||
</pre>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border border-foreground/15 bg-accent-soft text-foreground"
|
||||
onClick={requestRemoveUnavailableProject}
|
||||
>
|
||||
移除项目卡片
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="border border-foreground/15 bg-brand-soft text-foreground"
|
||||
onClick={() => setProjectEntryError(null)}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
|
||||
{!isPaintingModule ? (
|
||||
<ConfirmDialog
|
||||
@@ -978,25 +955,26 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{accountName}</span>
|
||||
</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 transition-transform', accountMenuOpen && 'rotate-90')} />
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 transition-transform duration-200 ease-out', accountMenuOpen && 'rotate-90')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<SidebarUpdateButton collapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
|
||||
{accountMenuOpen ? (
|
||||
<div
|
||||
ref={accountMenuRef}
|
||||
role="menu"
|
||||
data-testid="sidebar-account-menu"
|
||||
className={cn(
|
||||
'glass-surface z-50 max-h-[calc(100vh-7rem)] max-w-[calc(100vw-1rem)] overflow-y-auto rounded-2xl border border-border/80 p-1 text-foreground shadow-float',
|
||||
sidebarCollapsed
|
||||
? 'fixed bottom-[72px] left-2 w-64'
|
||||
: 'absolute bottom-[calc(100%+0.5rem)] left-0 w-full',
|
||||
)}
|
||||
>
|
||||
<DisclosureContent
|
||||
ref={accountMenuRef}
|
||||
open={accountMenuOpen}
|
||||
role="menu"
|
||||
data-testid="sidebar-account-menu"
|
||||
className={cn(
|
||||
'glass-surface z-50 max-w-[calc(100vw-1rem)] rounded-2xl border border-border/80 p-1 text-foreground shadow-float',
|
||||
sidebarCollapsed
|
||||
? 'fixed bottom-[72px] left-2 w-64'
|
||||
: 'absolute bottom-[calc(100%+0.5rem)] left-0 w-full',
|
||||
)}
|
||||
innerClassName="max-h-[calc(100vh-7rem)] overflow-y-auto"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent-soft text-xs font-semibold text-foreground">
|
||||
@@ -1021,13 +999,14 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">剩余用量</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform', usageDrawerOpen && 'rotate-90')} />
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-out', usageDrawerOpen && 'rotate-90')} />
|
||||
</button>
|
||||
{usageDrawerOpen ? (
|
||||
<div
|
||||
<DisclosureContent
|
||||
open={usageDrawerOpen}
|
||||
id="sidebar-account-usage-drawer"
|
||||
data-testid="sidebar-account-usage-drawer"
|
||||
className="ml-5 grid gap-0.5 rounded-md bg-surface-subtle/70 px-1 py-0.5 text-xs font-medium text-muted-foreground"
|
||||
className="ml-5 rounded-md bg-surface-subtle/70 px-1 py-0.5 text-xs font-medium text-muted-foreground"
|
||||
innerClassName="grid gap-0.5"
|
||||
>
|
||||
<div className="flex min-h-6 items-center justify-between gap-3 px-1">
|
||||
<span>订阅等级</span>
|
||||
@@ -1070,8 +1049,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</DisclosureContent>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@@ -1104,8 +1082,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
|
||||
<span className="min-w-0 flex-1 truncate">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in new issue
Block a user