feat: refine generation workspace experience

This commit is contained in:
2026-06-08 13:44:03 +08:00
35 changed files with 5249 additions and 350 deletions
+125 -4
View File
@@ -398,6 +398,8 @@ function CanvasPage({
const canvasRef = useRef<HTMLElement>(null);
const videoGenerationInFlightRef = useRef(new Set<string>());
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
const canvasDragCounterRef = useRef(0);
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
const suppressNextPaneClickRef = useRef(false);
const canvasAutoSaveTimerRef = useRef<number | null>(null);
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
@@ -1335,7 +1337,7 @@ function CanvasPage({
model: defaultVideoModel,
aspectRatio: "16:9",
resolution: getDefaultVideoQuality(defaultVideoModel),
duration: "4",
duration: "5",
videoMode: "text2video",
sourceTextNodeId: source.id,
position: {
@@ -1359,7 +1361,7 @@ function CanvasPage({
model: defaultVideoModel,
aspectRatio: "16:9",
resolution: getDefaultVideoQuality(defaultVideoModel),
duration: "4",
duration: "5",
videoMode: "text2video",
sourceTextNodeId: "",
position,
@@ -1415,7 +1417,7 @@ function CanvasPage({
imageUrl = "",
fileName = "本地图片",
position = { x: 0, y: 0 },
options?: { title?: string; sourceImageNodeId?: string }
options?: { title?: string; sourceImageNodeId?: string; sourceTextNodeId?: string }
) => {
const nodeNumber = imageNodeIdRef.current;
imageNodeIdRef.current += 1;
@@ -1429,6 +1431,7 @@ function CanvasPage({
imageSize: getDefaultImageQuality(fallbackVisibleImageModel),
fileName,
sourceImageNodeId: options?.sourceImageNodeId,
sourceTextNodeId: options?.sourceTextNodeId,
position,
size: createCanvasNodeSize("image"),
};
@@ -2034,6 +2037,120 @@ function CanvasPage({
setNodeMenu(null);
};
// ── Canvas drag-and-drop file upload ──────────────────────────────
const handleCanvasDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
canvasDragCounterRef.current += 1;
if (canvasDragCounterRef.current === 1) {
setIsCanvasDragging(true);
}
}, []);
const handleCanvasDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
canvasDragCounterRef.current -= 1;
if (canvasDragCounterRef.current <= 0) {
canvasDragCounterRef.current = 0;
setIsCanvasDragging(false);
}
}, []);
const handleCanvasDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleCanvasDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
canvasDragCounterRef.current = 0;
setIsCanvasDragging(false);
const files = Array.from(e.dataTransfer.files).filter(
(f) => f.type.startsWith("image/")
);
if (files.length === 0) return;
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
const dropPosition = {
x: (e.clientX - rect.left - canvasViewport.x) / canvasViewport.zoom,
y: (e.clientY - rect.top - canvasViewport.y) / canvasViewport.zoom,
};
let offsetX = 0;
let offsetY = 0;
for (const file of files) {
const imageUrl = URL.createObjectURL(file);
addImageNode(imageUrl, file.name, {
x: dropPosition.x + offsetX,
y: dropPosition.y + offsetY,
});
offsetX += 60;
offsetY += 60;
}
setContextMenu(null);
setNodeMenu(null);
},
[canvasViewport.x, canvasViewport.y, canvasViewport.zoom, addImageNode],
);
// ── Text composer drag-and-drop ──────────────────────────────────
const [textComposerDragNodeId, setTextComposerDragNodeId] = useState<string | null>(null);
const textComposerDragCounterRef = useRef(0);
const handleTextComposerDragEnter = useCallback((_e: React.DragEvent, nodeId: string) => {
_e.preventDefault();
_e.stopPropagation();
textComposerDragCounterRef.current += 1;
if (textComposerDragCounterRef.current === 1) {
setTextComposerDragNodeId(nodeId);
}
}, []);
const handleTextComposerDragLeave = useCallback((_e: React.DragEvent) => {
_e.preventDefault();
_e.stopPropagation();
textComposerDragCounterRef.current -= 1;
if (textComposerDragCounterRef.current <= 0) {
textComposerDragCounterRef.current = 0;
setTextComposerDragNodeId(null);
}
}, []);
const handleTextComposerDragOver = useCallback((_e: React.DragEvent) => {
_e.preventDefault();
_e.stopPropagation();
}, []);
const handleTextComposerDrop = useCallback(
(e: React.DragEvent, sourceNode: CanvasTextNode) => {
e.preventDefault();
e.stopPropagation();
textComposerDragCounterRef.current = 0;
setTextComposerDragNodeId(null);
const files = Array.from(e.dataTransfer.files).filter(
(f) => f.type.startsWith("image/")
);
if (files.length === 0) return;
let offsetX = 0;
let offsetY = 0;
for (const file of files) {
const imageUrl = URL.createObjectURL(file);
addImageNode(imageUrl, file.name, {
x: sourceNode.position.x + sourceNode.size.width + 40 + offsetX,
y: sourceNode.position.y + offsetY,
}, { sourceTextNodeId: sourceNode.id });
offsetX += 60;
offsetY += 60;
}
},
[addImageNode],
);
const activeTextNode = textNodeMenu
? textNodes.find((node) => node.id === textNodeMenu.nodeId) ?? null
: null;
@@ -3632,7 +3749,7 @@ function CanvasPage({
<WorkspacePageShell title="画布" fullWidth className="canvas-page page-motion">
<div className={`studio-tool-layout studio-tool-layout--no-top studio-tool-layout--no-left studio-tool-layout--no-right studio-tool-layout--canvas${(shouldShowEmptyProjectState || isWaitingForProjects) ? " studio-tool-layout--canvas-empty" : ""}`}>
<section
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${isCanvasDragging ? " is-canvas-dragging" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
ref={canvasRef}
onAuxClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasAuxClick}
onContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? (event) => event.preventDefault() : handleCanvasContextMenu}
@@ -3640,6 +3757,10 @@ function CanvasPage({
onDoubleClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDoubleClick}
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
onDragEnter={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragEnter}
onDragOver={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragOver}
onDragLeave={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragLeave}
onDrop={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDrop}
style={{
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
-1
View File
@@ -140,7 +140,6 @@ export const videoRatioOptions: CanvasOption[] = [
];
export const videoDurationOptions: CanvasOption[] = [
{ value: "4", label: "4s" },
{ value: "5", label: "5s" },
{ value: "6", label: "6s" },
{ value: "7", label: "7s" },