frontend: build three-pane workspace shell with collapsible panels and synced path state
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { PanelBottom, PanelLeft, PanelRight } from "lucide-react"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { Separator } from "~/components/ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
// 三个可折叠面板的显隐状态,由 WorkspaceShell 持有。
|
||||
export interface PanelVisibility {
|
||||
tree: boolean
|
||||
changes: boolean
|
||||
console: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部上下文栏:展示固定的 Server / Collection / Workspace 上下文,
|
||||
* 以及面板折叠开关与重新连接入口,不承载对象级操作。
|
||||
*/
|
||||
export function TopBar({
|
||||
session,
|
||||
panels,
|
||||
onTogglePanel,
|
||||
onReconnect,
|
||||
}: {
|
||||
session: WorkspaceSession
|
||||
panels: PanelVisibility
|
||||
onTogglePanel: (panel: keyof PanelVisibility) => void
|
||||
onReconnect: () => void
|
||||
}) {
|
||||
return (
|
||||
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b bg-background px-3">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
<span className="shrink-0 font-semibold">macTFS</span>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<span className="hidden shrink-0 text-xs text-muted-foreground lg:inline">
|
||||
服务器
|
||||
</span>
|
||||
<span className="hidden max-w-56 truncate font-mono text-xs lg:inline">
|
||||
{session.serverUri}
|
||||
</span>
|
||||
<Separator orientation="vertical" className="hidden h-4! lg:block" />
|
||||
<span className="shrink-0 text-xs text-muted-foreground">Collection</span>
|
||||
<span className="max-w-40 truncate text-xs font-medium">{session.collection}</span>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<span className="shrink-0 text-xs text-muted-foreground">Workspace</span>
|
||||
<span className="max-w-44 truncate font-mono text-xs">{session.workspace}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<PanelToggle
|
||||
label="源码目录面板"
|
||||
active={panels.tree}
|
||||
onClick={() => onTogglePanel("tree")}
|
||||
>
|
||||
<PanelLeft />
|
||||
</PanelToggle>
|
||||
<PanelToggle
|
||||
label="挂起更改面板"
|
||||
active={panels.changes}
|
||||
onClick={() => onTogglePanel("changes")}
|
||||
>
|
||||
<PanelRight />
|
||||
</PanelToggle>
|
||||
<PanelToggle
|
||||
label="操作日志面板"
|
||||
active={panels.console}
|
||||
onClick={() => onTogglePanel("console")}
|
||||
>
|
||||
<PanelBottom />
|
||||
</PanelToggle>
|
||||
<Separator orientation="vertical" className="mx-1 h-4!" />
|
||||
<Button size="sm" variant="outline" onClick={onReconnect}>
|
||||
重新连接
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 面板折叠开关按钮:图标 + tooltip,激活态用底色区分。
|
||||
*/
|
||||
function PanelToggle({
|
||||
label,
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-pressed={active}
|
||||
onClick={onClick}
|
||||
className={cn(!active && "text-muted-foreground")}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{active ? `收起${label}` : `展开${label}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +1,65 @@
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { useCallback, useState } from "react"
|
||||
|
||||
import { TopBar, type PanelVisibility } from "~/components/app/top-bar"
|
||||
import { FolderItemsPanel } from "~/components/explorer/folder-items-panel"
|
||||
import { SourceTreePanel } from "~/components/explorer/source-tree-panel"
|
||||
import { ChangesPanel } from "~/components/inspector/changes-panel"
|
||||
import { ConsolePanel } from "~/components/logs/console-panel"
|
||||
import { TooltipProvider } from "~/components/ui/tooltip"
|
||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||
|
||||
/**
|
||||
* 工作台占位外壳:展示已固定的 Collection / Workspace 上下文,三栏布局在 FE-004 实现。
|
||||
* 三栏工作台外壳:顶部上下文栏 + 左侧目录树 + 中间主工作区 + 右侧 Changes + 底部 Console,
|
||||
* 左、右、底面板支持收起;当前路径由上层 home.tsx 统一维护并下发。
|
||||
*/
|
||||
export function WorkspaceShell({
|
||||
session,
|
||||
selectedServerPath,
|
||||
onNavigate,
|
||||
onReconnect,
|
||||
}: {
|
||||
session: WorkspaceSession
|
||||
selectedServerPath: string
|
||||
onNavigate: (serverPath: string) => void
|
||||
onReconnect: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col bg-muted/40">
|
||||
<header className="flex h-14 shrink-0 items-center justify-between border-b bg-background px-4">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
<span className="font-semibold">macTFS</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="text-muted-foreground">服务器</span>
|
||||
<span className="truncate font-mono text-xs">{session.serverUri}</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={onReconnect}>
|
||||
重新连接
|
||||
</Button>
|
||||
</header>
|
||||
const [panels, setPanels] = useState<PanelVisibility>({
|
||||
tree: true,
|
||||
changes: true,
|
||||
console: true,
|
||||
})
|
||||
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="w-full max-w-md rounded-lg border bg-background p-6 shadow-sm">
|
||||
<p className="text-sm font-medium">工作台上下文已固定</p>
|
||||
<dl className="mt-4 space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dt className="text-muted-foreground">Collection</dt>
|
||||
<dd className="truncate font-medium">{session.collection}</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dt className="text-muted-foreground">当前 Workspace</dt>
|
||||
<dd className="truncate font-mono text-xs">{session.workspace}</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<dt className="text-muted-foreground">映射数</dt>
|
||||
<dd className="font-medium">{session.mappings.length}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
三栏工作台布局、目录树与文件列表将在后续任务实现。
|
||||
</p>
|
||||
/**
|
||||
* 切换指定面板的展开 / 收起状态。
|
||||
*/
|
||||
const togglePanel = useCallback((panel: keyof PanelVisibility) => {
|
||||
setPanels((prev) => ({ ...prev, [panel]: !prev[panel] }))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex h-svh flex-col overflow-hidden bg-background">
|
||||
<TopBar
|
||||
session={session}
|
||||
panels={panels}
|
||||
onTogglePanel={togglePanel}
|
||||
onReconnect={onReconnect}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{panels.tree && (
|
||||
<SourceTreePanel
|
||||
collection={session.collection}
|
||||
selectedServerPath={selectedServerPath}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
<FolderItemsPanel session={session} selectedServerPath={selectedServerPath} />
|
||||
{panels.changes && <ChangesPanel />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{panels.console && <ConsolePanel />}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { FolderOpen } from "lucide-react"
|
||||
|
||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||
|
||||
/**
|
||||
* 按最长前缀匹配查找服务端路径所属的 Mapping,未命中返回 null。
|
||||
*/
|
||||
function findMapping(session: WorkspaceSession, serverPath: string) {
|
||||
let matched: WorkspaceSession["mappings"][number] | null = null
|
||||
for (const mapping of session.mappings) {
|
||||
if (
|
||||
serverPath === mapping.serverPath ||
|
||||
serverPath.startsWith(`${mapping.serverPath}/`)
|
||||
) {
|
||||
if (!matched || mapping.serverPath.length > matched.serverPath.length) {
|
||||
matched = mapping
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间主工作区骨架:顶部展示当前路径与 Mapping 摘要,主体为文件列表占位,
|
||||
* 真实目录数据加载与表格在 FE-005 实现。
|
||||
*/
|
||||
export function FolderItemsPanel({
|
||||
session,
|
||||
selectedServerPath,
|
||||
}: {
|
||||
session: WorkspaceSession
|
||||
selectedServerPath: string
|
||||
}) {
|
||||
const mapping = findMapping(session, selectedServerPath)
|
||||
return (
|
||||
<section className="flex h-full min-w-0 flex-1 flex-col bg-background">
|
||||
<div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">
|
||||
<span className="shrink-0 text-xs text-muted-foreground">当前路径</span>
|
||||
<span className="min-w-0 truncate font-mono text-xs">{selectedServerPath}</span>
|
||||
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
|
||||
{mapping ? (
|
||||
<span className="font-mono">{mapping.localPath}</span>
|
||||
) : (
|
||||
"未映射到本地"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<FolderOpen className="size-8 text-muted-foreground/60" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
当前目录文件列表将在后续任务加载
|
||||
</p>
|
||||
<p className="font-mono text-xs text-muted-foreground/80">
|
||||
{selectedServerPath}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 shrink-0 items-center border-t px-3 text-xs text-muted-foreground">
|
||||
未选中对象
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FolderClosed } from "lucide-react"
|
||||
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import { SERVER_ROOT_PATH } from "~/lib/tfs/session"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
/**
|
||||
* 左侧 Source Tree 面板骨架:展示固定 Collection 的根节点并与中间列表共享路径状态,
|
||||
* 目录树懒加载与子节点展开在 FE-005 实现。
|
||||
*/
|
||||
export function SourceTreePanel({
|
||||
collection,
|
||||
selectedServerPath,
|
||||
onNavigate,
|
||||
}: {
|
||||
collection: string
|
||||
selectedServerPath: string
|
||||
onNavigate: (serverPath: string) => void
|
||||
}) {
|
||||
const rootActive = selectedServerPath === SERVER_ROOT_PATH
|
||||
return (
|
||||
<aside className="flex h-full w-[280px] shrink-0 flex-col border-r bg-sidebar">
|
||||
<div className="flex h-9 shrink-0 items-center justify-between border-b px-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">源码目录</span>
|
||||
<span className="max-w-32 truncate text-xs text-muted-foreground">{collection}</span>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate(SERVER_ROOT_PATH)}
|
||||
className={cn(
|
||||
"flex h-7 w-full items-center gap-1.5 rounded-md px-2 text-left text-sm",
|
||||
rootActive
|
||||
? "bg-primary/10 font-medium text-foreground"
|
||||
: "text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
<FolderClosed className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate font-mono text-xs">{SERVER_ROOT_PATH}</span>
|
||||
</button>
|
||||
<p className="px-2 py-3 text-xs leading-5 text-muted-foreground">
|
||||
服务端目录树将在后续任务加载,当前仅提供根节点导航。
|
||||
</p>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ListChecks } from "lucide-react"
|
||||
|
||||
/**
|
||||
* 右侧 Changes 面板骨架:Pending Changes 列表与 Checkin 入口在 FE-010 实现。
|
||||
*/
|
||||
export function ChangesPanel() {
|
||||
return (
|
||||
<aside className="flex h-full w-[340px] shrink-0 flex-col border-l bg-sidebar">
|
||||
<div className="flex h-9 shrink-0 items-center border-b px-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">挂起更改</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<ListChecks className="size-6 text-muted-foreground/60" />
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
挂起更改与签入将在后续任务实现
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 底部 Operation Console 骨架:操作日志列表与执行反馈在 FE-012 实现。
|
||||
*/
|
||||
export function ConsolePanel() {
|
||||
return (
|
||||
<footer className="flex h-[180px] shrink-0 flex-col border-t bg-background">
|
||||
<div className="flex h-8 shrink-0 items-center border-b px-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">操作日志</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<p className="text-xs text-muted-foreground">操作日志将在后续任务展示</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="relative flex-1 rounded-full bg-border"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { MappingInfo } from "~/lib/api"
|
||||
|
||||
// 服务端目录树根路径,工作台导航的默认起点。
|
||||
export const SERVER_ROOT_PATH = "$/"
|
||||
|
||||
// 工作台进入后固定的共享上下文,serverUri / collection / workspace 不再在台内切换。
|
||||
export interface WorkspaceSession {
|
||||
serverUri: string
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WorkspaceShell } from "~/components/app/workspace-shell"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { getServiceStatus, isElectron, startService } from "~/lib/electron"
|
||||
import type { ServiceStatus } from "~/lib/electron"
|
||||
import { SERVER_ROOT_PATH } from "~/lib/tfs/session"
|
||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||
|
||||
// 顶层视图状态:检测服务、服务未就绪、连接入口、已进入工作台。
|
||||
@@ -12,12 +13,13 @@ type AppPhase = "checking" | "blocked" | "connect" | "workspace"
|
||||
|
||||
/**
|
||||
* 第一版前端入口:先确保本地服务就绪,再走连接 / Collection / Workspace 流程,
|
||||
* 进入工作台后持有固定上下文(FE-003)。
|
||||
* 进入工作台后持有固定上下文,并统一维护左树与中间列表共享的当前路径(FE-004)。
|
||||
*/
|
||||
export default function Home() {
|
||||
const [phase, setPhase] = useState<AppPhase>("checking")
|
||||
const [status, setStatus] = useState<ServiceStatus | null>(null)
|
||||
const [session, setSession] = useState<WorkspaceSession | null>(null)
|
||||
const [selectedServerPath, setSelectedServerPath] = useState(SERVER_ROOT_PATH)
|
||||
|
||||
/**
|
||||
* 检测本地服务,未就绪时在 Electron 环境下尝试自动拉起。
|
||||
@@ -37,10 +39,11 @@ export default function Home() {
|
||||
}, [bootstrap])
|
||||
|
||||
/**
|
||||
* 连接流程完成后固定上下文并进入工作台。
|
||||
* 连接流程完成后固定上下文并进入工作台,当前路径重置到根目录。
|
||||
*/
|
||||
const handleReady = useCallback((next: WorkspaceSession) => {
|
||||
setSession(next)
|
||||
setSelectedServerPath(SERVER_ROOT_PATH)
|
||||
setPhase("workspace")
|
||||
}, [])
|
||||
|
||||
@@ -52,8 +55,22 @@ export default function Home() {
|
||||
setPhase("connect")
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 同步导航:左侧树与中间列表共用的当前服务端路径变更入口。
|
||||
*/
|
||||
const handleNavigate = useCallback((serverPath: string) => {
|
||||
setSelectedServerPath(serverPath)
|
||||
}, [])
|
||||
|
||||
if (phase === "workspace" && session) {
|
||||
return <WorkspaceShell session={session} onReconnect={handleReconnect} />
|
||||
return (
|
||||
<WorkspaceShell
|
||||
session={session}
|
||||
selectedServerPath={selectedServerPath}
|
||||
onNavigate={handleNavigate}
|
||||
onReconnect={handleReconnect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (phase === "connect") {
|
||||
|
||||
Reference in New Issue
Block a user