Merge branch 'fork/chen3737/main'

This commit is contained in:
HouYunFei
2026-07-27 16:31:22 +08:00
+106 -60
View File
@@ -1847,13 +1847,31 @@ function InfiniteCanvasPage() {
const handleImageInputChange = useCallback(
async (event: ReactChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
const target = uploadTargetRef.current;
if (!file || (!file.type.startsWith("image/") && !file.type.startsWith("video/") && !isAudioFile(file))) return;
const files = Array.from(event.target.files || []).filter(
(f) => f.type.startsWith("image/") || f.type.startsWith("video/") || isAudioFile(f),
);
if (!files.length) {
uploadTargetRef.current = null;
event.target.value = "";
return;
}
const target = uploadTargetRef.current;
const basePosition =
target?.position ||
screenToCanvas(
(containerRef.current?.getBoundingClientRect().left || 0) + size.width / 2,
(containerRef.current?.getBoundingClientRect().top || 0) + size.height / 2,
);
const STAGGER = 40; // 多文件时的偏移间距
// 如果有替换目标节点,第一个文件替换它,其余在附近新建
if (target?.nodeId) {
if (isAudioFile(file)) {
const audio = await uploadMediaFile(file, "audio");
const [first, ...rest] = files;
// 第一个文件:替换目标节点
if (isAudioFile(first)) {
const audio = await uploadMediaFile(first, "audio");
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
setNodes((prev) =>
prev.map((node) =>
@@ -1861,7 +1879,7 @@ function InfiniteCanvasPage() {
? {
...node,
type: CanvasNodeType.Audio,
title: file.name,
title: first.name,
position: { x: node.position.x + node.width / 2 - spec.width / 2, y: node.position.y + node.height / 2 - spec.height / 2 },
width: spec.width,
height: spec.height,
@@ -1872,12 +1890,8 @@ function InfiniteCanvasPage() {
);
setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null);
uploadTargetRef.current = null;
event.target.value = "";
return;
}
if (file.type.startsWith("video/")) {
const video = await uploadMediaFile(file, "video");
} else if (first.type.startsWith("video/")) {
const video = await uploadMediaFile(first, "video");
const nextSize = fitNodeSize(video.width || 1280, video.height || 720, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
setNodes((prev) =>
prev.map((node) =>
@@ -1885,7 +1899,7 @@ function InfiniteCanvasPage() {
? {
...node,
type: CanvasNodeType.Video,
title: file.name,
title: first.name,
position: { x: node.position.x + node.width / 2 - nextSize.width / 2, y: node.position.y + node.height / 2 - nextSize.height / 2 },
width: nextSize.width,
height: nextSize.height,
@@ -1896,50 +1910,69 @@ function InfiniteCanvasPage() {
);
setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null);
setDialogNodeId(target.nodeId);
uploadTargetRef.current = null;
event.target.value = "";
return;
} else {
const image = await uploadImage(first);
const s = fitNodeSize(image.width, image.height);
setNodes((prev) =>
prev.map((node) =>
node.id === target.nodeId
? {
...node,
type: CanvasNodeType.Image,
title: first.name,
width: s.width,
height: s.height,
metadata: {
...node.metadata,
...imageMetadata(image),
errorDetails: undefined,
freeResize: false,
isBatchRoot: undefined,
batchRootId: undefined,
batchChildIds: undefined,
batchUsesReferenceImages: undefined,
generationType: undefined,
model: undefined,
size: undefined,
quality: undefined,
count: undefined,
references: undefined,
primaryImageId: undefined,
imageBatchExpanded: undefined,
},
}
: node,
),
);
setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null);
}
// 剩余文件:在目标节点附近新建
for (let i = 0; i < rest.length; i++) {
const offsetPos = { x: basePosition.x + (i + 1) * STAGGER, y: basePosition.y + (i + 1) * STAGGER };
const f = rest[i];
if (isAudioFile(f)) {
void createAudioFileNode(f, offsetPos);
} else if (f.type.startsWith("video/")) {
void createVideoFileNode(f, offsetPos);
} else {
void createImageFileNode(f, offsetPos);
}
}
const image = await uploadImage(file);
const size = fitNodeSize(image.width, image.height);
setNodes((prev) =>
prev.map((node) =>
node.id === target.nodeId
? {
...node,
type: CanvasNodeType.Image,
title: file.name,
width: size.width,
height: size.height,
metadata: {
...node.metadata,
...imageMetadata(image),
errorDetails: undefined,
freeResize: false,
isBatchRoot: undefined,
batchRootId: undefined,
batchChildIds: undefined,
batchUsesReferenceImages: undefined,
generationType: undefined,
model: undefined,
size: undefined,
quality: undefined,
count: undefined,
references: undefined,
primaryImageId: undefined,
imageBatchExpanded: undefined,
},
}
: node,
),
);
setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null);
setDialogNodeId(target.nodeId);
} else {
const position = target?.position || screenToCanvas((containerRef.current?.getBoundingClientRect().left || 0) + size.width / 2, (containerRef.current?.getBoundingClientRect().top || 0) + size.height / 2);
void (isAudioFile(file) ? createAudioFileNode(file, position) : file.type.startsWith("video/") ? createVideoFileNode(file, position) : createImageFileNode(file, position));
// 无替换目标:所有文件在画布中心附近新建
for (let i = 0; i < files.length; i++) {
const offsetPos = { x: basePosition.x + i * STAGGER, y: basePosition.y + i * STAGGER };
const f = files[i];
if (isAudioFile(f)) {
void createAudioFileNode(f, offsetPos);
} else if (f.type.startsWith("video/")) {
void createVideoFileNode(f, offsetPos);
} else {
void createImageFileNode(f, offsetPos);
}
}
}
uploadTargetRef.current = null;
@@ -1951,11 +1984,24 @@ function InfiniteCanvasPage() {
const handleDrop = useCallback(
(event: ReactDragEvent<HTMLDivElement>) => {
event.preventDefault();
const file = Array.from(event.dataTransfer.files).find((item) => item.type.startsWith("image/") || item.type.startsWith("video/") || isAudioFile(item));
if (!file) return;
const files = Array.from(event.dataTransfer.files).filter(
(item) => item.type.startsWith("image/") || item.type.startsWith("video/") || isAudioFile(item),
);
if (!files.length) return;
const pos = screenToCanvas(event.clientX, event.clientY);
void (isAudioFile(file) ? createAudioFileNode(file, pos) : file.type.startsWith("video/") ? createVideoFileNode(file, pos) : createImageFileNode(file, pos));
const basePos = screenToCanvas(event.clientX, event.clientY);
const STAGGER = 40;
for (let i = 0; i < files.length; i++) {
const pos = { x: basePos.x + i * STAGGER, y: basePos.y + i * STAGGER };
const f = files[i];
if (isAudioFile(f)) {
void createAudioFileNode(f, pos);
} else if (f.type.startsWith("video/")) {
void createVideoFileNode(f, pos);
} else {
void createImageFileNode(f, pos);
}
}
},
[createAudioFileNode, createImageFileNode, createVideoFileNode, screenToCanvas],
);
@@ -2928,7 +2974,7 @@ function InfiniteCanvasPage() {
/>
) : null}
<input ref={imageInputRef} type="file" accept="image/*,video/*,audio/mpeg,audio/wav,audio/x-wav,.mp3,.wav" className="hidden" onChange={handleImageInputChange} />
<input ref={imageInputRef} type="file" multiple accept="image/*,video/*,audio/mpeg,audio/wav,audio/x-wav,.mp3,.wav" className="hidden" onChange={handleImageInputChange} />
<CanvasNodeInfoModal node={infoNode} open={Boolean(infoNode)} onClose={() => setInfoNodeId(null)} />
<CanvasPluginManagerModal open={pluginManagerOpen} onClose={() => setPluginManagerOpen(false)} />