feat(assets): 素材回收站与画布历史 - 合并 PR #370 并修复删除一致性
This commit is contained in:
@@ -2,7 +2,7 @@ import { useCallback, 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";
|
||||
@@ -44,6 +45,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() });
|
||||
@@ -382,6 +384,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"
|
||||
@@ -483,6 +489,7 @@ export default function CanvasPage() {
|
||||
onChange={setAssociationProjectId}
|
||||
/>
|
||||
</Modal>
|
||||
<CanvasHistoryDrawer open={historyOpen} onClose={() => setHistoryOpen(false)} />
|
||||
</WorkspacePage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,15 +157,12 @@ export function useCanvasRenderModel({
|
||||
const nodeSpatialIndexRef = useRef<{ source: CanvasNodeData[]; index: CanvasSpatialIndex<string> } | null>(null);
|
||||
const nodeSpatialIndex = useMemo(() => {
|
||||
const previous = nodeSpatialIndexRef.current;
|
||||
const geometryUnchanged = previous
|
||||
&& previous.source.length === nodes.length
|
||||
&& nodes.every((node, index) => {
|
||||
const geometryUnchanged =
|
||||
previous &&
|
||||
previous.source.length === nodes.length &&
|
||||
nodes.every((node, index) => {
|
||||
const old = previous.source[index];
|
||||
return old.id === node.id
|
||||
&& old.position.x === node.position.x
|
||||
&& old.position.y === node.position.y
|
||||
&& old.width === node.width
|
||||
&& old.height === node.height;
|
||||
return old.id === node.id && old.position.x === node.position.x && old.position.y === node.position.y && old.width === node.width && old.height === node.height;
|
||||
});
|
||||
if (geometryUnchanged) return previous.index;
|
||||
const index = buildCanvasSpatialIndex(nodes.map((node) => ({ id: node.id, bounds: canvasNodeBounds(node), value: node.id })));
|
||||
@@ -179,7 +176,8 @@ export function useCanvasRenderModel({
|
||||
const renderedNodeIds = renderedNodeIdsRef.current;
|
||||
const renderBudget = canvasNodeRenderBudget(viewport.k);
|
||||
const forcedNodeIds = new Set([...selectedNodeIds, ...(dragPreview?.nodeIds || [])].slice(0, renderBudget));
|
||||
const candidates = nodeSpatialIndex.query(renderBounds.retain, renderBudget + forcedNodeIds.size)
|
||||
const candidates = nodeSpatialIndex
|
||||
.query(renderBounds.retain, renderBudget + forcedNodeIds.size)
|
||||
.map((nodeId) => nodeById.get(nodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node));
|
||||
const candidateIds = new Set(candidates.map((node) => node.id));
|
||||
@@ -193,10 +191,7 @@ export function useCanvasRenderModel({
|
||||
[...prioritized, ...remaining].forEach((node) => {
|
||||
if (renderHiddenNodeIds.has(node.id)) return;
|
||||
const retained = forcedNodeIds.has(node.id) || renderedNodeIds.has(node.id);
|
||||
const insideEnterBounds = node.position.x + node.width > renderBounds.enter.left
|
||||
&& node.position.x < renderBounds.enter.right
|
||||
&& node.position.y + node.height > renderBounds.enter.top
|
||||
&& node.position.y < renderBounds.enter.bottom;
|
||||
const insideEnterBounds = node.position.x + node.width > renderBounds.enter.left && node.position.x < renderBounds.enter.right && node.position.y + node.height > renderBounds.enter.top && node.position.y < renderBounds.enter.bottom;
|
||||
if (!retained && !insideEnterBounds) return;
|
||||
(isFrameNode(node) ? frames : regular).push(node);
|
||||
});
|
||||
@@ -206,7 +201,7 @@ export function useCanvasRenderModel({
|
||||
renderedNodeIdsRef.current = new Set(visibleNodes.map((node) => node.id));
|
||||
}, [visibleNodes]);
|
||||
|
||||
const imageAssets = useMemo(() => assets.filter((asset): asset is ImageAsset => asset.kind === "image"), [assets]);
|
||||
const imageAssets = useMemo(() => assets.filter((asset): asset is ImageAsset => asset.kind === "image" && asset.status !== "archived"), [assets]);
|
||||
const semanticNodesRef = useRef(nodes);
|
||||
const semanticNodes = useMemo(() => {
|
||||
const previous = semanticNodesRef.current;
|
||||
@@ -239,9 +234,7 @@ export function useCanvasRenderModel({
|
||||
|
||||
const selectedNodeBounds = useMemo(() => {
|
||||
if (selectedNodeIds.size < 2) return null;
|
||||
const selectedNodes = [...selectedNodeIds]
|
||||
.map((nodeId) => nodeById.get(nodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node && !renderHiddenNodeIds.has(node.id)));
|
||||
const selectedNodes = [...selectedNodeIds].map((nodeId) => nodeById.get(nodeId)).filter((node): node is CanvasNodeData => Boolean(node && !renderHiddenNodeIds.has(node.id)));
|
||||
if (selectedNodes.length < 2) return null;
|
||||
const left = Math.min(...selectedNodes.map((node) => node.position.x));
|
||||
const top = Math.min(...selectedNodes.map((node) => node.position.y));
|
||||
@@ -249,14 +242,18 @@ 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 };
|
||||
}, [nodeById, renderHiddenNodeIds, selectedNodeIds]);
|
||||
const selectedVideoNodes = useMemo(() => [...selectedNodeIds]
|
||||
.map((nodeId) => nodeById.get(nodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node && node.type === CanvasNodeType.Video && 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;
|
||||
}), [nodeById, renderHiddenNodeIds, selectedNodeIds]);
|
||||
const selectedVideoNodes = useMemo(
|
||||
() =>
|
||||
[...selectedNodeIds]
|
||||
.map((nodeId) => nodeById.get(nodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node && node.type === CanvasNodeType.Video && 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;
|
||||
}),
|
||||
[nodeById, renderHiddenNodeIds, selectedNodeIds],
|
||||
);
|
||||
const relatedHighlight = useMemo(() => {
|
||||
const nodeIds = new Set<string>();
|
||||
const connectionIds = new Set<string>();
|
||||
@@ -344,7 +341,10 @@ export function useCanvasRenderModel({
|
||||
}
|
||||
return targetNodes;
|
||||
}, [activeNodeId, dialogNodeId, nodeById, visibleNodes]);
|
||||
const canvasResourceReferences = useMemo(() => buildCanvasResourceReferences(semanticNodes, connections, dialogNodeId || activeNodeId, resourceReferenceTargetNodes), [activeNodeId, connections, dialogNodeId, resourceReferenceTargetNodes, semanticNodes]);
|
||||
const canvasResourceReferences = useMemo(
|
||||
() => buildCanvasResourceReferences(semanticNodes, connections, dialogNodeId || activeNodeId, resourceReferenceTargetNodes),
|
||||
[activeNodeId, connections, dialogNodeId, resourceReferenceTargetNodes, semanticNodes],
|
||||
);
|
||||
const resourceReferenceByNodeId = useMemo(() => new Map(canvasResourceReferences.map((reference) => [reference.nodeId, reference])), [canvasResourceReferences]);
|
||||
const skillMentionReferences = useMemo(() => buildSkillMentionReferences(addedSkills), [addedSkills]);
|
||||
const mentionReferencesByNodeId = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user