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:
ddcat666andddCat authored and GitHub committed 2026-07-27 10:45:11 +08:00
1 parent 431b9e3294
commit d2fbc413b3
208 files changed
+22655 -4234

No files matched your search

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, KeyboardEvent, MouseEvent, PointerEvent } from "react";
import { Button, Image } from "antd";
import { FileText, Image as ImageIcon, Music2, Sparkles, Video, X } from "lucide-react";
import { FileText, Image as ImageIcon, Music2, Pencil, Sparkles, Video, X } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -306,6 +306,13 @@ function ResourcePreview({ candidate }: { candidate: ComposerCandidate }) {
);
}
const input = candidate.input;
if (input.sourceKind === "drawing") {
return (
<span className="grid size-9 shrink-0 place-items-center rounded-md bg-black/10">
<Pencil className="size-4" />
</span>
);
}
if (input.type === "image" && input.image) return <img src={input.image.dataUrl} alt="" className="size-9 rounded-md object-cover" />;
if (input.type === "video" && input.video) return <video src={input.video.url} className="size-9 rounded-md bg-black object-cover" muted preload="metadata" />;
const Icon = input.type === "audio" ? Music2 : input.type === "video" ? Video : input.type === "image" ? ImageIcon : FileText;
@@ -327,7 +334,7 @@ function createReferenceChip(input: NodeGenerationInput, inputs: NodeGenerationI
wrapper.dataset.referenceNodeId = input.nodeId;
wrapper.className = "mx-px inline-flex h-7 max-w-40 items-center justify-center overflow-hidden rounded-md border px-1 text-xs leading-none align-middle";
Object.assign(wrapper.style, chipStyle(theme));
if (input.type === "image" && input.image) {
if (input.type === "image" && input.image && input.sourceKind !== "drawing") {
const image = document.createElement("img");
image.src = input.image.dataUrl;
image.alt = input.title;
@@ -340,10 +347,10 @@ function createReferenceChip(input: NodeGenerationInput, inputs: NodeGenerationI
onImagePreview(input.image?.dataUrl || "");
});
} else {
wrapper.title = input.text || input.title;
wrapper.title = input.sourceKind === "drawing" ? resourceLabel(input, inputs) : input.text || input.title;
const text = document.createElement("span");
text.className = "block truncate";
text.textContent = input.type === "text" ? input.text || input.title : input.title;
text.textContent = input.sourceKind === "drawing" ? resourceLabel(input, inputs) : input.type === "text" ? input.text || input.title : input.title;
wrapper.appendChild(text);
}
return wrapper;
@@ -449,8 +456,9 @@ function parseComposerTokens(value: string): Token[] {
}
function resourceLabel(input: NodeGenerationInput, inputs: NodeGenerationInput[]) {
const sameTypeInputs = inputs.filter((item) => item.type === input.type);
const sameTypeInputs = inputs.filter((item) => item.type === input.type && item.sourceKind === input.sourceKind);
const index = Math.max(0, sameTypeInputs.findIndex((item) => item.nodeId === input.nodeId));
if (input.sourceKind === "drawing") return `绘图${index + 1}`;
if (input.type === "image") return `图片${index + 1}`;
if (input.type === "video") return `视频${index + 1}`;
if (input.type === "audio") return `音频${index + 1}`;