Merge branch 'fork/chen3737/main'

This commit is contained in:
HouYunFei
2026-07-27 16:31:22 +08:00
+77 -31
View File
@@ -1847,13 +1847,31 @@ function InfiniteCanvasPage() {
const handleImageInputChange = useCallback( const handleImageInputChange = useCallback(
async (event: ReactChangeEvent<HTMLInputElement>) => { async (event: ReactChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const files = Array.from(event.target.files || []).filter(
const target = uploadTargetRef.current; (f) => f.type.startsWith("image/") || f.type.startsWith("video/") || isAudioFile(f),
if (!file || (!file.type.startsWith("image/") && !file.type.startsWith("video/") && !isAudioFile(file))) return; );
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 (target?.nodeId) {
if (isAudioFile(file)) { const [first, ...rest] = files;
const audio = await uploadMediaFile(file, "audio");
// 第一个文件:替换目标节点
if (isAudioFile(first)) {
const audio = await uploadMediaFile(first, "audio");
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio]; const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
setNodes((prev) => setNodes((prev) =>
prev.map((node) => prev.map((node) =>
@@ -1861,7 +1879,7 @@ function InfiniteCanvasPage() {
? { ? {
...node, ...node,
type: CanvasNodeType.Audio, 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 }, position: { x: node.position.x + node.width / 2 - spec.width / 2, y: node.position.y + node.height / 2 - spec.height / 2 },
width: spec.width, width: spec.width,
height: spec.height, height: spec.height,
@@ -1872,12 +1890,8 @@ function InfiniteCanvasPage() {
); );
setSelectedNodeIds(new Set([target.nodeId])); setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null); setSelectedConnectionId(null);
uploadTargetRef.current = null; } else if (first.type.startsWith("video/")) {
event.target.value = ""; const video = await uploadMediaFile(first, "video");
return;
}
if (file.type.startsWith("video/")) {
const video = await uploadMediaFile(file, "video");
const nextSize = fitNodeSize(video.width || 1280, video.height || 720, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT); const nextSize = fitNodeSize(video.width || 1280, video.height || 720, VIDEO_NODE_MAX_WIDTH, VIDEO_NODE_MAX_HEIGHT);
setNodes((prev) => setNodes((prev) =>
prev.map((node) => prev.map((node) =>
@@ -1885,7 +1899,7 @@ function InfiniteCanvasPage() {
? { ? {
...node, ...node,
type: CanvasNodeType.Video, 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 }, position: { x: node.position.x + node.width / 2 - nextSize.width / 2, y: node.position.y + node.height / 2 - nextSize.height / 2 },
width: nextSize.width, width: nextSize.width,
height: nextSize.height, height: nextSize.height,
@@ -1896,22 +1910,18 @@ function InfiniteCanvasPage() {
); );
setSelectedNodeIds(new Set([target.nodeId])); setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null); setSelectedConnectionId(null);
setDialogNodeId(target.nodeId); } else {
uploadTargetRef.current = null; const image = await uploadImage(first);
event.target.value = ""; const s = fitNodeSize(image.width, image.height);
return;
}
const image = await uploadImage(file);
const size = fitNodeSize(image.width, image.height);
setNodes((prev) => setNodes((prev) =>
prev.map((node) => prev.map((node) =>
node.id === target.nodeId node.id === target.nodeId
? { ? {
...node, ...node,
type: CanvasNodeType.Image, type: CanvasNodeType.Image,
title: file.name, title: first.name,
width: size.width, width: s.width,
height: size.height, height: s.height,
metadata: { metadata: {
...node.metadata, ...node.metadata,
...imageMetadata(image), ...imageMetadata(image),
@@ -1936,10 +1946,33 @@ function InfiniteCanvasPage() {
); );
setSelectedNodeIds(new Set([target.nodeId])); setSelectedNodeIds(new Set([target.nodeId]));
setSelectedConnectionId(null); setSelectedConnectionId(null);
setDialogNodeId(target.nodeId); }
// 剩余文件:在目标节点附近新建
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 { } else {
const position = target?.position || screenToCanvas((containerRef.current?.getBoundingClientRect().left || 0) + size.width / 2, (containerRef.current?.getBoundingClientRect().top || 0) + size.height / 2); void createImageFileNode(f, offsetPos);
void (isAudioFile(file) ? createAudioFileNode(file, position) : file.type.startsWith("video/") ? createVideoFileNode(file, position) : createImageFileNode(file, position)); }
}
} else {
// 无替换目标:所有文件在画布中心附近新建
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; uploadTargetRef.current = null;
@@ -1951,11 +1984,24 @@ function InfiniteCanvasPage() {
const handleDrop = useCallback( const handleDrop = useCallback(
(event: ReactDragEvent<HTMLDivElement>) => { (event: ReactDragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
const file = Array.from(event.dataTransfer.files).find((item) => item.type.startsWith("image/") || item.type.startsWith("video/") || isAudioFile(item)); const files = Array.from(event.dataTransfer.files).filter(
if (!file) return; (item) => item.type.startsWith("image/") || item.type.startsWith("video/") || isAudioFile(item),
);
if (!files.length) return;
const pos = screenToCanvas(event.clientX, event.clientY); const basePos = screenToCanvas(event.clientX, event.clientY);
void (isAudioFile(file) ? createAudioFileNode(file, pos) : file.type.startsWith("video/") ? createVideoFileNode(file, pos) : createImageFileNode(file, pos)); 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], [createAudioFileNode, createImageFileNode, createVideoFileNode, screenToCanvas],
); );
@@ -2928,7 +2974,7 @@ function InfiniteCanvasPage() {
/> />
) : null} ) : 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)} /> <CanvasNodeInfoModal node={infoNode} open={Boolean(infoNode)} onClose={() => setInfoNodeId(null)} />
<CanvasPluginManagerModal open={pluginManagerOpen} onClose={() => setPluginManagerOpen(false)} /> <CanvasPluginManagerModal open={pluginManagerOpen} onClose={() => setPluginManagerOpen(false)} />