重构 Inspector 组件和 PreviewWorkspace 功能:
- 合并 InspectorContent 逻辑到 InspectorSidebar。 - 优化 Inspector 内容渲染,移除重复代码,支持组件统一删除操作。 - 添加节点交互逻辑接口 (`useNodeTransformInteraction`),提升 PreviewWorkspace 的可维护性。
This commit is contained in:
@@ -84,35 +84,6 @@ export function EditorDialogs({
|
||||
</button>
|
||||
</div>
|
||||
</ThemedModal>
|
||||
|
||||
<ThemedModal
|
||||
open={controller.pendingRoleChange !== null}
|
||||
onClose={controller.cancelRoleChange}
|
||||
ariaLabel="确认移除页面组件树"
|
||||
panelClassName="w-[440px] rounded-2xl p-5"
|
||||
>
|
||||
<h2 className="m-0 text-base font-semibold">确认移除页面组件树?</h2>
|
||||
<p className="text-sm leading-6 text-(--platform-text-soft)">
|
||||
这张界面图当前是 Page,已有 UI
|
||||
Tree。改为其它界面角色会自动删除页面根节点及其全部子节点,操作不可恢复。
|
||||
</p>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
|
||||
onClick={controller.cancelRoleChange}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white"
|
||||
onClick={controller.confirmRoleChange}
|
||||
>
|
||||
移除组件树并修改
|
||||
</button>
|
||||
</div>
|
||||
</ThemedModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+132
-73
@@ -11,6 +11,7 @@ import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UID
|
||||
import { UI_DESIGN_IMAGE_ROLES } from '../../model';
|
||||
import type { UiEditorPageController } from '../../useUiEditorPage';
|
||||
import { PrerequisiteIssues } from '../PrerequisiteIssues';
|
||||
import { ComponentPanel } from './Components/ComponentPanel';
|
||||
import { SpriteBorderEditor } from './SpriteBorder/SpriteBorderEditor';
|
||||
import TransformEditor from './Transform/TransformEditor';
|
||||
|
||||
@@ -59,6 +60,68 @@ export function InspectorSidebar({
|
||||
controller: UiEditorPageController;
|
||||
}) {
|
||||
const view = getInspectorView(controller);
|
||||
let inspectorContent: ReactNode;
|
||||
switch (view.kind) {
|
||||
case 'node':
|
||||
inspectorContent = (
|
||||
<NodeInspector
|
||||
node={view.node}
|
||||
parentSize={view.parentSize}
|
||||
readOnly={
|
||||
controller.editor.isLocked ||
|
||||
view.node.id === controller.treeForActiveImage?.root.id
|
||||
}
|
||||
keepChildrenUnchanged={controller.keepChildrenUnchanged}
|
||||
onKeepChildrenUnchangedChange={controller.setKeepChildrenUnchanged}
|
||||
onMetadataChange={controller.setNodeMetadata}
|
||||
onTransformChange={controller.setNodeTransform}
|
||||
sprites={controller.sprites}
|
||||
fonts={controller.editor.state.font_assets}
|
||||
onSetComponents={controller.setNodeComponents}
|
||||
onInsertComponent={controller.insertNodeComponent}
|
||||
onDeleteComponent={controller.deleteNodeComponent}
|
||||
onMoveComponent={controller.moveNodeComponent}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'sprite':
|
||||
inspectorContent = (
|
||||
<SpriteInspector
|
||||
sprite={view.sprite}
|
||||
spriteId={view.spriteId}
|
||||
previewUrl={view.previewUrl}
|
||||
referenceCount={view.referenceCount}
|
||||
onNameChange={controller.setSpriteName}
|
||||
onAssetTypeChange={controller.setSpriteAssetType}
|
||||
onBorderChange={controller.setSpriteBorder}
|
||||
onDelete={() => controller.requestSpriteRemoval(view.spriteId)}
|
||||
deleteDisabled={controller.editor.isLocked}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'image':
|
||||
inspectorContent = (
|
||||
<ImageInspector
|
||||
image={view.image}
|
||||
imageId={view.imageId}
|
||||
pageOptions={view.pageOptions}
|
||||
onNameChange={controller.setImageName}
|
||||
onDescriptionChange={controller.setImageDescription}
|
||||
onRoleChange={controller.setImageRole}
|
||||
onSlaveToChange={controller.setImageSlaveTo}
|
||||
onDelete={() => controller.requestDesignImageRemoval(view.imageId)}
|
||||
deleteDisabled={controller.editor.isLocked}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'empty':
|
||||
inspectorContent = (
|
||||
<p className="mt-4 text-xs text-(--platform-text-soft)">
|
||||
选择一张界面图后可编辑其属性。
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="flex min-h-0 flex-col overflow-y-auto border-l border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-4">
|
||||
@@ -68,28 +131,13 @@ export function InspectorSidebar({
|
||||
{INSPECTOR_VIEW_TITLES[view.kind]}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<InspectorContent controller={controller} view={view} />
|
||||
{inspectorContent}
|
||||
|
||||
{controller.issues ? (
|
||||
<div className="mt-5">
|
||||
<PrerequisiteIssues issues={controller.issues} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{view.kind === 'sprite' ? (
|
||||
<DeleteResourceButton
|
||||
label="删除"
|
||||
disabled={controller.editor.isLocked}
|
||||
onClick={() => controller.requestSpriteRemoval(view.spriteId)}
|
||||
/>
|
||||
) : view.kind === 'image' ? (
|
||||
<DeleteResourceButton
|
||||
label="删除"
|
||||
disabled={controller.editor.isLocked}
|
||||
onClick={() => controller.requestDesignImageRemoval(view.imageId)}
|
||||
/>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -137,75 +185,61 @@ function getInspectorView(controller: UiEditorPageController): InspectorView {
|
||||
return { kind: 'empty' };
|
||||
}
|
||||
|
||||
function InspectorContent({
|
||||
controller,
|
||||
view,
|
||||
}: {
|
||||
controller: UiEditorPageController;
|
||||
view: InspectorView;
|
||||
}) {
|
||||
switch (view.kind) {
|
||||
case 'node':
|
||||
return (
|
||||
<NodeInspector
|
||||
node={view.node}
|
||||
parentSize={view.parentSize}
|
||||
readOnly={
|
||||
controller.editor.isLocked ||
|
||||
view.node.id === controller.treeForActiveImage?.root.id
|
||||
}
|
||||
onMetadataChange={controller.setNodeMetadata}
|
||||
onTransformChange={controller.setNodeTransform}
|
||||
/>
|
||||
);
|
||||
case 'sprite':
|
||||
return (
|
||||
<SpriteInspector
|
||||
sprite={view.sprite}
|
||||
spriteId={view.spriteId}
|
||||
previewUrl={view.previewUrl}
|
||||
referenceCount={view.referenceCount}
|
||||
onNameChange={controller.setSpriteName}
|
||||
onAssetTypeChange={controller.setSpriteAssetType}
|
||||
onBorderChange={controller.setSpriteBorder}
|
||||
/>
|
||||
);
|
||||
case 'image':
|
||||
return (
|
||||
<ImageInspector
|
||||
image={view.image}
|
||||
imageId={view.imageId}
|
||||
pageOptions={view.pageOptions}
|
||||
onNameChange={controller.setImageName}
|
||||
onDescriptionChange={controller.setImageDescription}
|
||||
onRoleChange={controller.setImageRole}
|
||||
onSlaveToChange={controller.setImageSlaveTo}
|
||||
/>
|
||||
);
|
||||
case 'empty':
|
||||
return (
|
||||
<p className="mt-4 text-xs text-(--platform-text-soft)">
|
||||
选择一张界面图后可编辑其属性。
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NodeInspector({
|
||||
node,
|
||||
parentSize,
|
||||
readOnly,
|
||||
keepChildrenUnchanged,
|
||||
onKeepChildrenUnchangedChange,
|
||||
onMetadataChange,
|
||||
onTransformChange,
|
||||
sprites,
|
||||
fonts,
|
||||
onSetComponents,
|
||||
onInsertComponent,
|
||||
onDeleteComponent,
|
||||
onMoveComponent,
|
||||
}: {
|
||||
node: SelectedNode;
|
||||
parentSize: UiEditorPageController['selectedNodeParentSize'];
|
||||
readOnly: boolean;
|
||||
keepChildrenUnchanged: boolean;
|
||||
onKeepChildrenUnchangedChange: (value: boolean) => void;
|
||||
onMetadataChange: UiEditorPageController['setNodeMetadata'];
|
||||
onTransformChange: UiEditorPageController['setNodeTransform'];
|
||||
sprites: UiEditorPageController['sprites'];
|
||||
fonts: UiEditorPageController['editor']['state']['font_assets'];
|
||||
onSetComponents: UiEditorPageController['setNodeComponents'];
|
||||
onInsertComponent: UiEditorPageController['insertNodeComponent'];
|
||||
onDeleteComponent: UiEditorPageController['deleteNodeComponent'];
|
||||
onMoveComponent: UiEditorPageController['moveNodeComponent'];
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div
|
||||
className="flex items-center rounded-lg border border-(--platform-subpanel-border) bg-white/70 p-0.5 text-[10px]"
|
||||
role="group"
|
||||
aria-label="子节点布局行为"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={!keepChildrenUnchanged}
|
||||
disabled={readOnly}
|
||||
onClick={() => onKeepChildrenUnchangedChange(false)}
|
||||
className={`flex-1 rounded-md px-2 py-1.5 transition ${!keepChildrenUnchanged ? 'bg-blue-600 text-white' : 'text-(--platform-text-soft) hover:bg-black/5'} disabled:cursor-not-allowed disabled:opacity-50`}
|
||||
>
|
||||
子节点跟随父节点变化
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={keepChildrenUnchanged}
|
||||
disabled={readOnly}
|
||||
onClick={() => onKeepChildrenUnchangedChange(true)}
|
||||
className={`flex-1 rounded-md px-2 py-1.5 transition ${keepChildrenUnchanged ? 'bg-orange-500 text-white' : 'text-(--platform-text-soft) hover:bg-black/5'} disabled:cursor-not-allowed disabled:opacity-50`}
|
||||
>
|
||||
子节点保持不变
|
||||
</button>
|
||||
</div>
|
||||
<InspectorInput
|
||||
label="名称"
|
||||
value={node.metadata.name}
|
||||
@@ -232,9 +266,16 @@ function NodeInspector({
|
||||
readOnly={readOnly}
|
||||
onChange={onTransformChange}
|
||||
/>
|
||||
<p className="m-0 text-[10px] text-(--platform-text-soft)">
|
||||
组件预览与编辑待支持。
|
||||
</p>
|
||||
<ComponentPanel
|
||||
components={node.components}
|
||||
sprites={sprites}
|
||||
fonts={fonts}
|
||||
readOnly={readOnly}
|
||||
onSetComponents={onSetComponents}
|
||||
onInsertComponent={onInsertComponent}
|
||||
onDeleteComponent={onDeleteComponent}
|
||||
onMoveComponent={onMoveComponent}
|
||||
/>
|
||||
<ResourceId value={node.id} />
|
||||
</div>
|
||||
);
|
||||
@@ -248,6 +289,8 @@ function SpriteInspector({
|
||||
onNameChange,
|
||||
onAssetTypeChange,
|
||||
onBorderChange,
|
||||
onDelete,
|
||||
deleteDisabled,
|
||||
}: {
|
||||
sprite: SelectedSprite;
|
||||
spriteId: SelectedSpriteId;
|
||||
@@ -256,6 +299,8 @@ function SpriteInspector({
|
||||
onNameChange: UiEditorPageController['setSpriteName'];
|
||||
onAssetTypeChange: UiEditorPageController['setSpriteAssetType'];
|
||||
onBorderChange: UiEditorPageController['setSpriteBorder'];
|
||||
onDelete: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
@@ -293,6 +338,11 @@ function SpriteInspector({
|
||||
onChange={onBorderChange}
|
||||
/>
|
||||
<ResourceId value={spriteId} />
|
||||
<DeleteResourceButton
|
||||
label="删除"
|
||||
disabled={deleteDisabled}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -305,6 +355,8 @@ function ImageInspector({
|
||||
onDescriptionChange,
|
||||
onRoleChange,
|
||||
onSlaveToChange,
|
||||
onDelete,
|
||||
deleteDisabled,
|
||||
}: {
|
||||
image: ActiveImage;
|
||||
imageId: ActiveImageId;
|
||||
@@ -313,6 +365,8 @@ function ImageInspector({
|
||||
onDescriptionChange: UiEditorPageController['setImageDescription'];
|
||||
onRoleChange: UiEditorPageController['setImageRole'];
|
||||
onSlaveToChange: UiEditorPageController['setImageSlaveTo'];
|
||||
onDelete: () => void;
|
||||
deleteDisabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
@@ -366,6 +420,11 @@ function ImageInspector({
|
||||
</InspectorSelect>
|
||||
) : null}
|
||||
<ResourceId value={imageId} />
|
||||
<DeleteResourceButton
|
||||
label="删除"
|
||||
disabled={deleteDisabled}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+95
-172
@@ -1,3 +1,18 @@
|
||||
import {
|
||||
type CanvasViewport,
|
||||
createPanDragState,
|
||||
type DragState,
|
||||
fitViewportToBounds,
|
||||
moveViewportFromPan,
|
||||
resolveViewportFromWheel,
|
||||
scaleViewportFromScreenPoint,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
CanvasViewport as SharedCanvasViewport,
|
||||
CanvasWorld,
|
||||
ZoomControls,
|
||||
} from '@genarrative/image-canvas-react';
|
||||
import { Image as ImageIcon, Minus, Plus, ScanSearch } from 'lucide-react';
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
@@ -6,108 +21,10 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Image as ImageIcon, Minus, Plus, ScanSearch } from 'lucide-react';
|
||||
import {
|
||||
createPanDragState,
|
||||
fitViewportToBounds,
|
||||
moveViewportFromPan,
|
||||
resolveViewportFromWheel,
|
||||
scaleViewportFromScreenPoint,
|
||||
type CanvasViewport,
|
||||
type DragState,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
CanvasViewport as SharedCanvasViewport,
|
||||
CanvasWorld,
|
||||
ZoomControls,
|
||||
} from '@genarrative/image-canvas-react';
|
||||
|
||||
import type { Node as UiNode } from '../../../features/ui-editor/types/Node';
|
||||
import { tf2css } from '../../../features/ui-editor/utils/transform/tf2css';
|
||||
import type { UiEditorPageController } from '../useUiEditorPage';
|
||||
|
||||
type NodeDragState = {
|
||||
nodeId: string;
|
||||
pointerId: number;
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
startTransform: UiNode['transform'];
|
||||
};
|
||||
|
||||
function PreviewNode({
|
||||
node,
|
||||
isRoot = false,
|
||||
selectedNodeId,
|
||||
onSelectNode,
|
||||
onNodePointerDown,
|
||||
onNodePointerMove,
|
||||
onNodePointerUp,
|
||||
}: {
|
||||
node: UiNode;
|
||||
isRoot?: boolean;
|
||||
selectedNodeId: string | null;
|
||||
onSelectNode: (id: string) => void;
|
||||
onNodePointerDown: (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
node: UiNode,
|
||||
) => void;
|
||||
onNodePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
}) {
|
||||
let geometry;
|
||||
try {
|
||||
geometry = tf2css(node.transform);
|
||||
} catch {
|
||||
// TODO: expose malformed node diagnostics without breaking the rest of the preview.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-node-id={node.id}
|
||||
className={`absolute min-h-0 min-w-0 ${isRoot ? 'cursor-default' : 'cursor-move'}`}
|
||||
style={{
|
||||
...geometry,
|
||||
outline:
|
||||
selectedNodeId === node.id
|
||||
? '2px solid rgb(249 115 22)'
|
||||
: '1px solid rgb(59 130 246 / 70%)',
|
||||
outlineOffset: selectedNodeId === node.id ? 0 : -1,
|
||||
backgroundColor:
|
||||
selectedNodeId === node.id
|
||||
? 'rgb(249 115 22 / 10%)'
|
||||
: 'rgb(59 130 246 / 4%)',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectNode(node.id);
|
||||
}}
|
||||
onPointerDown={(event) => onNodePointerDown(event, node)}
|
||||
onPointerMove={onNodePointerMove}
|
||||
onPointerUp={onNodePointerUp}
|
||||
onPointerCancel={onNodePointerUp}
|
||||
title={node.metadata.name || undefined}
|
||||
>
|
||||
{node.metadata.name ? (
|
||||
<span className="pointer-events-none absolute left-0 top-0 max-w-full truncate bg-blue-600/80 px-1 text-[9px] leading-4 text-white">
|
||||
{node.metadata.name}
|
||||
</span>
|
||||
) : null}
|
||||
{/* TODO: render supported Component variants once component preview is in scope. */}
|
||||
{node.children.map((child) => (
|
||||
<PreviewNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onSelectNode={onSelectNode}
|
||||
onNodePointerDown={onNodePointerDown}
|
||||
onNodePointerMove={onNodePointerMove}
|
||||
onNodePointerUp={onNodePointerUp}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import type { UiEditorPageController } from '../../useUiEditorPage';
|
||||
import { type UiEditorRenderMode, UiTreeRenderer } from './UiTreeRenderer';
|
||||
import { useNodeTransformInteraction } from './useNodeTransformInteraction';
|
||||
|
||||
export function PreviewWorkspace({
|
||||
controller,
|
||||
@@ -118,13 +35,14 @@ export function PreviewWorkspace({
|
||||
const viewportElementRef = useRef<HTMLDivElement>(null);
|
||||
const viewportRef = useRef<CanvasViewport>({ x: 0, y: 0, scale: 0.5 });
|
||||
const panRef = useRef<Extract<DragState, { kind: 'pan' }> | null>(null);
|
||||
const nodeDragRef = useRef<NodeDragState | null>(null);
|
||||
const [viewport, setViewportState] = useState<CanvasViewport>(
|
||||
viewportRef.current,
|
||||
);
|
||||
const [canvasSize, setCanvasSize] = useState({ width: 900, height: 640 });
|
||||
const [spaceHeld, setSpaceHeld] = useState(false);
|
||||
const tree = controller.treeForActiveImage;
|
||||
const [renderMode, setRenderMode] =
|
||||
useState<UiEditorRenderMode>('editor-overlay');
|
||||
const tree = controller.treeForActiveImage ?? null;
|
||||
const logicalSize = useMemo(() => {
|
||||
if (!activeImage) return null;
|
||||
const ppu = activeImage.pixels_per_unit;
|
||||
@@ -146,6 +64,23 @@ export function PreviewWorkspace({
|
||||
activeImage?.pixels_per_unit,
|
||||
]);
|
||||
|
||||
const nodeInteractions = useNodeTransformInteraction({
|
||||
activeImageId,
|
||||
controller,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
viewportRef,
|
||||
});
|
||||
const {
|
||||
onNodePointerDown,
|
||||
onNodePointerMove,
|
||||
onNodePointerUp,
|
||||
onNodeResizePointerDown,
|
||||
onNodeResizePointerMove,
|
||||
onNodeResizePointerUp,
|
||||
} = nodeInteractions;
|
||||
|
||||
const setViewport = useCallback((next: CanvasViewport) => {
|
||||
viewportRef.current = next;
|
||||
setViewportState(next);
|
||||
@@ -241,54 +176,6 @@ export function PreviewWorkspace({
|
||||
};
|
||||
}, [fitToCanvas, logicalSize, setViewport]);
|
||||
|
||||
const handleNodePointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>, node: UiNode) => {
|
||||
if (event.button !== 0 || spaceHeld || node.id === tree?.root.id) return;
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
controller.selectNode(node.id);
|
||||
nodeDragRef.current = {
|
||||
nodeId: node.id,
|
||||
pointerId: event.pointerId,
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startTransform: structuredClone(node.transform),
|
||||
};
|
||||
},
|
||||
[controller, spaceHeld, tree?.root.id],
|
||||
);
|
||||
|
||||
const handleNodePointerMove = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
const drag = nodeDragRef.current;
|
||||
if (!drag || drag.pointerId !== event.pointerId || !activeImageId) return;
|
||||
const logicalDeltaX =
|
||||
(event.clientX - drag.startClientX) / viewportRef.current.scale;
|
||||
const logicalDeltaY =
|
||||
(event.clientY - drag.startClientY) / viewportRef.current.scale;
|
||||
const nextTransform = structuredClone(drag.startTransform);
|
||||
nextTransform.offset_min[0] += logicalDeltaX;
|
||||
nextTransform.offset_min[1] += logicalDeltaY;
|
||||
nextTransform.offset_max[0] += logicalDeltaX;
|
||||
nextTransform.offset_max[1] += logicalDeltaY;
|
||||
controller.updateNodeTransform(activeImageId, drag.nodeId, nextTransform);
|
||||
},
|
||||
[activeImageId, controller],
|
||||
);
|
||||
|
||||
const handleNodePointerUp = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
if (nodeDragRef.current?.pointerId !== event.pointerId) return;
|
||||
nodeDragRef.current = null;
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 1 || (event.button === 0 && spaceHeld)) {
|
||||
event.preventDefault();
|
||||
@@ -346,12 +233,34 @@ export function PreviewWorkspace({
|
||||
<h1 className="m-0 text-sm font-semibold">
|
||||
{controller.activeToolLabel}
|
||||
</h1>
|
||||
<span className="text-[10px] text-(--platform-text-soft)">
|
||||
{Object.keys(controller.images).length} 张界面图 ·{' '}
|
||||
{Object.keys(controller.sprites).length} 项素材
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center rounded-lg border border-(--platform-subpanel-border) bg-white/70 p-0.5 text-[10px]">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-md px-2 py-1 ${renderMode === 'editor-overlay' ? 'bg-blue-600 text-white' : 'text-(--platform-text-soft)'}`}
|
||||
aria-pressed={renderMode === 'editor-overlay'}
|
||||
onClick={() => setRenderMode('editor-overlay')}
|
||||
>
|
||||
编辑叠加
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-md px-2 py-1 ${renderMode === 'final-preview' ? 'bg-orange-500 text-white' : 'text-(--platform-text-soft)'}`}
|
||||
aria-pressed={renderMode === 'final-preview'}
|
||||
onClick={() => setRenderMode('final-preview')}
|
||||
>
|
||||
最终预览
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px] text-(--platform-text-soft)">
|
||||
{Object.keys(controller.images).length} 张界面图 ·{' '}
|
||||
{Object.keys(controller.sprites).length} 项素材
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
{activeImage && previewUrls[activeImageId ?? ''] && logicalSize ? (
|
||||
{activeImage &&
|
||||
logicalSize &&
|
||||
(renderMode === 'final-preview' || previewUrls[activeImageId ?? '']) ? (
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<SharedCanvasViewport
|
||||
ref={viewportElementRef}
|
||||
@@ -375,23 +284,37 @@ export function PreviewWorkspace({
|
||||
className="relative"
|
||||
style={{ width: logicalSize.width, height: logicalSize.height }}
|
||||
>
|
||||
<img
|
||||
src={previewUrls[activeImageId ?? '']}
|
||||
alt={activeImage.metadata.name}
|
||||
className="absolute inset-0 size-full object-fill shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
|
||||
draggable={false}
|
||||
/>
|
||||
{tree?.root ? (
|
||||
<PreviewNode
|
||||
node={tree.root}
|
||||
isRoot
|
||||
selectedNodeId={controller.selectedNodeId}
|
||||
onSelectNode={controller.selectNode}
|
||||
onNodePointerDown={handleNodePointerDown}
|
||||
onNodePointerMove={handleNodePointerMove}
|
||||
onNodePointerUp={handleNodePointerUp}
|
||||
{renderMode === 'editor-overlay' ? (
|
||||
<img
|
||||
src={previewUrls[activeImageId ?? '']}
|
||||
alt={activeImage.metadata.name}
|
||||
className="absolute inset-0 size-full object-fill shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
|
||||
draggable={false}
|
||||
/>
|
||||
) : null}
|
||||
) : (
|
||||
<div
|
||||
className="absolute inset-0 bg-[#f7f4ef] shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
|
||||
aria-label="最终组件预览画布"
|
||||
/>
|
||||
)}
|
||||
<UiTreeRenderer
|
||||
tree={tree}
|
||||
renderMode={renderMode}
|
||||
selectedNodeId={controller.selectedNodeId}
|
||||
resources={{
|
||||
previewUrls,
|
||||
sprites: controller.sprites,
|
||||
fonts: editor.state.font_assets,
|
||||
}}
|
||||
onSelectNode={controller.selectNode}
|
||||
onNodePointerDown={onNodePointerDown}
|
||||
onNodePointerMove={onNodePointerMove}
|
||||
onNodePointerUp={onNodePointerUp}
|
||||
onNodeResizePointerDown={onNodeResizePointerDown}
|
||||
onNodeResizePointerMove={onNodeResizePointerMove}
|
||||
onNodeResizePointerUp={onNodeResizePointerUp}
|
||||
viewportScale={viewport.scale}
|
||||
/>
|
||||
</div>
|
||||
</CanvasWorld>
|
||||
</SharedCanvasViewport>
|
||||
Reference in New Issue
Block a user