From d2871da74b51eebbb387c6be0954015b50fb7f17 Mon Sep 17 00:00:00 2001 From: ErSan Date: Tue, 27 Jan 2026 21:21:25 +0800 Subject: [PATCH] feat(editor): align layout skeleton --- packages/editor/src/editor/Canvas.tsx | 2 +- packages/editor/src/editor/EditorApp.tsx | 393 ++++++++++++++++++----- 2 files changed, 317 insertions(+), 78 deletions(-) diff --git a/packages/editor/src/editor/Canvas.tsx b/packages/editor/src/editor/Canvas.tsx index 0ad77d1..fa1ba1f 100644 --- a/packages/editor/src/editor/Canvas.tsx +++ b/packages/editor/src/editor/Canvas.tsx @@ -38,7 +38,7 @@ export interface CanvasProps { } function isPrimaryButton(e: React.PointerEvent | PointerEvent): boolean { - // align with goView: ignore middle click in box select + // Ignore middle click in box select. return (e as PointerEvent).buttons === 1 || (e as PointerEvent).button === 0; } diff --git a/packages/editor/src/editor/EditorApp.tsx b/packages/editor/src/editor/EditorApp.tsx index 27168cd..93fca77 100644 --- a/packages/editor/src/editor/EditorApp.tsx +++ b/packages/editor/src/editor/EditorApp.tsx @@ -1,22 +1,56 @@ -import { Button, Divider, Input, Layout, Space, Typography } from 'antd'; +import { + Button, + Divider, + Input, + InputNumber, + Layout, + Segmented, + Space, + Tabs, + Typography, +} from 'antd'; import { useEffect, useMemo, useReducer, useState } from 'react'; -import { createInitialState, editorReducer, exportScreenJSON } from './store'; import { bindEditorHotkeys } from './hotkeys'; import { Canvas } from './Canvas'; import { Inspector } from './Inspector'; +import { createInitialState, editorReducer, exportScreenJSON } from './store'; const { Header, Sider, Content } = Layout; +type RightPanelTabKey = 'custom' | 'animation' | 'data' | 'event'; + +type LeftCategoryKey = + | 'components' + | 'charts' + | 'info' + | 'list' + | 'widgets' + | 'image' + | 'icon'; + +function PanelTitle(props: { title: string; extra?: React.ReactNode }) { + return ( +
+ {props.title} + {props.extra} +
+ ); +} + export function EditorApp() { const [state, dispatch] = useReducer(editorReducer, undefined, createInitialState); const [importText, setImportText] = useState(''); + const [leftCategory, setLeftCategory] = useState('charts'); + const [rightTab, setRightTab] = useState('custom'); + + const selected = state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0]); + const hasSelection = state.selection.ids.length > 0; const bounds = useMemo( () => ({ w: state.doc.screen.width, h: state.doc.screen.height }), [state.doc.screen.width, state.doc.screen.height], ); - // keyboard tracking (ctrl/space) — goView uses window.$KeyboardActive useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { dispatch({ type: 'keyboard', ctrl: e.ctrlKey || e.metaKey, space: e.code === 'Space' || state.keyboard.space }); @@ -33,9 +67,6 @@ export function EditorApp() { }; }, [state.keyboard.space]); - // Canvas handles its own context menu. - - // Hotkeys (goView-like) useEffect(() => { return bindEditorHotkeys(() => false, dispatch); }, []); @@ -45,75 +76,205 @@ export function EditorApp() { }; return ( - -
- - AstralView Editor - - - - - + + + + + + Workspace +
- Export JSON (copy) + 1 +
+
+ + + +
- - Inspector - - Selected: {state.selection.ids.length ? state.selection.ids.join(', ') : 'None'} - + {/* Left icon rail */} + +
+ {([ + ['components', '组件'], + ['charts', '图表'], + ['info', '信息'], + ['list', '列表'], + ['widgets', '小组件'], + ['image', '图片'], + ['icon', '图标'], + ] as Array<[LeftCategoryKey, string]>).map(([key, label]) => { + const active = leftCategory === key; + return ( + + ); + })} +
+
- n.id === state.selection.ids[0])} - onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })} - onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })} - onUpdateIframeProps={(id, props) => dispatch({ type: 'updateIframeProps', id, props })} - onUpdateVideoProps={(id, props) => dispatch({ type: 'updateVideoProps', id, props })} + {/* Left library panel */} + + + +
+ +
- + {/* Placeholder: we will replace with real registry-based catalog */} +
+ {[ + { title: '柱状图', subtitle: 'Chart' }, + { title: '折线图', subtitle: 'Chart' }, + { title: '图片', subtitle: 'Media' }, + { title: '视频', subtitle: 'Media' }, + { title: '网页', subtitle: 'Embed' }, + { title: '文本', subtitle: 'Text' }, + ].map((it) => ( +
+
+
+
{it.title}
+
{it.subtitle}
+
+
+ ))} +
- Import/Export + + setImportText(e.target.value)} - rows={10} - placeholder="Paste screen JSON here" + rows={6} + placeholder="粘贴 JSON" + style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.10)', color: '#e2e8f0' }} /> - + - - - Notes - - Interactions target goView semantics: Ctrl multi-select, box-select full containment, scale-aware movement. - - + {/* Center */} + dispatch({ type: 'toggleHideSelected' })} onBringToFrontSelected={() => dispatch({ type: 'bringToFrontSelected' })} onSendToBackSelected={() => dispatch({ type: 'sendToBackSelected' })} - onBeginPan={(e) => { - dispatch({ type: 'beginPan', start: { screenX: e.screenX, screenY: e.screenY } }); - }} - onUpdatePan={(e: PointerEvent) => { - dispatch({ type: 'updatePan', current: { screenX: e.screenX, screenY: e.screenY } }); - }} + onBeginPan={(e) => dispatch({ type: 'beginPan', start: { screenX: e.screenX, screenY: e.screenY } })} + onUpdatePan={(e: PointerEvent) => dispatch({ type: 'updatePan', current: { screenX: e.screenX, screenY: e.screenY } })} onEndPan={() => dispatch({ type: 'endPan' })} - onBeginMove={(e) => { - dispatch({ type: 'beginMove', start: { screenX: e.screenX, screenY: e.screenY }, bounds }); - }} - onUpdateMove={(e: PointerEvent) => { - dispatch({ type: 'updateMove', current: { screenX: e.screenX, screenY: e.screenY }, bounds }); - }} + onBeginMove={(e) => dispatch({ type: 'beginMove', start: { screenX: e.screenX, screenY: e.screenY }, bounds })} + onUpdateMove={(e: PointerEvent) => dispatch({ type: 'updateMove', current: { screenX: e.screenX, screenY: e.screenY }, bounds })} onEndMove={() => dispatch({ type: 'endMove' })} - onBeginBoxSelect={(e, offsetX, offsetY, additive) => { + onBeginBoxSelect={(e, offsetX, offsetY, additive) => dispatch({ type: 'beginBoxSelect', additive, start: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY }, - }); - }} - onUpdateBoxSelect={(e: PointerEvent, offsetX, offsetY) => { - dispatch({ type: 'updateBoxSelect', current: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY } }); - }} + }) + } + onUpdateBoxSelect={(e: PointerEvent, offsetX, offsetY) => + dispatch({ type: 'updateBoxSelect', current: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY } }) + } onEndBoxSelect={() => dispatch({ type: 'endBoxSelect' })} - onBeginResize={(e, id, handle) => { - dispatch({ type: 'beginResize', id, handle, start: { screenX: e.screenX, screenY: e.screenY }, bounds }); - }} - onUpdateResize={(e: PointerEvent) => { - dispatch({ type: 'updateResize', current: { screenX: e.screenX, screenY: e.screenY }, bounds }); - }} + onBeginResize={(e, id, handle) => + dispatch({ type: 'beginResize', id, handle, start: { screenX: e.screenX, screenY: e.screenY }, bounds }) + } + onUpdateResize={(e: PointerEvent) => dispatch({ type: 'updateResize', current: { screenX: e.screenX, screenY: e.screenY }, bounds })} onEndResize={() => dispatch({ type: 'endResize' })} onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })} onWheelPan={(dx, dy) => dispatch({ type: 'panBy', dx, dy })} - onZoomAt={(scale, anchorX, anchorY) => { - const next = Math.max(0.1, Math.min(4, scale)); - dispatch({ type: 'zoomAt', scale: next, anchor: { x: anchorX, y: anchorY } }); - }} + onZoomAt={(scale, anchorX, anchorY) => dispatch({ type: 'zoomAt', scale: Math.max(0.1, Math.min(4, scale)), anchor: { x: anchorX, y: anchorY } })} /> + + {/* Right panel */} + + {hasSelection ? ( + <> + setRightTab(k as RightPanelTabKey)} + size="small" + items={[ + { key: 'custom', label: '定制', children: null }, + { key: 'animation', label: '动画', children: null }, + { key: 'data', label: '数据', children: null }, + { key: 'event', label: '事件', children: null }, + ]} + /> + + {rightTab === 'custom' ? ( + <> +
+ + {state.selection.ids.length}/12 + + } + /> + +
+ 名称 + + + 尺寸 + + + + + + 位置 + + + + +
+ + + + dispatch({ type: 'updateTextProps', id, props })} + onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })} + onUpdateIframeProps={(id, props) => dispatch({ type: 'updateIframeProps', id, props })} + onUpdateVideoProps={(id, props) => dispatch({ type: 'updateVideoProps', id, props })} + /> +
+ + ) : ( + 占位:该面板将在后续补齐。 + )} + + ) : ( + <> + +
+ 宽度 + + + 高度 + +
+ + + 占位:页面背景 / 主题 / 配色将在后续补齐。 + + )} +
);