extract sidebar
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import {
|
||||
Fragment,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
Gamepad2,
|
||||
Image,
|
||||
MessageSquare,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type FormEvent,
|
||||
Fragment,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type ReactNode,
|
||||
@@ -11,33 +19,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Bell,
|
||||
BookOpen,
|
||||
CircleHelp,
|
||||
Contact,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
Gamepad2,
|
||||
Home,
|
||||
Image,
|
||||
LogOut,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
User,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import HomeView, {
|
||||
type HomeDraft,
|
||||
type HomeAgentMode,
|
||||
type HomeAgentModeItem,
|
||||
type HomeAttachmentDraft,
|
||||
} from './view/home';
|
||||
import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore';
|
||||
import {
|
||||
getClientProfileDashboard,
|
||||
getClientProfileWalletLedger,
|
||||
} from './services/clientApi';
|
||||
|
||||
import type {
|
||||
AuthEntryResponse,
|
||||
@@ -48,15 +29,6 @@ import type {
|
||||
AuthUser,
|
||||
LogoutResponse,
|
||||
} from '../../../packages/shared/src/contracts/auth';
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
API_RESPONSE_ENVELOPE_VERSION,
|
||||
unwrapApiResponse,
|
||||
} from '../../../packages/shared/src/http';
|
||||
import type {
|
||||
ProfileDashboardSummary,
|
||||
ProfileWalletLedgerEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
createGameCreationAppManifest,
|
||||
createGameCreationAppSeedTasks,
|
||||
@@ -67,21 +39,42 @@ import {
|
||||
GAME_CREATION_APP_COMMANDS,
|
||||
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||||
type GameCreationAgentCapabilityDescriptor,
|
||||
type GameCreationAppAgentGroup,
|
||||
type GameCreationAppCommandDescriptor,
|
||||
type GameCreationAppLimitedRunCommandDescriptor,
|
||||
type GameCreationAgentRepairRouteTrace,
|
||||
type GameCreationAgentRunStep,
|
||||
type GameCreationAgentToolCallTrace,
|
||||
type GameCreationAgentRunTrace,
|
||||
type GameCreationAgentToolCallTrace,
|
||||
type GameCreationAppAgentGroup,
|
||||
type GameCreationAppAssetSourceKind,
|
||||
type GameCreationAppCommandDescriptor,
|
||||
type GameCreationAppLimitedRunCommandDescriptor,
|
||||
type GameCreationAppManifest,
|
||||
type GameCreationAppPermission,
|
||||
type GameCreationAppPreviewStatus,
|
||||
type GameCreationAppTaskStatus,
|
||||
type GameCreationAppTaskState,
|
||||
type GameCreationAppTaskStatus,
|
||||
selectGameCreationAppReadyTasks,
|
||||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type {
|
||||
ProfileDashboardSummary,
|
||||
ProfileWalletLedgerEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
API_RESPONSE_ENVELOPE_VERSION,
|
||||
unwrapApiResponse,
|
||||
} from '../../../packages/shared/src/http';
|
||||
import {
|
||||
getClientProfileDashboard,
|
||||
getClientProfileWalletLedger,
|
||||
} from './services/clientApi';
|
||||
import HomeView, {
|
||||
type HomeAgentMode,
|
||||
type HomeAgentModeItem,
|
||||
type HomeAttachmentDraft,
|
||||
type HomeDraft,
|
||||
} from './view/home';
|
||||
import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore';
|
||||
import { Sidebar, type LauncherView } from './view/layout';
|
||||
|
||||
const seedManifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
@@ -103,14 +96,6 @@ const launcherNotifications: Array<{
|
||||
detail: string;
|
||||
actionLabel: string;
|
||||
}> = [];
|
||||
type LauncherView =
|
||||
| 'home'
|
||||
| 'projects'
|
||||
| 'agent-chat'
|
||||
| 'guide'
|
||||
| 'contact'
|
||||
| 'news'
|
||||
| 'project-development';
|
||||
type LauncherImportedAttachment = {
|
||||
fileName: string;
|
||||
mediaType: string;
|
||||
@@ -2129,14 +2114,10 @@ export function WorkspaceLauncher({
|
||||
);
|
||||
const [walletLedgerStatus, setWalletLedgerStatus] =
|
||||
useState('尚未读取明细');
|
||||
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
||||
const [helpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const [launcherNotice, setLauncherNotice] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const helpMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const resetLauncherHomeDraft = useLauncherHomeDraftStore(
|
||||
(state) => state.reset,
|
||||
);
|
||||
@@ -2228,44 +2209,6 @@ export function WorkspaceLauncher({
|
||||
void loadAgentChatLlmStatus();
|
||||
}, [launcherView, agentChatSelectedAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
function closeSidebarMenusIfOutside(target: EventTarget | null) {
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
accountMenuRef.current?.contains(target) ||
|
||||
helpMenuRef.current?.contains(target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleFocusIn(event: FocusEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleWindowBlur() {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
window.addEventListener('blur', handleWindowBlur);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
document.removeEventListener('focusin', handleFocusIn);
|
||||
window.removeEventListener('blur', handleWindowBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function validateProjectPath(nextProjectPath: string) {
|
||||
const trimmedProjectPath = nextProjectPath.trim();
|
||||
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
||||
@@ -2620,8 +2563,6 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
|
||||
function showLauncherNotice(title: string) {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
setWalletPanelOpen(false);
|
||||
setLauncherNotice({
|
||||
title,
|
||||
@@ -3096,144 +3037,17 @@ export function WorkspaceLauncher({
|
||||
className="launcher-shell platform-theme platform-theme--light"
|
||||
aria-label="GameAgent 客户端"
|
||||
>
|
||||
<aside className="launcher-sidebar">
|
||||
<div className="launcher-brand" aria-label="陶泥儿">
|
||||
<span className="launcher-logo">tn</span>
|
||||
</div>
|
||||
<nav aria-label="客户端导航">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="首页"
|
||||
title="首页"
|
||||
className={launcherView === 'home' ? 'launcher-nav-active' : ''}
|
||||
onClick={() => setLauncherView('home')}
|
||||
>
|
||||
<Home size={17} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="项目组"
|
||||
title="项目组"
|
||||
className={launcherView === 'projects' ? 'launcher-nav-active' : ''}
|
||||
onClick={() => setLauncherView('projects')}
|
||||
>
|
||||
<FolderKanban size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
<div className="launcher-sidebar-bottom">
|
||||
<div className="launcher-account-nav" ref={accountMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-config-button"
|
||||
onClick={() => {
|
||||
setHelpMenuOpen(false);
|
||||
setWalletPanelOpen(false);
|
||||
setAccountMenuOpen((open) => !open);
|
||||
}}
|
||||
aria-expanded={accountMenuOpen}
|
||||
aria-label="我的"
|
||||
title="我的"
|
||||
>
|
||||
<User size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{accountMenuOpen ? (
|
||||
<div className="launcher-account-popover" role="menu">
|
||||
<strong>{currentUser.displayName || '陶泥儿用户'}</strong>
|
||||
<span>
|
||||
{currentUser.phoneNumberMasked ||
|
||||
currentUser.publicUserCode ||
|
||||
'账号信息读取中'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('会员与升级')}
|
||||
>
|
||||
会员与升级
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('使用指南')}
|
||||
>
|
||||
使用指南
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('反馈联系')}
|
||||
>
|
||||
反馈联系
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
resetLauncherHomeDraft();
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
<LogOut size={14} aria-hidden="true" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="launcher-help-nav" ref={helpMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="帮助"
|
||||
title="帮助"
|
||||
className="launcher-config-button"
|
||||
aria-expanded={helpMenuOpen}
|
||||
onClick={() => {
|
||||
setAccountMenuOpen(false);
|
||||
setWalletPanelOpen(false);
|
||||
setHelpMenuOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
<CircleHelp size={17} aria-hidden="true" />
|
||||
</button>
|
||||
{helpMenuOpen ? (
|
||||
<div className="launcher-help-menu" role="menu">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('使用指南')}
|
||||
>
|
||||
<BookOpen size={14} aria-hidden="true" />
|
||||
使用指南
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('联系我们')}
|
||||
>
|
||||
<Contact size={14} aria-hidden="true" />
|
||||
联系我们
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => showLauncherNotice('最新动态')}
|
||||
>
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
最新动态
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-config-button"
|
||||
onClick={() => setRuntimeConfigOpen(true)}
|
||||
aria-label="配置"
|
||||
title="配置"
|
||||
>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<Sidebar
|
||||
activeView={launcherView}
|
||||
currentUser={currentUser}
|
||||
onLogout={() => {
|
||||
resetLauncherHomeDraft();
|
||||
onLogout();
|
||||
}}
|
||||
onNoticeRequest={showLauncherNotice}
|
||||
onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)}
|
||||
onViewChange={setLauncherView}
|
||||
/>
|
||||
<section
|
||||
className={
|
||||
launcherNotifications.length > 0
|
||||
@@ -3260,8 +3074,6 @@ export function WorkspaceLauncher({
|
||||
type="button"
|
||||
aria-label="泥点余额"
|
||||
onClick={() => {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
setWalletPanelOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -157,38 +157,6 @@ textarea {
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-sidebar {
|
||||
border-color: var(--platform-surface-border);
|
||||
background: var(--platform-nav-fill);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-logo {
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
box-shadow: var(--platform-profile-action-shadow);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-sidebar nav button,
|
||||
.launcher-shell.platform-theme .launcher-config-button {
|
||||
color: var(--platform-nav-item-text);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-sidebar .launcher-nav-active {
|
||||
border: 1px solid var(--platform-nav-active-border);
|
||||
background: var(--platform-nav-active-fill);
|
||||
color: var(--platform-nav-item-text-active);
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-sidebar nav button:hover,
|
||||
.launcher-shell.platform-theme .launcher-config-button:hover,
|
||||
.launcher-shell.platform-theme .launcher-sidebar .launcher-help-menu button:hover,
|
||||
.launcher-shell.platform-theme .launcher-sidebar .launcher-help-menu button:focus-visible,
|
||||
.launcher-shell.platform-theme .launcher-sidebar .launcher-account-popover button:hover,
|
||||
.launcher-shell.platform-theme .launcher-sidebar .launcher-account-popover button:focus-visible {
|
||||
background: var(--platform-nav-item-hover-fill);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-main {
|
||||
background: var(--platform-body-fill);
|
||||
}
|
||||
@@ -205,16 +173,13 @@ textarea {
|
||||
.launcher-shell.platform-theme .launcher-countdown,
|
||||
.launcher-shell.platform-theme .launcher-account-bar button,
|
||||
.launcher-shell.platform-theme .launcher-wallet-group,
|
||||
.launcher-shell.platform-theme .launcher-account-popover,
|
||||
.launcher-shell.platform-theme .launcher-wallet-popover,
|
||||
.launcher-shell.platform-theme .launcher-help-menu {
|
||||
.launcher-shell.platform-theme .launcher-wallet-popover {
|
||||
border-color: var(--platform-subpanel-border);
|
||||
background: var(--platform-subpanel-fill);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-account-bar button,
|
||||
.launcher-shell.platform-theme .launcher-account-popover button,
|
||||
.launcher-shell.platform-theme .launcher-wallet-popover button,
|
||||
.launcher-shell.platform-theme .launcher-wallet-ledger div span {
|
||||
color: var(--platform-text-strong);
|
||||
@@ -224,146 +189,17 @@ textarea {
|
||||
border-left-color: var(--platform-subpanel-border);
|
||||
}
|
||||
|
||||
.launcher-shell.platform-theme .launcher-account-popover span,
|
||||
.launcher-shell.platform-theme .launcher-wallet-popover span,
|
||||
.launcher-shell.platform-theme .launcher-account-popover small,
|
||||
.launcher-shell.platform-theme .launcher-wallet-popover small {
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
|
||||
.launcher-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
height: 100vh;
|
||||
padding: 16px 0 18px;
|
||||
border-right: 1px solid #eceef2;
|
||||
background: #fff;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.launcher-brand {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.launcher-logo {
|
||||
display: grid;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #101010;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.launcher-sidebar nav {
|
||||
display: grid;
|
||||
gap: 13px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.launcher-sidebar-bottom {
|
||||
position: relative;
|
||||
z-index: 60;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 13px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.launcher-sidebar button,
|
||||
.launcher-promo button,
|
||||
.launcher-account-bar button {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.launcher-sidebar nav button,
|
||||
.launcher-config-button {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #161616;
|
||||
font-size: 17px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.launcher-sidebar .launcher-nav-active {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.launcher-account-nav,
|
||||
.launcher-help-nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.launcher-help-menu {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
bottom: -4px;
|
||||
left: 36px;
|
||||
display: grid;
|
||||
min-width: 150px;
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgb(15 23 42 / 14%);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.launcher-sidebar .launcher-help-menu button,
|
||||
.launcher-sidebar .launcher-account-popover button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 9px;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: #1f2937;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.launcher-sidebar .launcher-help-menu button:hover,
|
||||
.launcher-sidebar .launcher-help-menu button:focus-visible,
|
||||
.launcher-sidebar .launcher-account-popover button:hover,
|
||||
.launcher-sidebar .launcher-account-popover button:focus-visible {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.launcher-config-button {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.launcher-config-button:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.launcher-help-nav .launcher-config-button,
|
||||
.launcher-help-nav .launcher-config-button:hover,
|
||||
.launcher-help-nav .launcher-config-button:focus,
|
||||
.launcher-help-nav .launcher-config-button:focus-visible,
|
||||
.launcher-help-nav .launcher-config-button:active,
|
||||
.launcher-help-nav .launcher-config-button[aria-expanded='true'] {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.launcher-main {
|
||||
position: relative;
|
||||
display: grid;
|
||||
@@ -469,7 +305,6 @@ textarea {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.launcher-account-popover,
|
||||
.launcher-wallet-popover {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
@@ -486,30 +321,16 @@ textarea {
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.launcher-account-nav .launcher-account-popover {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: -4px;
|
||||
left: 36px;
|
||||
width: 218px;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.launcher-account-popover strong,
|
||||
.launcher-wallet-popover strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.launcher-account-popover span,
|
||||
.launcher-wallet-popover span,
|
||||
.launcher-account-popover small,
|
||||
.launcher-wallet-popover small {
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.launcher-account-popover button,
|
||||
.launcher-wallet-popover button {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
Bell,
|
||||
BookOpen,
|
||||
CircleHelp,
|
||||
Contact,
|
||||
FolderKanban,
|
||||
Home,
|
||||
LogOut,
|
||||
Settings,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
function cx(...classNames: Array<string | false | null | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const sidebarIconButtonClass =
|
||||
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text) hover:bg-(--platform-nav-item-hover-fill)';
|
||||
const sidebarHelpButtonClass =
|
||||
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text)';
|
||||
const sidebarMenuItemClass =
|
||||
'flex h-[30px] w-full cursor-pointer items-center justify-start gap-2 whitespace-nowrap rounded-[7px] border-0 bg-transparent px-[9px] py-0 text-left text-xs text-(--platform-text-strong) hover:bg-(--platform-nav-item-hover-fill) focus-visible:bg-(--platform-nav-item-hover-fill)';
|
||||
|
||||
export type LauncherView =
|
||||
| 'home'
|
||||
| 'projects'
|
||||
| 'agent-chat'
|
||||
| 'guide'
|
||||
| 'contact'
|
||||
| 'news'
|
||||
| 'project-development';
|
||||
|
||||
type SidebarUserInfo = {
|
||||
displayName?: string | null;
|
||||
phoneNumberMasked?: string | null;
|
||||
publicUserCode?: string | null;
|
||||
};
|
||||
|
||||
type LauncherSidebarProps = {
|
||||
activeView: LauncherView;
|
||||
currentUser: SidebarUserInfo;
|
||||
onViewChange: (view: LauncherView) => void;
|
||||
onNoticeRequest: (title: string) => void;
|
||||
onRuntimeConfigOpen: () => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export function Sidebar({
|
||||
activeView,
|
||||
currentUser,
|
||||
onViewChange,
|
||||
onNoticeRequest,
|
||||
onRuntimeConfigOpen,
|
||||
onLogout,
|
||||
}: LauncherSidebarProps) {
|
||||
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
||||
const [helpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const helpMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function closeSidebarMenusIfOutside(target: EventTarget | null) {
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
accountMenuRef.current?.contains(target) ||
|
||||
helpMenuRef.current?.contains(target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleFocusIn(event: FocusEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleWindowBlur() {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
window.addEventListener('blur', handleWindowBlur);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
document.removeEventListener('focusin', handleFocusIn);
|
||||
window.removeEventListener('blur', handleWindowBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function closeMenus() {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen(false);
|
||||
}
|
||||
|
||||
function showNotice(title: string) {
|
||||
closeMenus();
|
||||
onNoticeRequest(title);
|
||||
}
|
||||
|
||||
function changeView(view: LauncherView) {
|
||||
closeMenus();
|
||||
onViewChange(view);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-16 sticky top-0 z-40 flex h-screen flex-col items-center gap-8 overflow-visible border-r border-(--platform-surface-border) bg-(--platform-nav-fill) px-0 pb-4.5 pt-4">
|
||||
<div className="grid place-items-center" aria-label="陶泥儿">
|
||||
<span className="grid h-6 w-6 place-items-center rounded-full bg-(--platform-button-primary-fill) text-[10px] font-bold tracking-normal text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow)">
|
||||
{/*// TODO icon*/}
|
||||
</span>
|
||||
</div>
|
||||
<nav className="grid justify-items-center gap-3.25" aria-label="客户端导航">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="首页"
|
||||
title="首页"
|
||||
className={cx(
|
||||
sidebarIconButtonClass,
|
||||
activeView === 'home' &&
|
||||
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
||||
)}
|
||||
onClick={() => changeView('home')}
|
||||
>
|
||||
<Home size={17} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="项目组"
|
||||
title="项目组"
|
||||
className={cx(
|
||||
sidebarIconButtonClass,
|
||||
activeView === 'projects' &&
|
||||
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
||||
)}
|
||||
onClick={() => changeView('projects')}
|
||||
>
|
||||
<FolderKanban size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
<div className="relative z-[60] mt-auto grid justify-items-center gap-[13px]">
|
||||
<div className="relative" ref={accountMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={sidebarIconButtonClass}
|
||||
onClick={() => {
|
||||
setHelpMenuOpen(false);
|
||||
setAccountMenuOpen((open) => !open);
|
||||
}}
|
||||
aria-expanded={accountMenuOpen}
|
||||
aria-label="我的"
|
||||
title="我的"
|
||||
>
|
||||
<User size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{accountMenuOpen ? (
|
||||
<div
|
||||
className="absolute bottom-[-4px] left-14 z-[90] grid w-[218px] gap-1.5 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-2.5 text-(--platform-text-strong) shadow-[0_16px_38px_rgb(15_23_42_/_14%)]"
|
||||
role="menu"
|
||||
>
|
||||
<strong className="text-[13px]">
|
||||
{currentUser.displayName || '陶泥儿用户'}
|
||||
</strong>
|
||||
<span className="text-[11px] text-(--platform-text-soft)">
|
||||
{currentUser.phoneNumberMasked ||
|
||||
currentUser.publicUserCode ||
|
||||
'账号信息读取中'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('会员与升级')}
|
||||
>
|
||||
会员与升级
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('使用指南')}
|
||||
>
|
||||
使用指南
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('反馈联系')}
|
||||
>
|
||||
反馈联系
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => {
|
||||
closeMenus();
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
<LogOut size={14} aria-hidden="true" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative" ref={helpMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="帮助"
|
||||
title="帮助"
|
||||
className={sidebarHelpButtonClass}
|
||||
aria-expanded={helpMenuOpen}
|
||||
onClick={() => {
|
||||
setAccountMenuOpen(false);
|
||||
setHelpMenuOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
<CircleHelp size={17} aria-hidden="true" />
|
||||
</button>
|
||||
{helpMenuOpen ? (
|
||||
<div
|
||||
className="absolute bottom-[-4px] left-14 z-[90] grid min-w-[150px] gap-[5px] rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-2 text-(--platform-text-strong) shadow-[0_14px_34px_rgb(15_23_42_/_14%)]"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('使用指南')}
|
||||
>
|
||||
<BookOpen size={14} aria-hidden="true" />
|
||||
使用指南
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('联系我们')}
|
||||
>
|
||||
<Contact size={14} aria-hidden="true" />
|
||||
联系我们
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => showNotice('最新动态')}
|
||||
>
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
最新动态
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={sidebarIconButtonClass}
|
||||
onClick={() => {
|
||||
closeMenus();
|
||||
onRuntimeConfigOpen();
|
||||
}}
|
||||
aria-label="配置"
|
||||
title="配置"
|
||||
>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user