feat: 完成短剧项目空间与结构化资产创作闭环 (#38)

* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP

* feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环

Refs #30

* fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置

* feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环

Refs #30

---------

Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
ddcat666
2026-07-27 10:45:11 +08:00
committed by GitHub
parent 431b9e3294
commit d2fbc413b3
208 changed files with 22655 additions and 4234 deletions

View File

@@ -4,12 +4,14 @@ import { nanoid } from "nanoid";
import type { PendingConnectionCreate } from "@/components/canvas/canvas-workspace-overlays";
import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor, isHiddenBatchChild, normalizeConnection, storyboardHandleAtY, storyboardRowFromHandle } from "@/lib/canvas/canvas-project-domain";
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
import { getGenerationCount } from "@/lib/canvas/canvas-project-generation";
import { useEffectiveConfig } from "@/stores/use-config-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
type UseCanvasConnectionControllerOptions = {
projectId: string;
nodesRef: { current: CanvasNodeData[] };
connectionsRef: { current: CanvasConnection[] };
viewportRef: { current: ViewportTransform };
@@ -21,6 +23,7 @@ type UseCanvasConnectionControllerOptions = {
setSelectedConnectionId: Dispatch<SetStateAction<string | null>>;
setContextMenu: Dispatch<SetStateAction<ContextMenuState | null>>;
setDialogNodeId: Dispatch<SetStateAction<string | null>>;
setDrawingNodeId: Dispatch<SetStateAction<string | null>>;
};
type ConnectionDropTarget = {
@@ -34,6 +37,7 @@ const CONNECTION_NODE_HIT_PADDING = 32;
const NODE_STATUS_IDLE = "idle" as const;
export function useCanvasConnectionController({
projectId,
nodesRef,
connectionsRef,
viewportRef,
@@ -45,6 +49,7 @@ export function useCanvasConnectionController({
setSelectedConnectionId,
setContextMenu,
setDialogNodeId,
setDrawingNodeId,
}: UseCanvasConnectionControllerOptions) {
const { message } = App.useApp();
const effectiveConfig = useEffectiveConfig();
@@ -98,7 +103,7 @@ export function useCanvasConnectionController({
setContextMenu(null);
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
const createConnectedNode = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio, pending: PendingConnectionCreate) => {
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
const storyboardRow = type === CanvasNodeType.Video ? storyboardRowFromHandle(nodesRef.current, pending.connection.nodeId, pending.connection.handleId) : undefined;
const videoPrompt = storyboardRow ? (storyboardRow.videoMotionPrompt || storyboardRow.plotDescription).trim() : "";
const metadata = type === CanvasNodeType.Config
@@ -113,6 +118,35 @@ export function useCanvasConnectionController({
message.warning("配置节点之间不能连接");
return;
}
if (type === CanvasNodeType.Drawing) {
const sourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
const sourceUrl = sourceNode?.type === CanvasNodeType.Image ? sourceNode.metadata?.content : "";
if (pending.connection.handleType !== "source" || !sourceNode || !sourceUrl || !newNode.metadata?.drawingId) {
message.error("只有已有图片内容的输出连线可以创建绘图");
return;
}
closeConnectionCreateMenu();
setConnecting(null);
try {
const saved = await createCanvasDrawingFromImage(projectId, newNode.metadata.drawingId, {
url: sourceUrl,
storageKey: sourceNode.metadata?.storageKey,
name: sourceNode.title || "来源图片",
mimeType: sourceNode.metadata?.mimeType,
});
newNode.title = `${sourceNode.title || "图片"} · 绘图`;
newNode.metadata = {
...newNode.metadata,
drawingRevision: saved.revision,
drawingUpdatedAt: saved.updatedAt,
drawingShapeCount: saved.shapeCount,
drawingPageCount: saved.pageCount,
};
} catch (error) {
message.error(error instanceof Error ? `创建绘图失败:${error.message}` : "创建绘图失败");
return;
}
}
const fromHandleId = connection.fromNodeId === pending.connection.nodeId ? pending.connection.handleId : undefined;
const toHandleId = connection.toNodeId === pending.connection.nodeId ? pending.connection.handleId : undefined;
const connected = { ...connection, fromHandleId, toHandleId };
@@ -120,10 +154,11 @@ export function useCanvasConnectionController({
setConnections((currentConnections) => [...currentConnections, { id: nanoid(), ...connected }]);
setSelectedNodeIds(new Set([newNode.id]));
setSelectedConnectionId(null);
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
if (type === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
closeConnectionCreateMenu();
setConnecting(null);
}, [closeConnectionCreateMenu, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, nodesRef, setConnecting, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
}, [closeConnectionCreateMenu, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
const world = screenToCanvas(clientX, clientY);