返回 AiToEarn
PluginUpdateDocsContent.tsx
根目录 / project / aitoearn-web / src / app / [lng] / websit / plugin-update-docs / PluginUpdateDocsContent.tsx
1 /**
2 * PluginUpdateDocsContent - 浏览器插件更新图文教学内容组件
3 * 展示 .crx 插件包更新扩展的完整步骤
4 */
5 'use client'
6
7 import type { ReactNode } from 'react'
8 import {
9 BookOpen,
10 CheckCircle2,
11 ChevronRight,
12 CloudDownload,
13 Download,
14 HelpCircle,
15 ShieldCheck,
16 Sparkles,
17 ZoomIn,
18 } from 'lucide-react'
19 import Image from 'next/image'
20 import Link from 'next/link'
21 import { useCallback, useState } from 'react'
22 import { useTransClient } from '@/app/i18n/client'
23 import { MediaPreview } from '@/components/common/MediaPreview'
24 import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
25 import { Button } from '@/components/ui/button'
26 import { Card } from '@/components/ui/card'
27 import { PLUGIN_DOWNLOAD_LINKS } from '@/store/plugin/constants'
28 import { cn } from '@/utils/className'
29
30 interface PublicImageAsset {
31 src: string
32 width: number
33 height: number
34 }
35
36 const pluginUpdateDocsImages = {
37 pluginUpdateDocsImg1_1: {
38 src: '/assets/plugin-update-docs-images/pluginUpdateDocsImg1_1.png',
39 width: 537,
40 height: 321,
41 },
42 pluginUpdateDocsImg1_2: { src: '/assets/plugin-guide-images/pluginGuideImg1-2.png', width: 1878, height: 1015 },
43 pluginUpdateDocsImg1_3: { src: '/assets/plugin-guide-images/pluginGuideImg3.png', width: 1912, height: 1026 },
44 pluginUpdateDocsImg1_4: { src: '/assets/plugin-guide-images/pluginGuideImg1-5.png', width: 1850, height: 940 },
45 pluginUpdateDocsImg1_5: { src: '/assets/plugin-guide-images/pluginGuideImg1-3.png', width: 1893, height: 935 },
46 pluginUpdateDocsImg1_6: {
47 src: '/assets/plugin-update-docs-images/pluginUpdateDocsImg1_2.png',
48 width: 1203,
49 height: 766,
50 },
51 pluginUpdateDocsImg5: { src: '/assets/plugin-guide-images/pluginGuideImg1-5.png', width: 1850, height: 940 },
52 } satisfies Record<string, PublicImageAsset>
53
54 const {
55 pluginUpdateDocsImg1_1,
56 pluginUpdateDocsImg1_2,
57 pluginUpdateDocsImg1_3,
58 pluginUpdateDocsImg1_4,
59 pluginUpdateDocsImg1_5,
60 pluginUpdateDocsImg1_6,
61 pluginUpdateDocsImg5,
62 } = pluginUpdateDocsImages
63
64 const ALL_IMAGES = [
65 pluginUpdateDocsImg1_1,
66 pluginUpdateDocsImg1_2,
67 pluginUpdateDocsImg1_3,
68 pluginUpdateDocsImg1_4,
69 pluginUpdateDocsImg1_5,
70 pluginUpdateDocsImg1_6,
71 pluginUpdateDocsImg5,
72 ]
73
74 interface GuideImageProps {
75 src: PublicImageAsset
76 alt: string
77 caption?: string
78 className?: string
79 onClick?: () => void
80 }
81
82 function GuideImage({ src, alt, caption, className, onClick }: GuideImageProps) {
83 return (
84 <figure className={cn('space-y-3', className)}>
85 <button
86 type="button"
87 className="group relative block w-full overflow-hidden rounded-2xl border border-border/60 bg-background/90 p-2 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:shadow-lg cursor-pointer"
88 onClick={onClick}
89 >
90 <Image
91 src={src.src}
92 alt={alt}
93 className="h-auto w-full rounded-xl object-contain"
94 width={src.width}
95 height={src.height}
96 sizes="(min-width: 1280px) 760px, (min-width: 768px) 70vw, 100vw"
97 />
98 <div className="absolute inset-0 flex items-center justify-center rounded-xl bg-background/0 transition-colors duration-200 group-hover:bg-background/10">
99 <div className="flex items-center gap-2 rounded-full border border-border/60 bg-background/90 px-3 py-1.5 text-xs font-medium text-foreground opacity-0 shadow-sm transition-all duration-200 group-hover:opacity-100">
100 <ZoomIn className="h-3.5 w-3.5" />
101 {caption || alt}
102 </div>
103 </div>
104 </button>
105 {caption && (
106 <figcaption className="text-center text-sm text-muted-foreground">
107 {caption}
108 </figcaption>
109 )}
110 </figure>
111 )
112 }
113
114 interface SectionHeadingProps {
115 id: string
116 title: string
117 description?: string
118 icon: ReactNode
119 }
120
121 function SectionHeading({ id, title, description, icon }: SectionHeadingProps) {
122 return (
123 <div id={id} className="scroll-mt-24">
124 <div className="flex items-start gap-4 rounded-2xl border border-border/60 bg-linear-to-r from-sky-500/6 via-background to-transparent p-5 shadow-sm">
125 <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-linear-to-br from-sky-500/15 to-emerald-500/15 text-sky-700 dark:text-sky-300">
126 {icon}
127 </div>
128 <div className="space-y-1">
129 <h2 className="text-2xl font-semibold tracking-tight md:text-3xl">{title}</h2>
130 {description && (
131 <p className="text-sm leading-6 text-muted-foreground md:text-base">
132 {description}
133 </p>
134 )}
135 </div>
136 </div>
137 </div>
138 )
139 }
140
141 interface StepCardProps {
142 step: number
143 title: string
144 description: string
145 children?: ReactNode
146 }
147
148 function StepCard({ step, title, description, children }: StepCardProps) {
149 return (
150 <Card className="overflow-hidden border-border/60 shadow-sm">
151 <div className="border-b border-border/50 bg-linear-to-r from-sky-500/8 to-transparent px-5 py-4 md:px-6">
152 <div className="flex items-start gap-4">
153 <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-linear-to-br from-sky-600 to-emerald-500 text-sm font-semibold text-white shadow-sm">
154 {step}
155 </div>
156 <div className="space-y-1">
157 <h3 className="text-lg font-semibold md:text-xl">{title}</h3>
158 <p className="text-sm leading-6 text-muted-foreground md:text-base">
159 {description}
160 </p>
161 </div>
162 </div>
163 </div>
164 {children && <div className="space-y-5 p-5 md:p-6">{children}</div>}
165 </Card>
166 )
167 }
168
169 function TableOfContents({ t }: { t: (key: string) => string }) {
170 const items = [
171 { id: 'before-update', label: t('sections.beforeUpdate') },
172 { id: 'update-steps', label: t('sections.steps') },
173 ]
174
175 return (
176 <Card className="sticky top-20 overflow-hidden border-border/60 p-4 shadow-sm">
177 <div className="mb-3 text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
178 {t('tableOfContents')}
179 </div>
180 <nav className="space-y-1.5">
181 {items.map(item => (
182 <a
183 key={item.id}
184 href={`#${item.id}`}
185 className="flex items-center gap-2 rounded-xl px-3 py-2 text-sm text-foreground/80 transition-colors hover:bg-sky-500/8 hover:text-foreground"
186 >
187 <ChevronRight className="h-3.5 w-3.5 text-sky-600 dark:text-sky-300" />
188 {item.label}
189 </a>
190 ))}
191 </nav>
192 </Card>
193 )
194 }
195
196 export default function PluginUpdateDocsContent() {
197 const { t } = useTransClient('pluginUpdateDocs')
198 const [previewOpen, setPreviewOpen] = useState(false)
199 const [previewIndex, setPreviewIndex] = useState(0)
200
201 const openPreview = useCallback((index: number) => {
202 setPreviewIndex(index)
203 setPreviewOpen(true)
204 }, [])
205
206 const closePreview = useCallback(() => {
207 setPreviewOpen(false)
208 }, [])
209
210 const previewItems = ALL_IMAGES.map((img, index) => ({
211 type: 'image' as const,
212 src: img.src,
213 title: `${t('hero.title')} ${index + 1}`,
214 }))
215
216 const checklistItems = [
217 t('checklist.item1'),
218 t('checklist.item2'),
219 ]
220
221 const steps = [
222 {
223 title: t('steps.step1.title'),
224 content: t('steps.step1.content'),
225 caption: t('steps.step1.caption'),
226 image: pluginUpdateDocsImg1_1,
227 },
228 {
229 title: t('steps.step2.title'),
230 content: t('steps.step2.content'),
231 caption: t('steps.step2.caption'),
232 image: pluginUpdateDocsImg1_2,
233 },
234 {
235 title: t('steps.step3.title'),
236 content: t('steps.step3.content'),
237 caption: t('steps.step3.caption'),
238 image: pluginUpdateDocsImg1_3,
239 },
240 {
241 title: t('steps.step4.title'),
242 content: t('steps.step4.content'),
243 caption: t('steps.step4.caption'),
244 image: pluginUpdateDocsImg1_4,
245 },
246 {
247 title: t('steps.step5.title'),
248 content: t('steps.step5.content'),
249 caption: t('steps.step5.caption'),
250 image: pluginUpdateDocsImg1_5,
251 },
252 {
253 title: t('steps.step6.title'),
254 content: t('steps.step6.content'),
255 caption: t('steps.step6.caption'),
256 image: pluginUpdateDocsImg1_6,
257 },
258 {
259 title: t('steps.step7.title'),
260 content: t('steps.step7.content'),
261 caption: t('steps.step7.caption'),
262 image: pluginUpdateDocsImg5,
263 },
264 ]
265
266 return (
267 <div className="relative min-h-screen bg-background">
268 <div className="pointer-events-none absolute inset-0 overflow-hidden">
269 <div className="absolute left-[-8rem] top-24 h-72 w-72 rounded-full bg-sky-500/8 blur-3xl" />
270 <div className="absolute right-[-6rem] top-[28rem] h-80 w-80 rounded-full bg-emerald-500/8 blur-3xl" />
271 </div>
272
273 <MediaPreview
274 open={previewOpen}
275 items={previewItems}
276 initialIndex={previewIndex}
277 onClose={closePreview}
278 />
279
280 <header className="relative overflow-hidden border-b border-border/60 bg-linear-to-b from-sky-500/6 via-background to-background">
281 <div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 md:px-6 md:py-16 lg:grid-cols-[minmax(0,1fr)_320px]">
282 <div className="space-y-6">
283 <div className="inline-flex items-center gap-2 rounded-full border border-sky-500/15 bg-sky-500/6 px-4 py-2 text-sm font-medium text-foreground/80">
284 <Sparkles className="h-4 w-4 text-sky-600 dark:text-sky-300" />
285 {t('hero.eyebrow')}
286 </div>
287
288 <div className="space-y-4">
289 <h1 className="max-w-3xl text-3xl font-semibold tracking-tight md:text-5xl">
290 {t('hero.title')}
291 </h1>
292 <p className="max-w-3xl text-base leading-7 text-muted-foreground md:text-lg">
293 {t('hero.description')}
294 </p>
295 </div>
296
297 <div className="flex flex-wrap gap-3">
298 <Button asChild className="gap-2 cursor-pointer">
299 <a
300 href={PLUGIN_DOWNLOAD_LINKS.china}
301 target="_blank"
302 rel="noopener noreferrer"
303 >
304 <CloudDownload className="h-4 w-4" />
305 {t('actions.downloadChina')}
306 </a>
307 </Button>
308
309 <Button asChild variant="secondary" className="gap-2 cursor-pointer">
310 <a
311 href={PLUGIN_DOWNLOAD_LINKS.github}
312 target="_blank"
313 rel="noopener noreferrer"
314 >
315 <Download className="h-4 w-4" />
316 {t('actions.downloadGithub')}
317 </a>
318 </Button>
319
320 <Button asChild variant="outline" className="gap-2 cursor-pointer">
321 <Link href="/websit/plugin-guide">
322 <BookOpen className="h-4 w-4" />
323 {t('actions.viewInstallGuide')}
324 </Link>
325 </Button>
326 </div>
327
328 </div>
329
330 <Card className="border-border/60 bg-background/90 p-5 shadow-sm">
331 <div className="space-y-4">
332 <div className="text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">
333 {t('hero.summaryLabel')}
334 </div>
335
336 <div className="grid gap-3">
337 <div className="rounded-2xl border border-border/60 p-4">
338 <div className="text-sm text-muted-foreground">{t('facts.time')}</div>
339 <div className="mt-1 text-lg font-semibold">{t('facts.timeValue')}</div>
340 </div>
341 <div className="rounded-2xl border border-border/60 p-4">
342 <div className="text-sm text-muted-foreground">{t('facts.scenario')}</div>
343 <div className="mt-1 text-lg font-semibold leading-7">{t('facts.scenarioValue')}</div>
344 </div>
345 </div>
346 </div>
347 </Card>
348 </div>
349 </header>
350
351 <div className="mx-auto flex max-w-7xl gap-8 px-4 py-10 md:px-6 md:py-12">
352 <aside className="hidden w-72 shrink-0 lg:block">
353 <TableOfContents t={t} />
354 </aside>
355
356 <main className="min-w-0 flex-1 space-y-12">
357 <section className="space-y-6">
358 <SectionHeading
359 id="before-update"
360 title={t('sections.beforeUpdate')}
361 description={t('sections.beforeUpdateDesc')}
362 icon={<ShieldCheck className="h-5 w-5" />}
363 />
364
365 <Alert className="border-amber-500/25 bg-amber-500/8">
366 <HelpCircle className="h-4 w-4 text-amber-700 dark:text-amber-300" />
367 <AlertTitle className="text-amber-900 dark:text-amber-100">
368 {t('backupNotice.title')}
369 </AlertTitle>
370 <AlertDescription className="space-y-2 text-amber-800/90 dark:text-amber-100/80">
371 <p>{t('backupNotice.content1')}</p>
372 <p>{t('backupNotice.content2')}</p>
373 </AlertDescription>
374 </Alert>
375
376 <Card className="border-border/60 p-5 shadow-sm md:p-6">
377 <div className="mb-4 flex items-center gap-2">
378 <CheckCircle2 className="h-5 w-5 text-emerald-600 dark:text-emerald-300" />
379 <h3 className="text-lg font-semibold">{t('checklist.title')}</h3>
380 </div>
381 <ul className="space-y-3">
382 {checklistItems.map(item => (
383 <li key={item} className="flex gap-3 text-sm leading-6 text-muted-foreground md:text-base">
384 <span className="mt-2 h-2 w-2 shrink-0 rounded-full bg-emerald-500" />
385 <span>{item}</span>
386 </li>
387 ))}
388 </ul>
389 </Card>
390 </section>
391
392 <section className="space-y-6">
393 <SectionHeading
394 id="update-steps"
395 title={t('sections.steps')}
396 description={t('sections.stepsDesc')}
397 icon={<Download className="h-5 w-5" />}
398 />
399
400 <div className="space-y-6">
401 {steps.map((step, index) => (
402 <StepCard
403 key={step.title}
404 step={index + 1}
405 title={step.title}
406 description={step.content}
407 >
408 <GuideImage
409 src={step.image}
410 alt={step.caption}
411 caption={step.caption}
412 onClick={() => openPreview(index)}
413 />
414 </StepCard>
415 ))}
416 </div>
417 </section>
418 </main>
419 </div>
420 </div>
421 )
422 }
423
423 lines Plain Text