Improve canvas import and pane interactions

This commit is contained in:
Codex
2026-05-11 19:23:44 +08:00
parent 6db64d5161
commit 723d9832d5
3 changed files with 39 additions and 15 deletions

View File

@@ -56,6 +56,8 @@
:root {
--background: oklch(0.985 0.01 85);
--foreground: oklch(0.18 0.02 60);
--canvas-dot-color: rgb(57 48 35 / 0.18);
--canvas-dot-accent-color: rgb(196 126 30 / 0.26);
--card: oklch(0.995 0.005 85);
--card-foreground: oklch(0.18 0.02 60);
--popover: oklch(0.995 0.005 85);
@@ -91,6 +93,8 @@
.dark {
--background: oklch(0.16 0.015 260);
--foreground: oklch(0.98 0.01 80);
--canvas-dot-color: rgb(255 255 255 / 0.34);
--canvas-dot-accent-color: rgb(255 255 255 / 0.2);
--card: oklch(0.21 0.025 260);
--card-foreground: oklch(0.98 0.01 80);
--popover: oklch(0.25 0.025 260);

View File

@@ -1064,6 +1064,25 @@ export function InfiniteCanvasWorkspace() {
void importProjectDirectory(files);
}, [importProjectDirectory]);
const setImportDirectoryInputRef = useCallback((element: HTMLInputElement | null) => {
const directoryInput = element as FileInputWithDirectory | null;
importDirectoryInputRef.current = directoryInput;
if (!directoryInput) return;
directoryInput.webkitdirectory = true;
directoryInput.directory = true;
}, []);
const openImportDirectoryPicker = useCallback(() => {
if (!project || importingProject) return;
const input = importDirectoryInputRef.current;
if (!input) {
toast.error('目录导入控件尚未准备好');
return;
}
toast.info('正在打开目录选择器');
input.click();
}, [importingProject, project]);
useEffect(() => {
if (!dirty || !project || !accessToken) return;
const timer = window.setTimeout(() => {
@@ -2038,10 +2057,7 @@ export function InfiniteCanvasWorkspace() {
setSelectedNodeIds([]);
setAddMenu(null);
setContextMenu(null);
if (event.detail !== 1) return;
const menuPosition = getCanvasMenuPosition(event.clientX, event.clientY);
setAddMenu({ x: point.x, y: point.y, ...menuPosition });
}, [getCanvasMenuPosition]);
}, []);
const handleCanvasContextMenu = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (!project) return;
@@ -2057,9 +2073,14 @@ export function InfiniteCanvasWorkspace() {
}
}, [getCanvasMenuPosition, getNodeIdFromEvent, project, screenToCanvasPoint, selectedNodeIds]);
const handlePaneDoubleClick = useCallback((point: { x: number; y: number }) => {
addNode('text2img', point.x, point.y);
}, [addNode]);
const handlePaneDoubleClick = useCallback((point: { x: number; y: number }, event: MouseEvent | React.MouseEvent) => {
setConnectingFromId(null);
setSelectedNodeId(null);
setSelectedNodeIds([]);
setContextMenu(null);
const menuPosition = getCanvasMenuPosition(event.clientX, event.clientY);
setAddMenu({ x: point.x, y: point.y, ...menuPosition });
}, [getCanvasMenuPosition]);
const handleCanvasDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (!Array.from(event.dataTransfer?.items || []).some(item => item.kind === 'file' && item.type.startsWith('image/'))) return;
@@ -2254,12 +2275,11 @@ export function InfiniteCanvasWorkspace() {
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileChange} />
<input ref={importInputRef} type="file" accept="application/json,.json" className="hidden" onChange={handleImportFileChange} />
<input
ref={importDirectoryInputRef}
ref={setImportDirectoryInputRef}
type="file"
className="hidden"
multiple
onChange={handleImportDirectoryChange}
{...({ webkitdirectory: '', directory: '' } as Record<string, string>)}
/>
<aside className="hidden w-14 shrink-0 flex-col items-center gap-2 border-r bg-background/95 px-2 py-3 text-foreground shadow-sm lg:flex">
@@ -2648,7 +2668,7 @@ export function InfiniteCanvasWorkspace() {
<Button variant="outline" size="sm" className="gap-2" onClick={() => importInputRef.current?.click()} disabled={!project || importingProject} title="导入 JSON">
{importingProject ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />} JSON
</Button>
<Button variant="outline" size="sm" className="gap-2" onClick={() => importDirectoryInputRef.current?.click()} disabled={!project || importingProject} title="导入工作流目录">
<Button variant="outline" size="sm" className="gap-2" onClick={openImportDirectoryPicker} disabled={!project || importingProject} title="导入工作流目录">
{importingProject ? <Loader2 className="h-4 w-4 animate-spin" /> : <FolderOpen className="h-4 w-4" />}
</Button>
<Button variant="outline" size="icon" className="text-destructive" onClick={() => void deleteCurrentProject()} disabled={!project} title="删除画布">
@@ -2704,7 +2724,7 @@ export function InfiniteCanvasWorkspace() {
</div>
) : null}
<div className="pointer-events-none absolute left-6 top-6 z-20 rounded-lg border bg-background/70 px-3 py-2 text-xs text-muted-foreground shadow-sm backdrop-blur">
· · Ctrl+Z · Delete
· · Ctrl+Z · Delete
</div>
{!project ? (
<div className="absolute inset-0 z-30 flex items-center justify-center bg-background/40 backdrop-blur-[1px]">

View File

@@ -80,7 +80,7 @@ type ReactFlowCanvasProps = {
onViewportCommit: (viewport: CanvasViewport) => void;
onReady?: (controls: CanvasFlowControls | null) => void;
onPaneClick: (point: { x: number; y: number }, event: MouseEvent | React.MouseEvent) => void;
onPaneDoubleClick: (point: { x: number; y: number }) => void;
onPaneDoubleClick: (point: { x: number; y: number }, event: MouseEvent | React.MouseEvent) => void;
};
export type CanvasFlowControls = {
@@ -905,7 +905,7 @@ function FlowCanvasInner({
window.clearTimeout(paneClickTimerRef.current);
paneClickTimerRef.current = null;
}
onPaneDoubleClick(reactFlow.screenToFlowPosition({ x: event.clientX, y: event.clientY }, { snapToGrid: true, snapGrid: SNAP_GRID }));
onPaneDoubleClick(reactFlow.screenToFlowPosition({ x: event.clientX, y: event.clientY }, { snapToGrid: true, snapGrid: SNAP_GRID }), event);
return;
}
if (paneClickTimerRef.current !== null) {
@@ -970,8 +970,8 @@ function FlowCanvasInner({
onSelectionChange={handleSelectionChange}
onInit={handleInit}
>
<Background gap={20} size={1} color="hsl(var(--foreground) / 0.16)" />
<Background gap={100} size={1.5} color="hsl(var(--primary) / 0.28)" />
<Background gap={20} size={1} color="var(--canvas-dot-color)" />
<Background gap={100} size={1.5} color="var(--canvas-dot-accent-color)" />
<Controls showInteractive={false} position="bottom-left" />
<MiniMap
position="bottom-right"