feat(assets): 素材库与画布 - 新增可交互通用文件夹
This commit is contained in:
1 parent
0e251c3c8f
commit
5d775071e4
35 files changed
+2010
-155
No files matched your search
@@ -17,6 +17,7 @@ import { refreshCanvasCharacterReferenceNodes } from "@/lib/canvas/canvas-charac
|
||||
import { shouldAutoConnectCanvasRuntime } from "@/lib/canvas/local-runtime-connection";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { flushCanvasStorePersistence } from "@/stores/canvas/use-canvas-store";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { App } from "antd";
|
||||
@@ -108,6 +109,7 @@ import {
|
||||
CanvasNodeType,
|
||||
type CanvasAssistantSession,
|
||||
type CanvasConnection,
|
||||
type CanvasFolderStyle,
|
||||
type CanvasNodeData,
|
||||
type CanvasMediaPerformanceMode,
|
||||
type StoryboardColumn,
|
||||
@@ -200,6 +202,7 @@ function InfiniteCanvasPage() {
|
||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
||||
const [projectAssetOpen, setProjectAssetOpen] = useState(false);
|
||||
const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all");
|
||||
const [projectAssetInitialFolderId, setProjectAssetInitialFolderId] = useState("all");
|
||||
const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState<Position | undefined>();
|
||||
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
||||
const [subtitleNodeId, setSubtitleNodeId] = useState<string | null>(null);
|
||||
@@ -334,6 +337,23 @@ function InfiniteCanvasPage() {
|
||||
const linkedProjectId = shortDramaEnabled ? currentProject?.projectId || "" : "";
|
||||
const linkedProjectQuery = useQuery({ queryKey: ["project", linkedProjectId], queryFn: () => getProject(linkedProjectId), enabled: Boolean(linkedProjectId) });
|
||||
const refetchLinkedProject = linkedProjectQuery.refetch;
|
||||
const archiveNodesToLinkedFolder = useCallback((folder: CanvasNodeData, droppedNodes: CanvasNodeData[]) => {
|
||||
const folderId = folder.metadata?.folder?.assetFolderId;
|
||||
const domainProjectId = folder.metadata?.folder?.projectId || linkedProjectId;
|
||||
if (!folderId || !domainProjectId || !droppedNodes.length) return;
|
||||
void Promise.all(droppedNodes.map((node) => ensureCanvasNodeAsset({ canvasId: projectId, domainProjectId, folderId, node, source: "canvas-manual" })))
|
||||
.then((results) => {
|
||||
const archivedByNodeId = new Map(droppedNodes.map((node, index) => [node.id, { assetId: results[index].assetId, content: node.metadata?.content, previousAssetId: node.metadata?.assetId }]));
|
||||
setNodes((current) => current.map((node) => {
|
||||
const archived = archivedByNodeId.get(node.id);
|
||||
if (!archived || node.metadata?.content !== archived.content || node.metadata?.assetId !== archived.previousAssetId) return node;
|
||||
return { ...node, metadata: { ...node.metadata, assetId: archived.assetId } };
|
||||
}));
|
||||
void refetchLinkedProject();
|
||||
message.success(`已归档到“${folder.title}”`);
|
||||
})
|
||||
.catch((error) => message.error(error instanceof Error ? error.message : "素材归档失败"));
|
||||
}, [linkedProjectId, message, projectId, refetchLinkedProject, setNodes]);
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || !linkedProjectQuery.data) return;
|
||||
setNodes((current) => refreshCanvasCharacterReferenceNodes(current, linkedProjectQuery.data.assets));
|
||||
@@ -621,9 +641,10 @@ function InfiniteCanvasPage() {
|
||||
);
|
||||
|
||||
const openProjectAssets = useCallback(
|
||||
(initialCategory = "all", position?: Position, scope: "canvas" | "timeline" = "canvas") => {
|
||||
(initialCategory = "all", position?: Position, scope: "canvas" | "timeline" = "canvas", initialFolderId = "all") => {
|
||||
setProjectAssetScope(scope);
|
||||
setProjectAssetInitialCategory(initialCategory);
|
||||
setProjectAssetInitialFolderId(initialFolderId);
|
||||
setProjectAssetInsertPosition(position);
|
||||
setProjectAssetOpen(true);
|
||||
// 资产与项目实时同步:打开弹窗前刷新关联短剧项目资产,避免缓存导致资产列表空白/过期。
|
||||
@@ -647,6 +668,7 @@ function InfiniteCanvasPage() {
|
||||
const closeProjectAssets = useCallback(() => {
|
||||
setProjectAssetOpen(false);
|
||||
setProjectAssetInsertPosition(undefined);
|
||||
setProjectAssetInitialFolderId("all");
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -749,6 +771,7 @@ function InfiniteCanvasPage() {
|
||||
arrangeSelectedNodes,
|
||||
copyNodesToClipboard,
|
||||
copySelectedNodes,
|
||||
createFolder,
|
||||
createNode,
|
||||
createReferenceGroup,
|
||||
createStoryboardGroup,
|
||||
@@ -863,6 +886,7 @@ function InfiniteCanvasPage() {
|
||||
onNodeInteractionStart: handleNodeInteractionStart,
|
||||
onNodeClick: handleSelectedNodeClick,
|
||||
onBatchConnectionTarget: handleBatchConnectionTargetClick,
|
||||
onLinkedFolderDrop: archiveNodesToLinkedFolder,
|
||||
onDeselect: handleCanvasDeselect,
|
||||
onSelectionBoxEnd: () => setCanvasTool((tool) => (tool === "box-select" ? "move" : tool)),
|
||||
});
|
||||
@@ -891,6 +915,7 @@ function InfiniteCanvasPage() {
|
||||
collapsingBatchIds,
|
||||
downloadNodeImage,
|
||||
handleConfigNodeChange,
|
||||
handleFolderStyleChange,
|
||||
handleFontSizeChange,
|
||||
handleNodeContentChange,
|
||||
handleNodePromptChange,
|
||||
@@ -914,6 +939,60 @@ function InfiniteCanvasPage() {
|
||||
setHoveredNodeId,
|
||||
});
|
||||
|
||||
const handleProjectFolderInsert = useCallback((folderId: string) => {
|
||||
const folder = linkedProjectQuery.data?.assetFolders.find((item) => item.id === folderId);
|
||||
if (!folder || !linkedProjectId) throw new Error("素材文件夹已不存在,请刷新后重试");
|
||||
const style: CanvasFolderStyle = folder.style === "stacked" || folder.style === "midnight" || folder.style === "paper" || folder.style === "cinema" || folder.style === "compact" ? folder.style : "glass";
|
||||
createFolder(projectAssetInsertPosition, { id: folder.id, projectId: linkedProjectId, title: folder.name, style, createdAt: folder.createdAt });
|
||||
}, [createFolder, linkedProjectId, linkedProjectQuery.data?.assetFolders, projectAssetInsertPosition]);
|
||||
|
||||
const handleFrameToggle = useCallback((nodeId: string) => {
|
||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||
const linkedFolderId = node?.metadata?.folder?.assetFolderId;
|
||||
if (linkedFolderId) {
|
||||
openProjectAssets("all", node ? { x: node.position.x + node.width + 40, y: node.position.y } : undefined, "canvas", linkedFolderId);
|
||||
return;
|
||||
}
|
||||
toggleFrameCollapsed(nodeId);
|
||||
}, [nodesRef, openProjectAssets, toggleFrameCollapsed]);
|
||||
|
||||
const linkedFolderPreviewNodesById = useMemo(() => {
|
||||
const result = new Map<string, CanvasNodeData[]>();
|
||||
const localById = new Map(assets.map((asset) => [asset.id, asset]));
|
||||
for (const asset of linkedProjectQuery.data?.assets || []) {
|
||||
if (!asset.folderId) continue;
|
||||
const local = localById.get(asset.id);
|
||||
const characterCover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0];
|
||||
const type = asset.category === "character" || asset.mediaType === "image" ? CanvasNodeType.Image : asset.mediaType === "video" ? CanvasNodeType.Video : asset.mediaType === "audio" ? CanvasNodeType.Audio : CanvasNodeType.Text;
|
||||
const remoteResourceId = resourceIdFromStorageKey(asset.storageKey);
|
||||
const content = characterCover ? resourceFileUrl(characterCover.resourceId) : local?.kind === "image" ? local.data.dataUrl || local.coverUrl : local?.kind === "video" || local?.kind === "audio" ? local.data.url : local?.kind === "text" ? local.data.content : remoteResourceId ? resourceFileUrl(remoteResourceId) : asset.previewText || "";
|
||||
const preview: CanvasNodeData = { id: asset.id, type, title: asset.title, position: { x: 0, y: 0 }, width: 240, height: 160, metadata: { assetId: asset.id, content } };
|
||||
const current = result.get(asset.folderId) || [];
|
||||
current.push(preview);
|
||||
result.set(asset.folderId, current);
|
||||
}
|
||||
return result;
|
||||
}, [assets, linkedProjectQuery.data?.assets]);
|
||||
|
||||
useEffect(() => {
|
||||
const folders = linkedProjectQuery.data?.assetFolders;
|
||||
if (!folders?.length) return;
|
||||
const byId = new Map(folders.map((folder) => [folder.id, folder]));
|
||||
setNodes((current) => {
|
||||
let changed = false;
|
||||
const next = current.map((node) => {
|
||||
const folderId = node.metadata?.folder?.assetFolderId;
|
||||
const folder = folderId ? byId.get(folderId) : undefined;
|
||||
if (!folder) return node;
|
||||
const style: CanvasFolderStyle = folder.style === "stacked" || folder.style === "midnight" || folder.style === "paper" || folder.style === "cinema" || folder.style === "compact" ? folder.style : "glass";
|
||||
if (node.title === folder.name && node.metadata?.folder?.style === style) return node;
|
||||
changed = true;
|
||||
return { ...node, title: folder.name, metadata: { ...node.metadata, folder: { ...node.metadata!.folder!, style } } };
|
||||
});
|
||||
return changed ? next : current;
|
||||
});
|
||||
}, [linkedProjectQuery.data?.assetFolders, setNodes]);
|
||||
|
||||
const {
|
||||
activeDirectorScene,
|
||||
activeNodeId,
|
||||
@@ -1741,6 +1820,7 @@ function InfiniteCanvasPage() {
|
||||
nodeById={nodeById}
|
||||
visibleNodes={visibleNodes}
|
||||
frameChildrenById={frameChildrenById}
|
||||
linkedFolderPreviewNodesById={linkedFolderPreviewNodesById}
|
||||
dragPreview={dragPreview}
|
||||
selectedNodeIds={selectedNodeIds}
|
||||
frameDropTargetId={frameDropTargetId}
|
||||
@@ -1778,7 +1858,8 @@ function InfiniteCanvasPage() {
|
||||
onNodeHoverEnd={handleCanvasNodeHoverEnd}
|
||||
onConnectStart={handleConnectStart}
|
||||
onNodeResize={handleNodeResize}
|
||||
onToggleFrame={toggleFrameCollapsed}
|
||||
onToggleFrame={handleFrameToggle}
|
||||
onFolderStyleChange={handleFolderStyleChange}
|
||||
onNodeTitleChange={handleNodeTitleChange}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onNodeContentChange={handleNodeContentChange}
|
||||
@@ -1835,6 +1916,7 @@ function InfiniteCanvasPage() {
|
||||
onChooseStyle={() => setStylePickerOpen(true)}
|
||||
onAddScript={() => createNode(CanvasNodeType.Script)}
|
||||
onAddFrame={() => createNode(CanvasNodeType.Frame)}
|
||||
onAddFolder={createFolder}
|
||||
onAddDrawing={() => createNode(CanvasNodeType.Drawing)}
|
||||
onOpenDirector={() => createDirectorShot()}
|
||||
onUndo={undoCanvas}
|
||||
@@ -2045,6 +2127,7 @@ function InfiniteCanvasPage() {
|
||||
screenToCanvas={screenToCanvas}
|
||||
onClose={() => setContextMenu(null)}
|
||||
onAddNode={(type, position) => createNode(type, position)}
|
||||
onAddFolder={createFolder}
|
||||
onChooseStyle={() => setStylePickerOpen(true)}
|
||||
onOpenDirector={createDirectorShot}
|
||||
onUpload={(nodeId, position) => handleUploadRequest(nodeId, position)}
|
||||
@@ -2071,7 +2154,7 @@ function InfiniteCanvasPage() {
|
||||
void copyNodeMediaUrlToClipboard(node);
|
||||
}}
|
||||
onSetAssetCategory={(nodeId, assetCategory) => handleConfigNodeChange(nodeId, { assetCategory })}
|
||||
onToggleFrame={(node) => toggleFrameCollapsed(node.id)}
|
||||
onToggleFrame={(node) => handleFrameToggle(node.id)}
|
||||
/>
|
||||
|
||||
<CanvasUploadModal open={uploadModalOpen} onClose={closeUploadModal} onUpload={handleUploadFiles} />
|
||||
@@ -2276,7 +2359,7 @@ function InfiniteCanvasPage() {
|
||||
/>
|
||||
|
||||
<AssetPickerModal open={assetPickerOpen} onInsert={handleTimelineAssetInsert} onClose={closeAssetPicker} />
|
||||
<CanvasProjectAssetModal open={projectAssetOpen} detail={linkedProjectQuery.data} initialCategory={projectAssetInitialCategory} onClose={closeProjectAssets} onInsert={handleTimelineProjectAssetsInsert} />
|
||||
<CanvasProjectAssetModal open={projectAssetOpen} detail={linkedProjectQuery.data} initialCategory={projectAssetInitialCategory} initialFolderId={projectAssetInitialFolderId} onClose={closeProjectAssets} onInsert={handleTimelineProjectAssetsInsert} onInsertFolder={projectAssetScope === "canvas" ? handleProjectFolderInsert : undefined} />
|
||||
{codexCompactAgent && !assistantMounted ? (
|
||||
<CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={canUndoAgentOps} undoOpsCount={agentUndoCount} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} />
|
||||
) : null}
|
||||
|
||||
Reference in new issue
Block a user