feat: redesign floating workspace interactions
This commit is contained in:
@@ -1,26 +1,20 @@
|
||||
import {
|
||||
Bot,
|
||||
CalendarClock,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Columns2,
|
||||
FlaskConical,
|
||||
Grid2X2,
|
||||
Layers3,
|
||||
LayoutDashboard,
|
||||
LayoutTemplate,
|
||||
Map as MapIcon,
|
||||
PanelLeft,
|
||||
PanelsTopLeft,
|
||||
SlidersHorizontal,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode
|
||||
} from "react";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
@@ -58,28 +52,14 @@ import {
|
||||
} from "./workspace-model";
|
||||
import { WorkspaceWindow } from "./workspace-window";
|
||||
|
||||
export type WorkbenchNavigationSection = "agent" | "conditions" | "layers" | "tools";
|
||||
|
||||
type WorkbenchMainFrameProps = {
|
||||
renderAgentPanel: (onCollapse: () => void) => ReactNode;
|
||||
conditionsPanel: ReactNode;
|
||||
layersPanel: ReactNode;
|
||||
toolsPanel: ReactNode;
|
||||
mapContent: ReactNode;
|
||||
mapOverlay?: ReactNode;
|
||||
testEnabled: boolean;
|
||||
activeSection: WorkbenchNavigationSection;
|
||||
navigationCollapsed: boolean;
|
||||
onActiveSectionChange: (section: WorkbenchNavigationSection) => void;
|
||||
onNavigationCollapsedChange: (collapsed: boolean) => void;
|
||||
};
|
||||
|
||||
const NAVIGATION_ITEMS: Array<{ id: WorkbenchNavigationSection; label: string; icon: typeof Bot }> = [
|
||||
{ id: "agent", label: "Agent", icon: Bot },
|
||||
{ id: "conditions", label: "工况任务", icon: CalendarClock },
|
||||
{ id: "layers", label: "图层", icon: Layers3 },
|
||||
{ id: "tools", label: "地图工具", icon: SlidersHorizontal }
|
||||
];
|
||||
export type WorkbenchMainFrameHandle = {
|
||||
openTestAnalysis: () => void;
|
||||
};
|
||||
|
||||
const INITIAL_BOUNDS: WorkspaceBounds = { width: 960, height: 680 };
|
||||
|
||||
@@ -89,19 +69,10 @@ type WorkspaceDragState = {
|
||||
targetId: string | null;
|
||||
};
|
||||
|
||||
export function WorkbenchMainFrame({
|
||||
renderAgentPanel,
|
||||
conditionsPanel,
|
||||
layersPanel,
|
||||
toolsPanel,
|
||||
export const WorkbenchMainFrame = forwardRef<WorkbenchMainFrameHandle, WorkbenchMainFrameProps>(function WorkbenchMainFrame({
|
||||
mapContent,
|
||||
mapOverlay,
|
||||
testEnabled,
|
||||
activeSection,
|
||||
navigationCollapsed,
|
||||
onActiveSectionChange,
|
||||
onNavigationCollapsedChange
|
||||
}: WorkbenchMainFrameProps) {
|
||||
mapOverlay
|
||||
}, ref) {
|
||||
const workspaceRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragStateRef = useRef<WorkspaceDragState | null>(null);
|
||||
const [bounds, setBounds] = useState<WorkspaceBounds>(INITIAL_BOUNDS);
|
||||
@@ -154,6 +125,8 @@ export function WorkbenchMainFrame({
|
||||
setWorkspace((current) => openAnalysisDocument(current, document, bounds));
|
||||
}, [bounds, workspace.analyses.length]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ openTestAnalysis: openTestDocument }), [openTestDocument]);
|
||||
|
||||
const findSwapTarget = useCallback((sourceId: string, pointer: { x: number; y: number }) => {
|
||||
const root = workspaceRef.current;
|
||||
if (!root) return null;
|
||||
@@ -215,110 +188,16 @@ export function WorkbenchMainFrame({
|
||||
setWorkspaceAnnouncement("已取消窗口移动");
|
||||
}, []);
|
||||
|
||||
const navigationWidth = navigationCollapsed ? 48 : 368;
|
||||
return (
|
||||
<div
|
||||
className="workbench-main-frame pointer-events-none absolute inset-0 lg:top-14"
|
||||
data-testid="workbench-main-frame"
|
||||
style={{ "--workbench-frame-navigation-width": `${navigationWidth}px` } as CSSProperties}
|
||||
>
|
||||
<aside
|
||||
aria-label="工作台导航"
|
||||
className="pointer-events-auto absolute bottom-0 left-0 top-0 z-30 hidden overflow-hidden border-r border-slate-300 bg-[#edf1f5] shadow-[1px_0_3px_rgba(15,23,42,0.12)] lg:flex"
|
||||
style={{ width: navigationWidth }}
|
||||
>
|
||||
<nav className="flex w-12 shrink-0 flex-col items-center border-r border-slate-300 bg-[#e2e8ef] py-2" aria-label="主工作区">
|
||||
<span className="mb-2 grid h-9 w-9 place-items-center rounded-md bg-slate-900 text-white"><PanelLeft size={17} aria-hidden="true" /></span>
|
||||
{NAVIGATION_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = item.id === activeSection;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={item.label}
|
||||
aria-pressed={active}
|
||||
title={item.label}
|
||||
className={cn(
|
||||
"relative grid h-11 w-11 place-items-center rounded-md text-slate-600 transition-[background-color,color,scale] active:scale-[0.96] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500",
|
||||
active ? "bg-white text-blue-700 shadow-[0_1px_3px_rgba(15,23,42,0.14)]" : "hover:bg-white/70 hover:text-slate-900"
|
||||
)}
|
||||
onClick={() => { onActiveSectionChange(item.id); onNavigationCollapsedChange(false); }}
|
||||
>
|
||||
<Icon size={19} aria-hidden="true" />
|
||||
{active ? <span className="absolute bottom-1.5 h-1 w-1 rounded-full bg-blue-600" aria-hidden="true" /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="mt-auto flex flex-col items-center gap-1">
|
||||
{testEnabled ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="测试打开 Agent 多图表窗口"
|
||||
title="测试打开 Agent 多图表窗口"
|
||||
disabled={workspace.analyses.length >= MAX_ANALYSIS_WINDOWS}
|
||||
className="grid h-11 w-11 place-items-center rounded-md text-blue-700 transition-[background-color,color,scale] hover:bg-blue-50 active:scale-[0.96] disabled:text-slate-400 disabled:opacity-50 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
onClick={openTestDocument}
|
||||
>
|
||||
<FlaskConical size={19} aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={navigationCollapsed ? "展开工作台导航" : "折叠工作台导航"}
|
||||
title={navigationCollapsed ? "展开工作台导航" : "折叠工作台导航"}
|
||||
className="grid h-11 w-11 place-items-center rounded-md text-slate-600 transition-[background-color,color,scale] hover:bg-white active:scale-[0.96] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
onClick={() => onNavigationCollapsedChange(!navigationCollapsed)}
|
||||
>
|
||||
{navigationCollapsed ? <ChevronsRight size={18} aria-hidden="true" /> : <ChevronsLeft size={18} aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div
|
||||
aria-hidden={navigationCollapsed}
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col bg-[#f7f9fb]",
|
||||
navigationCollapsed && "invisible"
|
||||
)}
|
||||
>
|
||||
<header className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 px-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-600" aria-hidden="true" />
|
||||
<h2 className="truncate text-sm font-semibold text-slate-900">{NAVIGATION_ITEMS.find((item) => item.id === activeSection)?.label}</h2>
|
||||
</div>
|
||||
{testEnabled ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTestDocument}
|
||||
disabled={workspace.analyses.length >= MAX_ANALYSIS_WINDOWS}
|
||||
className="flex h-9 items-center gap-1.5 rounded-md bg-blue-600 px-3 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(37,99,235,0.3)] transition-[background-color,scale] hover:bg-blue-700 active:scale-[0.96] disabled:bg-slate-300 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Sparkles size={14} aria-hidden="true" />
|
||||
测试分析窗
|
||||
</button>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden [--workbench-condition-width:320px]">
|
||||
{desktop ? (
|
||||
<>
|
||||
<NavigationPanel active={activeSection === "agent"}>
|
||||
{renderAgentPanel(() => onNavigationCollapsedChange(true))}
|
||||
</NavigationPanel>
|
||||
<NavigationPanel active={activeSection === "conditions"}>{conditionsPanel}</NavigationPanel>
|
||||
<NavigationPanel active={activeSection === "layers"}>{layersPanel}</NavigationPanel>
|
||||
<NavigationPanel active={activeSection === "tools"}>{toolsPanel}</NavigationPanel>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
ref={workspaceRef}
|
||||
id="main-workspace"
|
||||
data-layout-mode={workspace.layout.mode}
|
||||
className="pointer-events-auto absolute inset-0 left-0 overflow-hidden bg-[#dce3ea] lg:bottom-10 lg:left-[var(--workbench-frame-navigation-width)]"
|
||||
className="pointer-events-auto absolute inset-0 overflow-hidden bg-[#dce3ea] lg:bottom-10"
|
||||
>
|
||||
<WorkspaceWindow
|
||||
id={workspace.map.id}
|
||||
@@ -371,7 +250,7 @@ export function WorkbenchMainFrame({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 right-0 z-40 hidden h-10 items-center gap-1 border-t border-slate-300 bg-[#e7ecf1] px-2 lg:left-[var(--workbench-frame-navigation-width)] lg:flex"
|
||||
className="pointer-events-auto absolute bottom-0 left-0 right-0 z-40 hidden h-10 items-center gap-1 border-t border-slate-300 bg-[#e7ecf1] px-2 lg:flex"
|
||||
role="toolbar"
|
||||
aria-label="工作区窗口任务栏"
|
||||
>
|
||||
@@ -410,7 +289,7 @@ export function WorkbenchMainFrame({
|
||||
{!desktop ? <span className="sr-only">移动端保持地图与抽屉工作台</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function TaskbarButton({ active, minimized, icon, label, onClick }: { active: boolean; minimized: boolean; icon: ReactNode; label: string; onClick: () => void }) {
|
||||
return (
|
||||
@@ -520,11 +399,3 @@ function getWorkspaceWindowTitle(state: WorkspaceState, windowId: string) {
|
||||
if (windowId === state.map.id) return state.map.title;
|
||||
return state.analyses.find((window) => window.id === windowId)?.title ?? "工作区窗口";
|
||||
}
|
||||
|
||||
function NavigationPanel({ active, children }: { active: boolean; children: ReactNode }) {
|
||||
return (
|
||||
<div hidden={!active} className="absolute inset-0 min-h-0 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user