mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
feat(canvas): 画布工具栏上传和拖拽支持多选文件
This commit is contained in:
@@ -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,50 +1910,69 @@ 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;
|
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 {
|
} 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;
|
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)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user