Files
Genarrative/src/components/image-editor/ImageCanvasPanelDockView.tsx
kdletters d8b935317d 拆分编辑器前端画布视图
抽出素材栏、生成器、舞台工具栏和画布世界视图

补充各拆分视图的聚焦测试

更新 TRACKING.md 记录第三十四阶段验证
2026-06-17 17:48:12 +08:00

319 lines
11 KiB
TypeScript

import {
ImagePlus,
Layers,
Map as MapIcon,
Redo2,
RotateCcw,
Undo2,
X,
} from 'lucide-react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import { PlatformFloatingMenu, PlatformFloatingMenuItem } from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { StageMinimapModel } from './ImageCanvasInteractionModel';
import {
CANVAS_BACKGROUND_OPTIONS,
DEFAULT_CANVAS_BACKGROUND_COLOR,
formatPercent,
} from './ImageCanvasEditorModel';
import type { CanvasViewport, SidebarPanel } from './ImageCanvasEditorTypes';
type ImageCanvasPanelDockViewProps = {
viewport: CanvasViewport;
canvasBackgroundColor: string;
canvasBackgroundHexValue: string;
canUndo: boolean;
canRedo: boolean;
isZoomMenuOpen: boolean;
isBackgroundSettingsOpen: boolean;
activeSidebarPanel: SidebarPanel | null;
isMinimapOpen: boolean;
minimapModel: StageMinimapModel | null;
onFitLayers: () => void;
onUndoCanvasChange: () => void;
onRedoCanvasChange: () => void;
onUpdateScaleFromCenter: (nextScale: number) => void;
onToggleZoomMenu: () => void;
onCloseZoomMenu: () => void;
onToggleBackgroundSettings: () => void;
onApplyCanvasBackgroundColor: (color: string) => void;
onCanvasBackgroundHexChange: (value: string) => void;
onToggleSidebarPanel: (panel: SidebarPanel) => void;
onToggleMinimap: () => void;
onMinimapPointerDown: (event: ReactPointerEvent<HTMLButtonElement>) => void;
};
export function ImageCanvasPanelDockView({
viewport,
canvasBackgroundColor,
canvasBackgroundHexValue,
canUndo,
canRedo,
isZoomMenuOpen,
isBackgroundSettingsOpen,
activeSidebarPanel,
isMinimapOpen,
minimapModel,
onFitLayers,
onUndoCanvasChange,
onRedoCanvasChange,
onUpdateScaleFromCenter,
onToggleZoomMenu,
onCloseZoomMenu,
onToggleBackgroundSettings,
onApplyCanvasBackgroundColor,
onCanvasBackgroundHexChange,
onToggleSidebarPanel,
onToggleMinimap,
onMinimapPointerDown,
}: ImageCanvasPanelDockViewProps) {
return (
<>
<EditorIconButton
className="image-canvas-editor__reset-button"
label="重置画布视图"
title="重置画布视图"
icon={RotateCcw}
onClick={() => onFitLayers()}
/>
<div
className="image-canvas-editor__panel-dock"
role="toolbar"
aria-label="画布面板入口"
onPointerDown={(event) => event.stopPropagation()}
>
<EditorIconButton
label="撤销"
title="撤销"
icon={Undo2}
disabled={!canUndo}
onClick={onUndoCanvasChange}
/>
<EditorIconButton
label="重做"
title="重做"
icon={Redo2}
disabled={!canRedo}
onClick={onRedoCanvasChange}
/>
<div className="image-canvas-editor__zoom-menu-wrap">
<PlatformInlineOptionButton
className="image-canvas-editor__zoom-trigger"
aria-label={`当前缩放比例 ${formatPercent(viewport.scale)}`}
aria-haspopup="menu"
aria-expanded={isZoomMenuOpen}
onClick={onToggleZoomMenu}
>
{formatPercent(viewport.scale)}
</PlatformInlineOptionButton>
{isZoomMenuOpen ? (
<PlatformFloatingMenu label="缩放菜单" placement="top-start">
<PlatformFloatingMenuItem
className="image-canvas-editor__zoom-menu-item"
onClick={() => {
onUpdateScaleFromCenter(viewport.scale * 1.16);
onCloseZoomMenu();
}}
>
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__zoom-menu-item"
onClick={() => {
onUpdateScaleFromCenter(viewport.scale * 0.86);
onCloseZoomMenu();
}}
>
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__zoom-menu-item"
onClick={() => {
onFitLayers();
onCloseZoomMenu();
}}
>
</PlatformFloatingMenuItem>
{[0.5, 1, 2].map((scale) => (
<PlatformFloatingMenuItem
key={scale}
className="image-canvas-editor__zoom-menu-item"
onClick={() => {
onUpdateScaleFromCenter(scale);
onCloseZoomMenu();
}}
>
{Math.round(scale * 100)}%
</PlatformFloatingMenuItem>
))}
</PlatformFloatingMenu>
) : null}
</div>
<div className="image-canvas-editor__background-control">
<PlatformIconButton
label="画布背景色"
title="画布背景色"
aria-expanded={isBackgroundSettingsOpen}
onClick={onToggleBackgroundSettings}
icon={
<span
className="image-canvas-editor__background-swatch-current"
style={{ backgroundColor: canvasBackgroundColor }}
/>
}
/>
{isBackgroundSettingsOpen ? (
<div
className="image-canvas-editor__background-panel"
role="dialog"
aria-label="画布背景设置"
>
<div className="image-canvas-editor__background-panel-head">
<span></span>
<button
type="button"
className="image-canvas-editor__background-close"
aria-label="关闭画布背景设置"
onClick={onToggleBackgroundSettings}
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="image-canvas-editor__background-current-row">
<span
className="image-canvas-editor__background-current-preview"
style={{ backgroundColor: canvasBackgroundColor }}
aria-hidden="true"
/>
<span>{canvasBackgroundColor}</span>
</div>
<label className="image-canvas-editor__background-spectrum">
<input
type="color"
aria-label="画布背景色相"
value={canvasBackgroundColor}
onChange={(event) =>
onApplyCanvasBackgroundColor(event.currentTarget.value)
}
/>
<span
className="image-canvas-editor__background-spectrum-surface"
aria-hidden="true"
/>
<span
className="image-canvas-editor__background-spectrum-handle"
aria-hidden="true"
/>
</label>
<label className="image-canvas-editor__background-hue">
<input
type="color"
aria-label="自定义画布背景色"
value={canvasBackgroundColor}
onChange={(event) =>
onApplyCanvasBackgroundColor(event.currentTarget.value)
}
/>
</label>
<div
className="image-canvas-editor__background-presets"
aria-label="画布背景预设色"
>
{CANVAS_BACKGROUND_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className="image-canvas-editor__background-preset"
aria-label={option.label}
aria-pressed={canvasBackgroundColor === option.value}
onClick={() => onApplyCanvasBackgroundColor(option.value)}
>
<span
className="image-canvas-editor__background-swatch"
style={{ backgroundColor: option.value }}
/>
</button>
))}
</div>
<div className="image-canvas-editor__background-footer">
<label className="image-canvas-editor__background-hex-field">
<span>HEX</span>
<input
aria-label="画布背景十六进制颜色"
value={canvasBackgroundHexValue}
spellCheck={false}
onChange={(event) =>
onCanvasBackgroundHexChange(event.currentTarget.value)
}
/>
</label>
<button
type="button"
className="image-canvas-editor__background-reset"
onClick={() =>
onApplyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR)
}
>
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</div>
</div>
) : null}
</div>
<EditorIconButton
label="打开素材"
title="素材"
icon={ImagePlus}
pressed={activeSidebarPanel === 'assets'}
onClick={() => onToggleSidebarPanel('assets')}
/>
<EditorIconButton
label="打开图层"
title="图层"
icon={Layers}
pressed={activeSidebarPanel === 'layers'}
onClick={() => onToggleSidebarPanel('layers')}
/>
<EditorIconButton
label="切换小地图"
title="小地图"
icon={MapIcon}
pressed={isMinimapOpen}
onClick={onToggleMinimap}
/>
</div>
{isMinimapOpen && minimapModel ? (
<button
type="button"
className="image-canvas-editor__minimap"
aria-label="画布小地图"
title="拖拽移动视图"
onPointerDown={onMinimapPointerDown}
>
<span className="image-canvas-editor__minimap-stage">
{minimapModel.layers.map((layer) => (
<span
key={layer.id}
className="image-canvas-editor__minimap-layer"
title={layer.title}
style={layer.rect}
/>
))}
<span
className="image-canvas-editor__minimap-viewport"
style={minimapModel.viewport}
/>
</span>
</button>
) : null}
</>
);
}