import { Check, Clapperboard, Download, FileText, Frame, Image as ImageIcon, MoreHorizontal, Music2, Pencil, Plus, Settings2, Sparkles, Trash2, Video, X } from "lucide-react"; import type { ReactNode } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { Dropdown, Input } from "antd"; import { useCanvasStore, type CanvasProject } from "@/stores/canvas/use-canvas-store"; import { useCanvasUiStore } from "@/stores/canvas/use-canvas-ui-store"; import { exportCanvasProjects } from "@/lib/canvas/canvas-export"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources"; import { resolveBackendApiUrl } from "@/stores/use-config-store"; import { CachedResourceImage } from "@/components/cached-resource-image"; import { cn } from "@/lib/utils"; export function CanvasCreateCard({ disabled, onClick }: { disabled?: boolean; onClick: () => void }) { return ; } export function CanvasProjectCard({ project, projectName, variant = "library", readOnly = false, footer }: { project: CanvasProject; projectName?: string; variant?: "library" | "recent"; readOnly?: boolean; footer?: ReactNode }) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const renameProject = useCanvasStore((state) => state.renameProject); const selectedIds = useCanvasUiStore((state) => state.selectedProjectIds); const editingId = useCanvasUiStore((state) => state.editingProjectId); const editingTitle = useCanvasUiStore((state) => state.editingProjectTitle); const startEditing = useCanvasUiStore((state) => state.startEditingProject); const setEditingTitle = useCanvasUiStore((state) => state.setEditingProjectTitle); const stopEditing = useCanvasUiStore((state) => state.stopEditingProject); const toggleSelected = useCanvasUiStore((state) => state.toggleSelectedProjectId); const setDeleteIds = useCanvasUiStore((state) => state.setDeleteProjectIds); const editing = editingId === project.id; const selected = selectedIds.includes(project.id); const open = () => navigate(`/canvas/${project.id}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`); const saveTitle = () => { renameProject(project.id, editingTitle); stopEditing(); }; const compact = variant === "recent"; return (
(!editing || readOnly) && open()}>
{!compact && !readOnly ? event.stopPropagation()}> toggleSelected(project.id, event.target.checked)} className="app-canvas-project-checkbox" aria-label={`选择 ${project.title}`} /> : null}
{editing && !readOnly ? ( event.stopPropagation()} onChange={(event) => setEditingTitle(event.target.value)} onKeyDown={(event) => event.key === "Enter" && saveTitle()} autoFocus /> ) : ( )} {editing && !readOnly ? (
event.stopPropagation()}>
) : !readOnly ? (
event.stopPropagation()}> domEvent.stopPropagation(), items: [ { key: "export", icon: , label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "影策画布") }, { type: "divider" }, { key: "delete", danger: true, icon: , label: "删除", onClick: () => setDeleteIds([project.id]) }, ], }} >
) : null}
{projectName || "自由画布"}
{footer ?
event.stopPropagation()}>{footer}
: null}
); } export function ProjectPreview({ project, preferLatestImage = false }: { project: CanvasProject; preferLatestImage?: boolean }) { const mediaNodes = project.nodes .flatMap((node) => { if (node.type !== CanvasNodeType.Image && node.type !== CanvasNodeType.Video) return []; const url = getNodeMediaUrl(node); return isPreviewUrl(url) ? [{ node, url, storageKey: node.metadata?.storageKey }] : []; }); const imageNodes = mediaNodes.filter(({ node }) => node.type === CanvasNodeType.Image); const media = preferLatestImage ? imageNodes[imageNodes.length - 1] || mediaNodes[mediaNodes.length - 1] : imageNodes[0] || mediaNodes[0]; if (media) { const { node, url, storageKey } = media; return (
{node.type === CanvasNodeType.Video ?
: }
); } const nodes = project.nodes.slice(0, 8); if (!nodes.length) return
空白画布等待第一幕
; const previewNodes = buildNodePreviewLayout(nodes); return (
{previewNodes.map(({ node, style }) => { const presentation = getNodePresentation(node); return ( {presentation.icon} {node.title || presentation.label} ); })}
); } function getNodeMediaUrl(node: CanvasNodeData) { const resourceId = resourceIdFromStorageKey(node.metadata?.storageKey); if (resourceId) return resourceFileUrl(resourceId); return resolveBackendApiUrl(node.metadata?.previewContent || node.metadata?.content || ""); } function buildNodePreviewLayout(nodes: CanvasNodeData[]) { const minX = Math.min(...nodes.map((node) => node.position.x)); const minY = Math.min(...nodes.map((node) => node.position.y)); const maxX = Math.max(...nodes.map((node) => node.position.x + node.width)); const maxY = Math.max(...nodes.map((node) => node.position.y + node.height)); const spanX = Math.max(maxX - minX, 1); const spanY = Math.max(maxY - minY, 1); return nodes.map((node) => { const left = 6 + ((node.position.x - minX) / spanX) * 70; const top = 8 + ((node.position.y - minY) / spanY) * 66; const width = Math.min(92 - left, Math.max(16, Math.min(38, (node.width / spanX) * 78))); const height = Math.min(94 - top, Math.max(13, Math.min(24, (node.height / spanY) * 72))); return { node, style: { left: `${left}%`, top: `${top}%`, width: `${width}%`, height: `${height}%` } }; }); } function getNodePresentation(node: CanvasNodeData) { switch (node.type) { case CanvasNodeType.Text: return { label: "文本", icon: }; case CanvasNodeType.Script: return { label: "分镜脚本", icon: }; case CanvasNodeType.Image: return { label: "图片", icon: }; case CanvasNodeType.Video: return { label: "视频", icon: