返回 oh-my-ppt
GenerationThumbnail.tsx
根目录 / src / renderer / src / components / session-generating / GenerationThumbnail.tsx
1 import { CircleAlert, Loader2 } from 'lucide-react'
2 import { PreviewIframe } from '@renderer/components/preview/PreviewIframe'
3 import {
4 Tooltip,
5 TooltipContent,
6 TooltipProvider,
7 TooltipTrigger
8 } from '@renderer/components/ui/Tooltip'
9 import { cn } from '@renderer/lib/utils'
10 import type { GenerationPreviewPage } from './types'
11 import type { SlideSizePreset } from '@shared/slide-size'
12
13 export function GenerationThumbnail({
14 page,
15 previewEnabled = true,
16 slideSize
17 }: {
18 page: GenerationPreviewPage
19 previewEnabled?: boolean
20 slideSize: SlideSizePreset | null
21 }): React.JSX.Element {
22 if (!slideSize) {
23 return <div className="h-[195px] w-full rounded-xl bg-[#f5f1e8]/88" />
24 }
25 const thumbnailFitStyle =
26 slideSize.width >= slideSize.height
27 ? { width: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` }
28 : { height: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` }
29 const hasPreview =
30 previewEnabled && page.status === 'completed' && (page.htmlPath || page.sourceUrl)
31
32 const card = (
33 <div
34 tabIndex={hasPreview ? 0 : undefined}
35 className={cn(
36 'group relative flex w-full min-w-0 flex-col overflow-hidden rounded-xl border bg-[#fffaf1]/78 p-1.5 shadow-[0_16px_34px_rgba(70,82,58,0.12)] transition-all duration-500',
37 page.status === 'completed' && 'border-[#b8d3a6] translate-y-0 opacity-100',
38 page.status === 'generating' &&
39 'border-[#8fb873] bg-[#f6fbef]/88 shadow-[0_18px_40px_rgba(95,132,72,0.22)]',
40 page.status === 'failed' && 'border-[#d7b5ae] bg-[#fbf1ee]/92',
41 page.status === 'pending' && 'border-[#dfd4bf]/72 opacity-72'
42 )}
43 >
44 <div
45 className="relative flex h-[180px] w-full min-w-0 shrink-0 items-center justify-center overflow-hidden bg-[#f5f1e8]/88 shadow-[0_5px_14px_rgba(93,107,77,0.08)]"
46 style={{ contain: 'paint' }}
47 >
48 <div className="relative max-h-full max-w-full overflow-hidden" style={thumbnailFitStyle}>
49 {hasPreview ? (
50 <PreviewIframe
51 key={`generating-thumb-${page.id}-${page.previewVersion ?? 0}`}
52 src={page.sourceUrl}
53 htmlPath={page.htmlPath}
54 pageId={page.pageId}
55 title={`generating-page-${page.pageNumber}`}
56 slideSize={slideSize}
57 inspectable={false}
58 thumbnail
59 />
60 ) : (
61 <div
62 className={cn(
63 'flex h-full w-full flex-col justify-between p-3',
64 page.status === 'generating'
65 ? 'bg-[linear-gradient(135deg,#eef6e7_0%,#fff8ec_100%)]'
66 : page.status === 'failed'
67 ? 'bg-[#f7e7e2]'
68 : 'bg-[linear-gradient(135deg,#f5efe4_0%,#e9decb_100%)]'
69 )}
70 >
71 <div className="flex items-center justify-between">
72 <span className="h-2 w-16 rounded-full bg-white/72" />
73 <span className="h-5 w-5 rounded-md border border-white/80 bg-white/58" />
74 </div>
75 <div className="space-y-2">
76 <span className="block h-3 w-3/4 rounded-full bg-white/78" />
77 <span className="block h-2 w-11/12 rounded-full bg-white/56" />
78 <span className="block h-2 w-7/12 rounded-full bg-white/56" />
79 </div>
80 <div className="grid grid-cols-3 gap-2">
81 <span className="h-7 rounded-md bg-white/54" />
82 <span className="h-7 rounded-md bg-white/42" />
83 <span className="h-7 rounded-md bg-white/54" />
84 </div>
85 </div>
86 )}
87 </div>
88
89 {page.status === 'generating' && (
90 <div className="absolute inset-0 border-2 border-[#83ad67]/70">
91 <div className="absolute right-2 top-2 rounded-full bg-[#fffaf1]/90 p-1 shadow-sm">
92 <Loader2 className="h-3.5 w-3.5 animate-spin text-[#5f8a43]" />
93 </div>
94 </div>
95 )}
96
97 {page.status === 'failed' && (
98 <div className="absolute inset-0 flex items-center justify-center bg-[#fbf1ee]/76">
99 <CircleAlert className="h-6 w-6 text-[#a45f58]" />
100 </div>
101 )}
102 </div>
103
104 <div className="flex w-full min-w-0 items-center justify-between gap-2 px-0.5 pb-0.5 pt-2">
105 <span className="shrink-0 rounded-md bg-[#5d6b4d]/10 px-1.5 py-0.5 text-[10px] font-semibold text-[#4f613f]">
106 P{page.pageNumber}
107 </span>
108 {page.status !== 'failed' && (
109 <span className="min-w-0 truncate text-xs font-medium text-[#4d5b40]" title={page.title}>
110 {page.title}
111 </span>
112 )}
113 </div>
114 </div>
115 )
116
117 if (!hasPreview) return card
118
119 return (
120 <TooltipProvider delayDuration={0} skipDelayDuration={0}>
121 <Tooltip disableHoverableContent>
122 <TooltipTrigger asChild>{card}</TooltipTrigger>
123 <TooltipContent
124 side="top"
125 align="center"
126 sideOffset={12}
127 aria-label={`Page ${page.pageNumber} preview`}
128 className="pointer-events-none z-[60] max-w-none border-[#b8d3a6] bg-[#fffaf1] p-2 shadow-[0_24px_64px_rgba(70,82,58,0.24)]"
129 >
130 <div className="relative flex h-[280px] w-[440px] items-center justify-center overflow-hidden rounded-lg bg-[#f5f1e8]">
131 <div
132 className="relative max-h-full max-w-full overflow-hidden"
133 style={thumbnailFitStyle}
134 >
135 <PreviewIframe
136 key={`generating-hover-preview-${page.id}-${page.previewVersion ?? 0}`}
137 src={page.sourceUrl}
138 htmlPath={page.htmlPath}
139 pageId={page.pageId}
140 title={`generating-hover-preview-${page.pageNumber}`}
141 slideSize={slideSize}
142 inspectable={false}
143 />
144 </div>
145 </div>
146 </TooltipContent>
147 </Tooltip>
148 </TooltipProvider>
149 )
150 }
151
151 lines Plain Text