feat(assets&canvas): 实现素材回收站机制、自动定时清理策略与过期时间提示,增加画布创建与变更历史时间线
This commit is contained in:
1 parent
dca252434b
commit
867a18a948
17 files changed
+1411
-321
No files matched your search
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { App, Button, Dropdown, Modal, Select } from "antd";
|
||||
import { ArrowDownAZ, Clock3, Download, FileUp, ListFilter, MoreHorizontal, Plus, Search, SlidersHorizontal, Trash2, X } from "lucide-react";
|
||||
import { ArrowDownAZ, Clock3, Download, FileUp, History, ListFilter, MoreHorizontal, Plus, Search, SlidersHorizontal, Trash2, X } from "lucide-react";
|
||||
|
||||
import { CollectionGrid, PageHeader, WorkspacePage } from "@/components/layout/workspace-page";
|
||||
import { WorkspaceLoadingState, WorkspaceState } from "@/components/layout/workspace-state";
|
||||
@@ -12,6 +12,7 @@ import { setMediaBlob } from "@/services/file-storage";
|
||||
import { setImageBlob } from "@/services/image-storage";
|
||||
import { CanvasCreateCard } from "@/components/canvas/canvas-project-card";
|
||||
import { CanvasFolderCard } from "@/components/canvas/canvas-folder-card";
|
||||
import { CanvasHistoryDrawer } from "@/components/canvas/canvas-history-drawer";
|
||||
import type { CanvasExportFile } from "@/types/canvas-export";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
import { flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
@@ -42,6 +43,7 @@ export default function CanvasPage() {
|
||||
const selectedIds = useCanvasUiStore((state) => state.selectedProjectIds);
|
||||
const setDeleteIds = useCanvasUiStore((state) => state.setDeleteProjectIds);
|
||||
const updateProject = useCanvasStore((state) => state.updateProject);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [associationOpen, setAssociationOpen] = useState(false);
|
||||
const [associationProjectId, setAssociationProjectId] = useState("");
|
||||
const projectQuery = useQuery({ queryKey: ["projects"], queryFn: () => listProjects() });
|
||||
@@ -172,9 +174,9 @@ export default function CanvasPage() {
|
||||
const oldKey = node.metadata?.storageKey;
|
||||
const mapped = oldKey ? storageKeyMap.get(oldKey) : undefined;
|
||||
const isDeadBlob = (val?: string) => typeof val === "string" && val.startsWith("blob:");
|
||||
const nextStorageKey = mapped ? mapped.storageKey : (oldKey && !isDeadBlob(oldKey) ? oldKey : undefined);
|
||||
const content = mapped ? mapped.url : (isDeadBlob(node.metadata?.content) ? "" : node.metadata?.content);
|
||||
const previewContent = mapped ? mapped.url : (isDeadBlob(node.metadata?.previewContent) ? "" : node.metadata?.previewContent);
|
||||
const nextStorageKey = mapped ? mapped.storageKey : oldKey && !isDeadBlob(oldKey) ? oldKey : undefined;
|
||||
const content = mapped ? mapped.url : isDeadBlob(node.metadata?.content) ? "" : node.metadata?.content;
|
||||
const previewContent = mapped ? mapped.url : isDeadBlob(node.metadata?.previewContent) ? "" : node.metadata?.previewContent;
|
||||
return {
|
||||
...node,
|
||||
metadata: {
|
||||
@@ -357,6 +359,10 @@ export default function CanvasPage() {
|
||||
<span>{sortLabel}</span>
|
||||
</button>
|
||||
</Dropdown>
|
||||
<button type="button" className="canvas-library-filter flex items-center gap-1.5" onClick={() => setHistoryOpen(true)} aria-label="查看画布创建与变更历史时间线" title="查看画布创建与变更历史时间线">
|
||||
<History className="size-3.5 text-[var(--workspace-accent)]" />
|
||||
<span>历史</span>
|
||||
</button>
|
||||
{keyword || projectFilter !== "all" || sort !== "updated" ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -419,20 +425,17 @@ export default function CanvasPage() {
|
||||
<CollectionGrid className="canvas-library-grid">
|
||||
{showCreateCard ? <CanvasCreateCard disabled={!hydrated} onClick={createAndEnter} /> : null}
|
||||
{visibleProjects.map((project) => (
|
||||
<CanvasFolderCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
projectName={project.projectId ? projectNames.get(project.projectId) || "未同步项目" : undefined}
|
||||
onClick={() => enterProject(project.id)}
|
||||
/>
|
||||
<CanvasFolderCard key={project.id} project={project} projectName={project.projectId ? projectNames.get(project.projectId) || "未同步项目" : undefined} onClick={() => enterProject(project.id)} />
|
||||
))}
|
||||
</CollectionGrid>
|
||||
) : (
|
||||
<WorkspaceState icon="canvas" title="没有匹配的画布" description="换一个画布名称或重置筛选条件。" />
|
||||
)}
|
||||
{hydrated && visibleProjects.length ? <div ref={loadMoreRef} className="library-load-more" aria-live="polite">
|
||||
{visibleProjects.length < filteredProjects.length ? `继续下滑加载更多(每页 50 条)` : `已加载全部 ${filteredProjects.length} 个画布`}
|
||||
</div> : null}
|
||||
{hydrated && visibleProjects.length ? (
|
||||
<div ref={loadMoreRef} className="library-load-more" aria-live="polite">
|
||||
{visibleProjects.length < filteredProjects.length ? `继续下滑加载更多(每页 50 条)` : `已加载全部 ${filteredProjects.length} 个画布`}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<input ref={inputRef} type="file" accept="application/zip,.zip" className="hidden" onChange={(event) => void importCanvas(event.target.files?.[0])} />
|
||||
@@ -454,6 +457,7 @@ export default function CanvasPage() {
|
||||
onChange={setAssociationProjectId}
|
||||
/>
|
||||
</Modal>
|
||||
<CanvasHistoryDrawer open={historyOpen} onClose={() => setHistoryOpen(false)} />
|
||||
</WorkspacePage>
|
||||
);
|
||||
}
|
||||
@@ -122,8 +122,11 @@ export function useCanvasRenderModel({
|
||||
return [...frames, ...regular];
|
||||
}, [dragPreview, nodes, renderBounds, renderHiddenNodeIds, selectedNodeIds]);
|
||||
|
||||
const imageAssets = useMemo(() => assets.filter((asset): asset is ImageAsset => asset.kind === "image"), [assets]);
|
||||
const canvasImageNodes = useMemo(() => nodes.filter((node) => node.type === CanvasNodeType.Image && Boolean(node.metadata?.content) && !collapsedBatchChildIds.has(node.id) && !(node.parentId && nodeById.get(node.parentId)?.metadata?.frame?.collapsed)), [collapsedBatchChildIds, nodeById, nodes]);
|
||||
const imageAssets = useMemo(() => assets.filter((asset): asset is ImageAsset => asset.kind === "image" && asset.status !== "archived"), [assets]);
|
||||
const canvasImageNodes = useMemo(
|
||||
() => nodes.filter((node) => node.type === CanvasNodeType.Image && Boolean(node.metadata?.content) && !collapsedBatchChildIds.has(node.id) && !(node.parentId && nodeById.get(node.parentId)?.metadata?.frame?.collapsed)),
|
||||
[collapsedBatchChildIds, nodeById, nodes],
|
||||
);
|
||||
const semanticNodesRef = useRef(nodes);
|
||||
const semanticNodes = useMemo(() => {
|
||||
const previous = semanticNodesRef.current;
|
||||
@@ -164,13 +167,17 @@ export function useCanvasRenderModel({
|
||||
const bottom = Math.max(...selectedNodes.map((node) => node.position.y + node.height));
|
||||
return { left, top, width: right - left, height: bottom - top, count: selectedNodes.length };
|
||||
}, [nodes, renderHiddenNodeIds, selectedNodeIds]);
|
||||
const selectedVideoNodes = useMemo(() => nodes
|
||||
.filter((node) => selectedNodeIds.has(node.id) && node.type === CanvasNodeType.Video && Boolean(node.metadata?.content) && !renderHiddenNodeIds.has(node.id))
|
||||
.sort((a, b) => {
|
||||
const shotA = a.metadata?.shotIndex ?? Number.MAX_SAFE_INTEGER;
|
||||
const shotB = b.metadata?.shotIndex ?? Number.MAX_SAFE_INTEGER;
|
||||
return shotA - shotB || a.position.y - b.position.y || a.position.x - b.position.x;
|
||||
}), [nodes, renderHiddenNodeIds, selectedNodeIds]);
|
||||
const selectedVideoNodes = useMemo(
|
||||
() =>
|
||||
nodes
|
||||
.filter((node) => selectedNodeIds.has(node.id) && node.type === CanvasNodeType.Video && Boolean(node.metadata?.content) && !renderHiddenNodeIds.has(node.id))
|
||||
.sort((a, b) => {
|
||||
const shotA = a.metadata?.shotIndex ?? Number.MAX_SAFE_INTEGER;
|
||||
const shotB = b.metadata?.shotIndex ?? Number.MAX_SAFE_INTEGER;
|
||||
return shotA - shotB || a.position.y - b.position.y || a.position.x - b.position.x;
|
||||
}),
|
||||
[nodes, renderHiddenNodeIds, selectedNodeIds],
|
||||
);
|
||||
const batchChildCountById = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
nodes.forEach((node) => {
|
||||
@@ -216,25 +223,29 @@ export function useCanvasRenderModel({
|
||||
});
|
||||
return { nodeIds, connectionIds };
|
||||
}, [activeNodeId, connections]);
|
||||
const displayConnections = useMemo(() => connections.flatMap((connection) => {
|
||||
if (collapsedBatchChildIds.has(connection.fromNodeId) || collapsedBatchChildIds.has(connection.toNodeId)) return [];
|
||||
const fromNode = nodeById.get(connection.fromNodeId);
|
||||
const toNode = nodeById.get(connection.toNodeId);
|
||||
if (!fromNode || !toNode) return [];
|
||||
const fromParent = fromNode.parentId ? nodeById.get(fromNode.parentId) : null;
|
||||
const toParent = toNode.parentId ? nodeById.get(toNode.parentId) : null;
|
||||
const displayFrom = fromParent && isFrameNode(fromParent) && fromParent.metadata?.frame?.collapsed ? fromParent : fromNode;
|
||||
const displayTo = toParent && isFrameNode(toParent) && toParent.metadata?.frame?.collapsed ? toParent : toNode;
|
||||
if (displayFrom.id === displayTo.id) return [];
|
||||
const from = dragPreview?.nodeIds.has(displayFrom.id) ? { ...displayFrom, position: { x: displayFrom.position.x + dragPreview.x, y: displayFrom.position.y + dragPreview.y } } : displayFrom;
|
||||
const to = dragPreview?.nodeIds.has(displayTo.id) ? { ...displayTo, position: { x: displayTo.position.x + dragPreview.x, y: displayTo.position.y + dragPreview.y } } : displayTo;
|
||||
const connectionLeft = Math.min(from.position.x, to.position.x);
|
||||
const connectionTop = Math.min(from.position.y, to.position.y);
|
||||
const connectionRight = Math.max(from.position.x + from.width, to.position.x + to.width);
|
||||
const connectionBottom = Math.max(from.position.y + from.height, to.position.y + to.height);
|
||||
if (connectionRight <= renderBounds.left || connectionLeft >= renderBounds.right || connectionBottom <= renderBounds.top || connectionTop >= renderBounds.bottom) return [];
|
||||
return [{ connection, from, to }];
|
||||
}), [collapsedBatchChildIds, connections, dragPreview, nodeById, renderBounds]);
|
||||
const displayConnections = useMemo(
|
||||
() =>
|
||||
connections.flatMap((connection) => {
|
||||
if (collapsedBatchChildIds.has(connection.fromNodeId) || collapsedBatchChildIds.has(connection.toNodeId)) return [];
|
||||
const fromNode = nodeById.get(connection.fromNodeId);
|
||||
const toNode = nodeById.get(connection.toNodeId);
|
||||
if (!fromNode || !toNode) return [];
|
||||
const fromParent = fromNode.parentId ? nodeById.get(fromNode.parentId) : null;
|
||||
const toParent = toNode.parentId ? nodeById.get(toNode.parentId) : null;
|
||||
const displayFrom = fromParent && isFrameNode(fromParent) && fromParent.metadata?.frame?.collapsed ? fromParent : fromNode;
|
||||
const displayTo = toParent && isFrameNode(toParent) && toParent.metadata?.frame?.collapsed ? toParent : toNode;
|
||||
if (displayFrom.id === displayTo.id) return [];
|
||||
const from = dragPreview?.nodeIds.has(displayFrom.id) ? { ...displayFrom, position: { x: displayFrom.position.x + dragPreview.x, y: displayFrom.position.y + dragPreview.y } } : displayFrom;
|
||||
const to = dragPreview?.nodeIds.has(displayTo.id) ? { ...displayTo, position: { x: displayTo.position.x + dragPreview.x, y: displayTo.position.y + dragPreview.y } } : displayTo;
|
||||
const connectionLeft = Math.min(from.position.x, to.position.x);
|
||||
const connectionTop = Math.min(from.position.y, to.position.y);
|
||||
const connectionRight = Math.max(from.position.x + from.width, to.position.x + to.width);
|
||||
const connectionBottom = Math.max(from.position.y + from.height, to.position.y + to.height);
|
||||
if (connectionRight <= renderBounds.left || connectionLeft >= renderBounds.right || connectionBottom <= renderBounds.top || connectionTop >= renderBounds.bottom) return [];
|
||||
return [{ connection, from, to }];
|
||||
}),
|
||||
[collapsedBatchChildIds, connections, dragPreview, nodeById, renderBounds],
|
||||
);
|
||||
|
||||
const configInputsById = useMemo(() => {
|
||||
const map = new Map<string, NodeGenerationInput[]>();
|
||||
|
||||
Reference in new issue
Block a user