返回 oh-my-ppt
PageThumbnail.tsx
根目录 / src / renderer / src / components / session-detail / sidebar / PageThumbnail.tsx
1 import { memo } from 'react'
2 import { Loader2 } from 'lucide-react'
3 import { cn } from '@renderer/lib/utils'
4 import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/Tooltip'
5 import { PreviewIframe } from '../../preview/PreviewIframe'
6 import type { SessionPreviewPage } from '../shared/types'
7 import { useT } from '@renderer/i18n'
8 import { useSessionStore } from '@renderer/store'
9 import { trySessionSlideSize } from '@shared/slide-size'
10
11 export const PageThumbnail = memo(function PageThumbnail({
12 page,
13 isSelected,
14 previewVersion,
15 renderPreview = true,
16 onSelect,
17 actions,
18 failureOverlay
19 }: {
20 page: SessionPreviewPage
21 isSelected: boolean
22 previewVersion: number
23 renderPreview?: boolean
24 onSelect?: (pageId: string) => void
25 actions?: React.ReactNode
26 failureOverlay?: React.ReactNode
27 }): React.JSX.Element {
28 const t = useT()
29 const currentSession = useSessionStore((state) => state.currentSession)
30 const slideSize = trySessionSlideSize(currentSession)
31 const isGeneratingPlaceholder = page.status === 'generating' || page.status === 'pending'
32 if (!slideSize) {
33 return <div className="h-[154px] w-full rounded-[1.25rem] bg-[#e8e0d0]/34" />
34 }
35 const thumbnailFitStyle =
36 slideSize.width >= slideSize.height
37 ? { width: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` }
38 : { height: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` }
39
40 const pageInfoTooltip = (
41 <TooltipContent side="right" align="start">
42 <div className="max-w-[240px]">
43 <div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#7a875f]">
44 {t('sessionDetail.pageNumber', { pageNumber: page.pageNumber })}
45 </div>
46 <div className="mt-0.5 text-sm font-medium text-[#3e4a32]">{page.title}</div>
47 </div>
48 </TooltipContent>
49 )
50
51 return (
52 <div
53 role="button"
54 tabIndex={0}
55 onClick={onSelect ? () => onSelect(page.id) : undefined}
56 aria-disabled={!onSelect}
57 className={cn(
58 'group relative block w-full min-w-0 overflow-hidden rounded-[1.25rem] p-1.5 text-left transition-all duration-200',
59 onSelect ? 'cursor-pointer' : 'cursor-default opacity-60',
60 isSelected
61 ? 'bg-[#d4e4c1]/86 shadow-[0_14px_26px_rgba(93,107,77,0.18)]'
62 : 'bg-[#e8e0d0]/34 hover:bg-[#e8e0d0]/68 hover:shadow-[0_8px_18px_rgba(93,107,77,0.09)]'
63 )}
64 >
65 <div
66 className={cn(
67 'pointer-events-none absolute -right-7 -top-8 h-20 w-20 rounded-[30%_70%_70%_30%/30%_30%_70%_70%] transition-opacity',
68 isSelected
69 ? 'bg-[#8fbc8f]/24 opacity-100'
70 : 'bg-[#d4e4c1]/28 opacity-0 group-hover:opacity-100'
71 )}
72 />
73 <div
74 className={cn(
75 'relative flex h-[138px] w-full items-center justify-center overflow-hidden rounded-[1rem] bg-[#f5f1e8]/88 shadow-[0_5px_14px_rgba(93,107,77,0.08)]',
76 isSelected
77 ? 'shadow-[0_6px_16px_rgba(93,107,77,0.13)]'
78 : 'group-hover:shadow-[0_6px_15px_rgba(93,107,77,0.1)]'
79 )}
80 style={{
81 contain: 'paint'
82 }}
83 >
84 <div className="relative max-h-full max-w-full overflow-hidden" style={thumbnailFitStyle}>
85 {isGeneratingPlaceholder ? (
86 <div
87 className="flex h-full w-full flex-col items-center justify-center gap-2 bg-[#f8f4eb] text-[#5d6b4d]"
88 aria-live="polite"
89 >
90 <Loader2 className="h-4 w-4 animate-spin" />
91 <span className="text-[10px] font-semibold">
92 {t('sessionDetail.activityProcessing')}
93 </span>
94 </div>
95 ) : renderPreview ? (
96 <PreviewIframe
97 key={`thumb-${page.id}-${previewVersion}`}
98 src={page.sourceUrl}
99 htmlPath={page.htmlPath}
100 pageId={page.pageId}
101 title={`filmstrip-page-${page.pageNumber}`}
102 slideSize={slideSize}
103 inspectable={false}
104 thumbnail
105 />
106 ) : (
107 <div className="flex h-full w-full items-center justify-center bg-[#eee7d9]/78 text-[10px] font-semibold uppercase tracking-[0.12em] text-[#8a9a7b]">
108 P{page.pageNumber}
109 </div>
110 )}
111 </div>
112 {failureOverlay}
113 </div>
114 <Tooltip>
115 <TooltipTrigger asChild>
116 <div className="relative min-w-0">
117 <div className="relative mt-1.5 flex items-center justify-between gap-1 px-0.5">
118 <span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#5c6c47]">
119 P{page.pageNumber}
120 </span>
121 {isSelected ? (
122 <span className="rounded-full bg-[#5d6b4d] px-1.5 py-0.5 text-[9px] font-semibold text-white shadow-[0_3px_8px_rgba(62,74,50,0.18)]">
123 {t('sessionDetail.current')}
124 </span>
125 ) : null}
126 </div>
127 <div
128 className="relative mt-0.5 block w-full min-w-0 max-w-full overflow-hidden whitespace-normal break-words px-0.5 text-[11px] font-medium leading-4 text-[#4c5d3d]"
129 style={{
130 display: '-webkit-box',
131 WebkitLineClamp: 2,
132 WebkitBoxOrient: 'vertical'
133 }}
134 >
135 {page.title}
136 </div>
137 </div>
138 </TooltipTrigger>
139 {pageInfoTooltip}
140 </Tooltip>
141 {actions}
142 </div>
143 )
144 })
145
145 lines Plain Text