feat(editor): align layout skeleton
This commit is contained in:
parent
5f093ed157
commit
d2871da74b
@ -38,7 +38,7 @@ export interface CanvasProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPrimaryButton(e: React.PointerEvent | PointerEvent): boolean {
|
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;
|
return (e as PointerEvent).buttons === 1 || (e as PointerEvent).button === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 { useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
import { createInitialState, editorReducer, exportScreenJSON } from './store';
|
|
||||||
import { bindEditorHotkeys } from './hotkeys';
|
import { bindEditorHotkeys } from './hotkeys';
|
||||||
import { Canvas } from './Canvas';
|
import { Canvas } from './Canvas';
|
||||||
import { Inspector } from './Inspector';
|
import { Inspector } from './Inspector';
|
||||||
|
import { createInitialState, editorReducer, exportScreenJSON } from './store';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
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 (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
|
<Typography.Text style={{ color: '#cbd5e1', fontSize: 12, letterSpacing: 0.6 }}>{props.title}</Typography.Text>
|
||||||
|
{props.extra}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorApp() {
|
export function EditorApp() {
|
||||||
const [state, dispatch] = useReducer(editorReducer, undefined, createInitialState);
|
const [state, dispatch] = useReducer(editorReducer, undefined, createInitialState);
|
||||||
const [importText, setImportText] = useState('');
|
const [importText, setImportText] = useState('');
|
||||||
|
const [leftCategory, setLeftCategory] = useState<LeftCategoryKey>('charts');
|
||||||
|
const [rightTab, setRightTab] = useState<RightPanelTabKey>('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(
|
const bounds = useMemo(
|
||||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||||
[state.doc.screen.width, state.doc.screen.height],
|
[state.doc.screen.width, state.doc.screen.height],
|
||||||
);
|
);
|
||||||
|
|
||||||
// keyboard tracking (ctrl/space) — goView uses window.$KeyboardActive
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
dispatch({ type: 'keyboard', ctrl: e.ctrlKey || e.metaKey, space: e.code === 'Space' || state.keyboard.space });
|
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]);
|
}, [state.keyboard.space]);
|
||||||
|
|
||||||
// Canvas handles its own context menu.
|
|
||||||
|
|
||||||
// Hotkeys (goView-like)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return bindEditorHotkeys(() => false, dispatch);
|
return bindEditorHotkeys(() => false, dispatch);
|
||||||
}, []);
|
}, []);
|
||||||
@ -45,75 +76,205 @@ export function EditorApp() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ minHeight: '100vh', background: '#0b1220' }}>
|
||||||
<Header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<Header
|
||||||
<Typography.Title level={4} style={{ margin: 0, color: '#fff' }}>
|
style={{
|
||||||
AstralView Editor
|
height: 44,
|
||||||
</Typography.Title>
|
padding: '0 12px',
|
||||||
<Space>
|
display: 'flex',
|
||||||
<Button onClick={() => dispatch({ type: 'undo' })} disabled={!state.history.past.length}>
|
alignItems: 'center',
|
||||||
Undo
|
justifyContent: 'space-between',
|
||||||
|
background: '#0f172a',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space size={10}>
|
||||||
|
<Button size="small" type="text" style={{ color: '#cbd5e1' }}>
|
||||||
|
Home
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => dispatch({ type: 'redo' })} disabled={!state.history.future.length}>
|
<Space size={6}>
|
||||||
Redo
|
<Button size="small" type="text" style={{ color: '#94a3b8' }}>
|
||||||
</Button>
|
◀
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => {
|
<Button size="small" type="text" style={{ color: '#94a3b8' }}>
|
||||||
const json = api.exportJSON();
|
▶
|
||||||
setImportText(json);
|
</Button>
|
||||||
void navigator.clipboard?.writeText(json);
|
</Space>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space size={8}>
|
||||||
|
<Typography.Text style={{ color: '#cbd5e1', fontSize: 12 }}>Workspace</Typography.Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 22,
|
||||||
|
padding: '0 8px',
|
||||||
|
borderRadius: 6,
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#cbd5e1',
|
||||||
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export JSON (copy)
|
1
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space size={8}>
|
||||||
|
<Button size="small" style={{ background: 'rgba(255,255,255,0.06)', color: '#e2e8f0', border: 'none' }}>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
<Button size="small" type="primary">
|
||||||
|
Publish
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Sider width={360} theme="light" style={{ padding: 16, overflow: 'auto' }}>
|
{/* Left icon rail */}
|
||||||
<Typography.Title level={5}>Inspector</Typography.Title>
|
<Sider
|
||||||
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
width={56}
|
||||||
Selected: {state.selection.ids.length ? state.selection.ids.join(', ') : 'None'}
|
theme="dark"
|
||||||
</Typography.Paragraph>
|
style={{
|
||||||
|
background: '#111827',
|
||||||
|
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
paddingTop: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: '0 8px' }}>
|
||||||
|
{([
|
||||||
|
['components', '组件'],
|
||||||
|
['charts', '图表'],
|
||||||
|
['info', '信息'],
|
||||||
|
['list', '列表'],
|
||||||
|
['widgets', '小组件'],
|
||||||
|
['image', '图片'],
|
||||||
|
['icon', '图标'],
|
||||||
|
] as Array<[LeftCategoryKey, string]>).map(([key, label]) => {
|
||||||
|
const active = leftCategory === key;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLeftCategory(key)}
|
||||||
|
style={{
|
||||||
|
height: 34,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
background: active ? 'rgba(16,185,129,0.12)' : 'transparent',
|
||||||
|
color: active ? '#a7f3d0' : '#cbd5e1',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
{label.slice(0, 1)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Sider>
|
||||||
|
|
||||||
<Inspector
|
{/* Left library panel */}
|
||||||
selected={state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0])}
|
<Sider
|
||||||
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
width={300}
|
||||||
onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })}
|
theme="dark"
|
||||||
onUpdateIframeProps={(id, props) => dispatch({ type: 'updateIframeProps', id, props })}
|
style={{
|
||||||
onUpdateVideoProps={(id, props) => dispatch({ type: 'updateVideoProps', id, props })}
|
background: '#111827',
|
||||||
|
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
padding: 12,
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PanelTitle title="组件" />
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder="搜索组件"
|
||||||
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.10)', color: '#e2e8f0' }}
|
||||||
/>
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Segmented
|
||||||
|
size="small"
|
||||||
|
value="all"
|
||||||
|
options={[
|
||||||
|
{ label: '所有', value: 'all' },
|
||||||
|
{ label: '常用', value: 'common' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Divider />
|
{/* Placeholder: we will replace with real registry-based catalog */}
|
||||||
|
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
|
{[
|
||||||
|
{ title: '柱状图', subtitle: 'Chart' },
|
||||||
|
{ title: '折线图', subtitle: 'Chart' },
|
||||||
|
{ title: '图片', subtitle: 'Media' },
|
||||||
|
{ title: '视频', subtitle: 'Media' },
|
||||||
|
{ title: '网页', subtitle: 'Embed' },
|
||||||
|
{ title: '文本', subtitle: 'Text' },
|
||||||
|
].map((it) => (
|
||||||
|
<div
|
||||||
|
key={it.title}
|
||||||
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
padding: 10,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 72,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'rgba(255,255,255,0.06)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ color: '#e2e8f0', fontSize: 13 }}>{it.title}</div>
|
||||||
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>{it.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Typography.Title level={5}>Import/Export</Typography.Title>
|
<Divider style={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
<PanelTitle title="导入 / 导出" />
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
value={importText}
|
value={importText}
|
||||||
onChange={(e) => setImportText(e.target.value)}
|
onChange={(e) => setImportText(e.target.value)}
|
||||||
rows={10}
|
rows={6}
|
||||||
placeholder="Paste screen JSON here"
|
placeholder="粘贴 JSON"
|
||||||
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.10)', color: '#e2e8f0' }}
|
||||||
/>
|
/>
|
||||||
<Space style={{ marginTop: 8 }}>
|
<Space style={{ marginTop: 8 }}>
|
||||||
<Button
|
<Button
|
||||||
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => dispatch({ type: 'importJSON', json: importText })}
|
||||||
dispatch({ type: 'importJSON', json: importText });
|
|
||||||
}}
|
|
||||||
disabled={!importText.trim()}
|
disabled={!importText.trim()}
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setImportText(api.exportJSON())}>Load current</Button>
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
const json = api.exportJSON();
|
||||||
|
setImportText(json);
|
||||||
|
void navigator.clipboard?.writeText(json);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Typography.Title level={5}>Notes</Typography.Title>
|
|
||||||
<Typography.Paragraph style={{ color: '#666' }}>
|
|
||||||
Interactions target goView semantics: Ctrl multi-select, box-select full containment, scale-aware movement.
|
|
||||||
</Typography.Paragraph>
|
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|
||||||
<Content style={{ padding: 16, overflow: 'auto' }}>
|
{/* Center */}
|
||||||
|
<Content style={{ padding: 12, overflow: 'hidden', background: '#0b1220' }}>
|
||||||
<Canvas
|
<Canvas
|
||||||
screen={state.doc.screen}
|
screen={state.doc.screen}
|
||||||
selectionIds={state.selection.ids}
|
selectionIds={state.selection.ids}
|
||||||
@ -130,46 +291,124 @@ export function EditorApp() {
|
|||||||
onToggleHideSelected={() => dispatch({ type: 'toggleHideSelected' })}
|
onToggleHideSelected={() => dispatch({ type: 'toggleHideSelected' })}
|
||||||
onBringToFrontSelected={() => dispatch({ type: 'bringToFrontSelected' })}
|
onBringToFrontSelected={() => dispatch({ type: 'bringToFrontSelected' })}
|
||||||
onSendToBackSelected={() => dispatch({ type: 'sendToBackSelected' })}
|
onSendToBackSelected={() => dispatch({ type: 'sendToBackSelected' })}
|
||||||
onBeginPan={(e) => {
|
onBeginPan={(e) => dispatch({ type: 'beginPan', start: { screenX: e.screenX, screenY: e.screenY } })}
|
||||||
dispatch({ type: 'beginPan', start: { screenX: e.screenX, screenY: e.screenY } });
|
onUpdatePan={(e: PointerEvent) => dispatch({ type: 'updatePan', current: { screenX: e.screenX, screenY: e.screenY } })}
|
||||||
}}
|
|
||||||
onUpdatePan={(e: PointerEvent) => {
|
|
||||||
dispatch({ type: 'updatePan', current: { screenX: e.screenX, screenY: e.screenY } });
|
|
||||||
}}
|
|
||||||
onEndPan={() => dispatch({ type: 'endPan' })}
|
onEndPan={() => dispatch({ type: 'endPan' })}
|
||||||
onBeginMove={(e) => {
|
onBeginMove={(e) => dispatch({ type: 'beginMove', start: { screenX: e.screenX, screenY: e.screenY }, bounds })}
|
||||||
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 })}
|
||||||
}}
|
|
||||||
onUpdateMove={(e: PointerEvent) => {
|
|
||||||
dispatch({ type: 'updateMove', current: { screenX: e.screenX, screenY: e.screenY }, bounds });
|
|
||||||
}}
|
|
||||||
onEndMove={() => dispatch({ type: 'endMove' })}
|
onEndMove={() => dispatch({ type: 'endMove' })}
|
||||||
onBeginBoxSelect={(e, offsetX, offsetY, additive) => {
|
onBeginBoxSelect={(e, offsetX, offsetY, additive) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'beginBoxSelect',
|
type: 'beginBoxSelect',
|
||||||
additive,
|
additive,
|
||||||
start: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY },
|
start: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY },
|
||||||
});
|
})
|
||||||
}}
|
}
|
||||||
onUpdateBoxSelect={(e: PointerEvent, offsetX, offsetY) => {
|
onUpdateBoxSelect={(e: PointerEvent, offsetX, offsetY) =>
|
||||||
dispatch({ type: 'updateBoxSelect', current: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY } });
|
dispatch({ type: 'updateBoxSelect', current: { offsetX, offsetY, screenX: e.screenX, screenY: e.screenY } })
|
||||||
}}
|
}
|
||||||
onEndBoxSelect={() => dispatch({ type: 'endBoxSelect' })}
|
onEndBoxSelect={() => dispatch({ type: 'endBoxSelect' })}
|
||||||
onBeginResize={(e, id, handle) => {
|
onBeginResize={(e, id, handle) =>
|
||||||
dispatch({ type: 'beginResize', id, handle, start: { screenX: e.screenX, screenY: e.screenY }, bounds });
|
dispatch({ type: 'beginResize', id, handle, start: { screenX: e.screenX, screenY: e.screenY }, bounds })
|
||||||
}}
|
}
|
||||||
onUpdateResize={(e: PointerEvent) => {
|
onUpdateResize={(e: PointerEvent) => dispatch({ type: 'updateResize', current: { screenX: e.screenX, screenY: e.screenY }, bounds })}
|
||||||
dispatch({ type: 'updateResize', current: { screenX: e.screenX, screenY: e.screenY }, bounds });
|
|
||||||
}}
|
|
||||||
onEndResize={() => dispatch({ type: 'endResize' })}
|
onEndResize={() => dispatch({ type: 'endResize' })}
|
||||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||||
onWheelPan={(dx, dy) => dispatch({ type: 'panBy', dx, dy })}
|
onWheelPan={(dx, dy) => dispatch({ type: 'panBy', dx, dy })}
|
||||||
onZoomAt={(scale, anchorX, anchorY) => {
|
onZoomAt={(scale, anchorX, anchorY) => dispatch({ type: 'zoomAt', scale: Math.max(0.1, Math.min(4, scale)), anchor: { x: anchorX, y: anchorY } })}
|
||||||
const next = Math.max(0.1, Math.min(4, scale));
|
|
||||||
dispatch({ type: 'zoomAt', scale: next, anchor: { x: anchorX, y: anchorY } });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
|
{/* Right panel */}
|
||||||
|
<Sider
|
||||||
|
width={360}
|
||||||
|
theme="dark"
|
||||||
|
style={{
|
||||||
|
background: '#111827',
|
||||||
|
borderLeft: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
padding: 12,
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasSelection ? (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
activeKey={rightTab}
|
||||||
|
onChange={(k) => 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' ? (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<PanelTitle
|
||||||
|
title="组件配置"
|
||||||
|
extra={
|
||||||
|
<Typography.Text style={{ color: '#64748b', fontSize: 12 }}>
|
||||||
|
{state.selection.ids.length}/12
|
||||||
|
</Typography.Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '64px 1fr', gap: 10, alignItems: 'center' }}>
|
||||||
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>名称</Typography.Text>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
value={selected?.type ?? ''}
|
||||||
|
disabled
|
||||||
|
style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.10)', color: '#e2e8f0' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>尺寸</Typography.Text>
|
||||||
|
<Space size={8}>
|
||||||
|
<InputNumber size="small" value={selected?.rect.w ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
|
<InputNumber size="small" value={selected?.rect.h ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>位置</Typography.Text>
|
||||||
|
<Space size={8}>
|
||||||
|
<InputNumber size="small" value={selected?.rect.x ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
|
<InputNumber size="small" value={selected?.rect.y ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
|
||||||
|
<Inspector
|
||||||
|
selected={selected}
|
||||||
|
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 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography.Paragraph style={{ color: '#64748b' }}>占位:该面板将在后续补齐。</Typography.Paragraph>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PanelTitle title="页面配置" />
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '64px 1fr', gap: 10, alignItems: 'center' }}>
|
||||||
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>宽度</Typography.Text>
|
||||||
|
<InputNumber size="small" value={state.doc.screen.width} controls={false} readOnly style={{ width: 160 }} />
|
||||||
|
|
||||||
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>高度</Typography.Text>
|
||||||
|
<InputNumber size="small" value={state.doc.screen.height} controls={false} readOnly style={{ width: 160 }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{ borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
<Typography.Paragraph style={{ color: '#64748b' }}>占位:页面背景 / 主题 / 配色将在后续补齐。</Typography.Paragraph>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Sider>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user