返回 oh-my-ppt
TemplateCard.tsx
根目录 / src / renderer / src / components / templates / TemplateCard.tsx
1 import { ChevronDown, Eye, LayoutTemplate, PencilLine, Sparkles, Trash2 } from 'lucide-react'
2 import { Button } from '../ui/Button'
3 import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
4 import {
5 DropdownMenu,
6 DropdownMenuContent,
7 DropdownMenuItem,
8 DropdownMenuTrigger
9 } from '../ui/DropdownMenu'
10 import type { TemplateListItem } from '@renderer/lib/ipc'
11 import { useT } from '@renderer/i18n'
12 import dayjs from 'dayjs'
13 import { localAssetUrl } from '@shared/local-asset'
14 import { resolveSlideSize } from '@shared/slide-size'
15
16 export function TemplateCard({
17 template,
18 directCreating,
19 onUseDirect,
20 onUseGenerate,
21 onEdit,
22 onDelete,
23 onPreview
24 }: {
25 template: TemplateListItem
26 directCreating?: boolean
27 onUseDirect: (template: TemplateListItem) => void
28 onUseGenerate: (template: TemplateListItem) => void
29 onEdit: (template: TemplateListItem) => void
30 onDelete: (template: TemplateListItem) => void
31 onPreview: (template: TemplateListItem) => void
32 }): React.JSX.Element {
33 const t = useT()
34 const thumbnailUrl = template.thumbnailPath ? localAssetUrl(template.thumbnailPath) : ''
35 const slideSize = resolveSlideSize({
36 id: template.slideSizeId,
37 width: template.slideWidth,
38 height: template.slideHeight
39 })
40
41 return (
42 <Card
43 data-template-card-id={template.id}
44 className="group overflow-hidden !rounded-lg transition-all duration-200 ease-out hover:-translate-y-0.5 hover:shadow-[0_16px_30px_rgba(88,75,56,0.18)]"
45 >
46 <button
47 type="button"
48 className="relative block w-full overflow-hidden bg-[#f3ecdf] text-left"
49 style={{ aspectRatio: `${slideSize.width}/${slideSize.height}` }}
50 onClick={() => onPreview(template)}
51 disabled={!template.previewHtmlPath}
52 aria-label={t('common.preview')}
53 >
54 {thumbnailUrl ? (
55 <img
56 src={thumbnailUrl}
57 loading="lazy"
58 alt=""
59 aria-hidden="true"
60 className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.015]"
61 />
62 ) : (
63 <div className="absolute inset-0 flex items-center justify-center bg-[radial-gradient(circle_at_top_left,_#fffaf0,_#eee4d2)] text-[#8a7e6c]">
64 <div className="flex items-center gap-2 rounded-lg border border-white/70 bg-white/55 px-3 py-2 text-xs shadow-sm backdrop-blur-sm">
65 <LayoutTemplate className="h-4 w-4" />
66 <span>{t('templates.thumbnailGenerating')}</span>
67 </div>
68 </div>
69 )}
70 </button>
71 <CardHeader className="pb-2">
72 <CardTitle className="flex items-start justify-between gap-3 text-base">
73 <span className="min-w-0 truncate text-[#3e4a32]">{template.name}</span>
74 <span className="shrink-0 rounded-md border border-[#d6c08d]/80 bg-[#fff7e8] px-2 py-1 text-[11px] font-medium text-[#7c6a4c]">
75 {t('templates.pageCount', { count: template.pageCount })}
76 </span>
77 </CardTitle>
78 </CardHeader>
79 <CardContent>
80 <p className="line-clamp-2 min-h-[40px] text-xs leading-5 text-muted-foreground">
81 {template.description || t('templates.noDescription')}
82 </p>
83 {template.tags.length > 0 && (
84 <div className="mt-3 flex flex-wrap gap-1.5">
85 {template.tags.slice(0, 4).map((tag) => (
86 <span
87 key={tag}
88 className="rounded-md border border-[#d6c08d]/80 bg-[#fff7e8] px-1.5 py-0.5 text-[11px] text-[#7c6a4c]"
89 >
90 {tag}
91 </span>
92 ))}
93 </div>
94 )}
95 <div className="mt-4 flex items-center justify-between gap-3 border-t border-[#e5dccd]/58 pt-3">
96 <span className="min-w-0 truncate text-[11px] text-muted-foreground">
97 {dayjs(template.updatedAt).format('YYYY/MM/DD')}
98 </span>
99 <div className="flex shrink-0 items-center gap-1">
100 <div className="flex items-center gap-1 rounded-md bg-[#f4ecdf]/52 p-0.5">
101 {template.previewHtmlPath ? (
102 <Button
103 size="sm"
104 variant="ghost"
105 className="h-7 w-7 rounded-[6px] p-0 text-[#5f6b50]"
106 onClick={() => onPreview(template)}
107 title={t('common.preview')}
108 aria-label={t('common.preview')}
109 >
110 <Eye className="h-3.5 w-3.5" />
111 </Button>
112 ) : null}
113 <Button
114 size="sm"
115 variant="ghost"
116 className="h-7 w-7 rounded-[6px] p-0 text-[#5f6b50]"
117 onClick={() => onEdit(template)}
118 title={t('common.edit')}
119 aria-label={t('common.edit')}
120 >
121 <PencilLine className="h-3.5 w-3.5" />
122 </Button>
123 <Button
124 size="sm"
125 variant="ghost"
126 className="h-7 w-7 rounded-[6px] p-0 text-[#8a514b] hover:text-[#7a332d]"
127 onClick={() => onDelete(template)}
128 title={t('common.delete')}
129 aria-label={t('common.delete')}
130 >
131 <Trash2 className="h-3.5 w-3.5" />
132 </Button>
133 </div>
134 <DropdownMenu>
135 <DropdownMenuTrigger asChild>
136 <Button
137 size="sm"
138 className="h-8 rounded-md bg-[#5d6b4d] px-3 text-white shadow-none hover:bg-[#4f613f]"
139 disabled={directCreating}
140 >
141 <LayoutTemplate className="mr-1.5 h-3.5 w-3.5" />
142 {directCreating ? t('templates.creatingEditable') : t('templates.use')}
143 <ChevronDown className="ml-1.5 h-3.5 w-3.5" />
144 </Button>
145 </DropdownMenuTrigger>
146 <DropdownMenuContent align="end" className="w-44">
147 <DropdownMenuItem
148 className="text-xs"
149 onSelect={() => onUseDirect(template)}
150 disabled={directCreating}
151 >
152 <PencilLine className="h-3.5 w-3.5" />
153 {t('templates.createEditable')}
154 </DropdownMenuItem>
155 <DropdownMenuItem
156 className="text-xs"
157 onSelect={() => onUseGenerate(template)}
158 disabled={directCreating}
159 >
160 <Sparkles className="h-3.5 w-3.5" />
161 {t('templates.createAndGenerate')}
162 </DropdownMenuItem>
163 </DropdownMenuContent>
164 </DropdownMenu>
165 </div>
166 </div>
167 </CardContent>
168 </Card>
169 )
170 }
171
172 export function TemplateEmptyState(): React.JSX.Element {
173 const t = useT()
174
175 return (
176 <section className="flex min-h-[calc(100vh-220px)] items-center justify-center px-4 py-12">
177 <div className="flex w-full max-w-[460px] flex-col items-center text-center">
178 <div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg border border-[#d7cab1] bg-[#fff9ef] text-[#617052] shadow-[0_6px_16px_rgba(78,88,62,0.08)]">
179 <LayoutTemplate className="h-8 w-8" />
180 </div>
181 <h3 className="text-xl font-semibold text-[#3e4a32]">{t('templates.emptyTitle')}</h3>
182 <p className="mt-2 text-sm leading-6 text-[#7b705f]">{t('templates.emptyDescription')}</p>
183 <p className="mt-1 text-xs leading-5 text-[#95866f]">{t('templates.emptyHint')}</p>
184 </div>
185 </section>
186 )
187 }
188
188 lines Plain Text