返回 AiToEarn
1 /**
2 * EcosystemDiagram - AiToEarn 生态系统图
3 * 一图解释 AiToEarn 的核心功能和生态闭环
4 * 设计风格:白色背景 + 宇宙行星动画 + 环形布局 + 自动轮播详情
5 */
6 'use client'
7
8 import { useEffect, useRef, useState } from 'react'
9 import { useTransClient } from '@/app/i18n/client'
10 import { cn } from '@/utils/className'
11
12 /** 功能节点类型 */
13 type FunctionKey = 'publish' | 'create' | 'manage' | 'analyze' | 'dashboard'
14
15 export function EcosystemDiagram() {
16 const { t } = useTransClient('home')
17 const [activeNode, setActiveNode] = useState<FunctionKey | null>('publish')
18 const [isAutoPlay, setIsAutoPlay] = useState(true)
19 const autoPlayTimerRef = useRef<NodeJS.Timeout | null>(null)
20
21 // 功能节点顺序
22 const nodeOrder: FunctionKey[] = ['publish', 'create', 'manage', 'analyze', 'dashboard']
23
24 // 自动轮播
25 useEffect(() => {
26 if (!isAutoPlay)
27 return
28
29 autoPlayTimerRef.current = setInterval(() => {
30 setActiveNode((prev) => {
31 const currentIndex = prev ? nodeOrder.indexOf(prev) : -1
32 const nextIndex = (currentIndex + 1) % nodeOrder.length
33 return nodeOrder[nextIndex]
34 })
35 }, 4000) // 每 4 秒切换
36
37 return () => {
38 if (autoPlayTimerRef.current) {
39 clearInterval(autoPlayTimerRef.current)
40 }
41 }
42 }, [isAutoPlay])
43
44 // 手动点击节点
45 const handleNodeClick = (key: FunctionKey) => {
46 setIsAutoPlay(false) // 停止自动播放
47 setActiveNode(key)
48 // 8 秒后恢复自动播放
49 setTimeout(() => setIsAutoPlay(true), 8000)
50 }
51
52 // 获取节点详情
53 const getNodeDetails = (key: FunctionKey): string[] => {
54 const detailsKey = `ecosystem.${key}Details` as const
55 const details = t(detailsKey as any, { returnObjects: true })
56 if (Array.isArray(details)) {
57 return details.map(item => String(item))
58 }
59 // 如果不是数组,可能是字符串,尝试解析
60 if (typeof details === 'string' && details !== detailsKey) {
61 return [details]
62 }
63 return []
64 }
65
66 return (
67 <section className="py-16 md:py-24 px-4 relative overflow-hidden">
68 <div className="max-w-6xl mx-auto">
69 {/* 标题区域 */}
70 <div className="text-center mb-12 md:mb-16">
71 <p className="text-muted-foreground text-base md:text-lg max-w-2xl mx-auto">
72 {t('ecosystem.subtitle')}
73 </p>
74 </div>
75
76 {/* 生态系统图 - 白色背景容器 */}
77 <div className={cn('relative rounded-3xl overflow-hidden', 'bg-white dark:bg-background')}>
78 {/* 内容区域 */}
79 <div className="relative z-10 py-12 md:py-16 px-6 md:px-12">
80 {/* 桌面端布局 */}
81 <div className="hidden md:flex items-center justify-center gap-8 lg:gap-12">
82 {/* 左侧:内容创作者生态圈 - 宇宙行星风格 */}
83 <div className="relative">
84 <div className="relative w-[320px] h-[320px] lg:w-[380px] lg:h-[380px]">
85 {/* 宇宙背景光晕 */}
86 <div className="absolute inset-[-20px] rounded-full bg-gradient-to-r from-blue-500/5 via-purple-500/5 to-cyan-500/5 blur-2xl animate-pulse" />
87
88 {/* 最外层轨道环 - 缓慢旋转 */}
89 <div className="absolute inset-0 rounded-full border border-dashed border-blue-300/30 dark:border-blue-400/20 animate-[spin_60s_linear_infinite]">
90 {/* 轨道上的卫星点 */}
91 <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-blue-400/60 shadow-[0_0_8px_2px_rgba(59,130,246,0.4)]" />
92 <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-1.5 h-1.5 rounded-full bg-purple-400/60 shadow-[0_0_6px_2px_rgba(168,85,247,0.4)]" />
93 </div>
94
95 {/* 第二层轨道环 - 反向旋转 */}
96 <div className="absolute inset-6 rounded-full border border-purple-300/30 dark:border-purple-400/20 animate-[spin_45s_linear_infinite_reverse]">
97 {/* 轨道上的卫星点 */}
98 <div className="absolute top-1/2 right-0 translate-x-1/2 -translate-y-1/2 w-2.5 h-2.5 rounded-full bg-cyan-400/70 shadow-[0_0_10px_3px_rgba(34,211,238,0.5)]" />
99 <div className="absolute top-1/2 left-0 -translate-x-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full bg-indigo-400/60 shadow-[0_0_6px_2px_rgba(129,140,248,0.4)]" />
100 </div>
101
102 {/* 第三层轨道环 - 正向旋转较快 */}
103 <div className="absolute inset-12 rounded-full border border-cyan-300/30 dark:border-cyan-400/20 animate-[spin_30s_linear_infinite]">
104 {/* 轨道上的卫星点 */}
105 <div className="absolute bottom-1/4 right-0 translate-x-1/2 w-2 h-2 rounded-full bg-pink-400/60 shadow-[0_0_8px_2px_rgba(244,114,182,0.4)]" />
106 </div>
107
108 {/* 内层发光星球核心 */}
109 <div className="absolute inset-[72px] lg:inset-20 rounded-full overflow-hidden">
110 {/* 多层渐变光晕 */}
111 <div className="absolute inset-0 rounded-full bg-gradient-to-br from-blue-100 via-purple-50 to-cyan-100 dark:from-blue-900/40 dark:via-purple-900/30 dark:to-cyan-900/40" />
112 <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-transparent via-white/50 to-transparent dark:via-white/10 animate-[spin_20s_linear_infinite]" />
113 {/* 核心光芒 */}
114 <div className="absolute inset-2 rounded-full bg-white/80 dark:bg-background/80 shadow-[inset_0_0_30px_rgba(59,130,246,0.15),0_0_20px_rgba(139,92,246,0.1)]" />
115 {/* 脉动光环 */}
116 <div className="absolute inset-0 rounded-full border-2 border-blue-200/30 dark:border-blue-500/20 animate-ping opacity-30" />
117 </div>
118
119 {/* 中心文字 */}
120 <div className="absolute inset-0 flex items-center justify-center z-10">
121 <span className="text-foreground font-bold text-lg lg:text-xl drop-shadow-sm">
122 {t('ecosystem.creator')}
123 </span>
124 </div>
125
126 {/* 星星装饰粒子 */}
127 <div className="absolute top-[10%] left-[15%] w-1 h-1 rounded-full bg-blue-400/80 animate-[twinkle_2s_ease-in-out_infinite]" />
128 <div className="absolute top-[20%] right-[10%] w-0.5 h-0.5 rounded-full bg-purple-400/80 animate-[twinkle_3s_ease-in-out_infinite_0.5s]" />
129 <div className="absolute bottom-[15%] left-[10%] w-1 h-1 rounded-full bg-cyan-400/80 animate-[twinkle_2.5s_ease-in-out_infinite_1s]" />
130 <div className="absolute bottom-[25%] right-[15%] w-0.5 h-0.5 rounded-full bg-pink-400/80 animate-[twinkle_2s_ease-in-out_infinite_1.5s]" />
131
132 {/* 功能节点 - 均匀分布在圆环周围 */}
133 {nodeOrder.map((key, index) => {
134 // 5 个节点均匀分布在圆周上
135 // 起始角度从 -90° (顶部) 开始,顺时针分布
136 const totalNodes = nodeOrder.length
137 const angleStep = 360 / totalNodes
138 // 从左上方开始,顺时针分布
139 const startAngle = -126 // 起始角度(从正上方偏左开始)
140 const angle = startAngle + index * angleStep
141 // 半径比例(相对于容器尺寸的 50%,再往外偏移一点)
142 const radius = 58 // 百分比
143
144 return (
145 <FunctionNode
146 key={key}
147 nodeKey={key}
148 label={t(`ecosystem.${key}` as any)}
149 angle={angle}
150 radius={radius}
151 isActive={activeNode === key}
152 onClick={() => handleNodeClick(key)}
153 />
154 )
155 })}
156 </div>
157 </div>
158
159 {/* 中间:内容交易 + 双向箭头 - 能量传输效果 */}
160 <div className="flex flex-col items-center gap-4">
161 <div className="flex items-center gap-2 text-muted-foreground text-sm">
162 <span>{t('ecosystem.sell')}</span>
163 <div className="relative">
164 <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
165 <path
166 strokeLinecap="round"
167 strokeLinejoin="round"
168 strokeWidth={2}
169 d="M17 8l4 4m0 0l-4 4m4-4H3"
170 />
171 </svg>
172 {/* 能量粒子动画 */}
173 <div className="absolute top-1/2 left-0 w-1 h-1 rounded-full bg-blue-400 animate-[energyFlow_1.5s_ease-in-out_infinite]" />
174 </div>
175 </div>
176
177 <div className="relative px-5 py-2.5 rounded-full bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 border border-blue-200/50 dark:border-blue-700/30 shadow-md">
178 <span className="text-foreground font-medium text-sm whitespace-nowrap">
179 {t('ecosystem.trade')}
180 </span>
181 {/* 光晕效果 */}
182 <div className="absolute inset-0 rounded-full bg-gradient-to-r from-blue-400/10 to-purple-400/10 animate-pulse" />
183 </div>
184
185 <div className="flex items-center gap-2 text-muted-foreground text-sm">
186 <div className="relative">
187 <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188 <path
189 strokeLinecap="round"
190 strokeLinejoin="round"
191 strokeWidth={2}
192 d="M7 16l-4-4m0 0l4-4m-4 4h18"
193 />
194 </svg>
195 {/* 能量粒子动画 */}
196 <div className="absolute top-1/2 right-0 w-1 h-1 rounded-full bg-purple-400 animate-[energyFlowReverse_1.5s_ease-in-out_infinite]" />
197 </div>
198 <span>{t('ecosystem.buy')}</span>
199 </div>
200 </div>
201
202 {/* 右侧:内容需求者 - 小行星风格 */}
203 <div>
204 <div className="relative w-32 h-32 lg:w-40 lg:h-40">
205 {/* 外层光晕 */}
206 <div className="absolute inset-[-10px] rounded-full bg-gradient-to-br from-purple-500/10 via-pink-500/10 to-orange-500/10 blur-xl animate-pulse" />
207
208 {/* 外层轨道环 - 缓慢旋转 */}
209 <div className="absolute inset-[-8px] rounded-full border border-dashed border-purple-300/30 dark:border-purple-400/20 animate-[spin_40s_linear_infinite]">
210 <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full bg-orange-400/70 shadow-[0_0_6px_2px_rgba(251,146,60,0.5)]" />
211 </div>
212
213 {/* 内层轨道 - 反向旋转 */}
214 <div className="absolute inset-1 rounded-full border border-pink-300/25 dark:border-pink-400/15 animate-[spin_25s_linear_infinite_reverse]">
215 <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-1 h-1 rounded-full bg-cyan-400/60 shadow-[0_0_4px_2px_rgba(34,211,238,0.4)]" />
216 </div>
217
218 {/* 星球本体 */}
219 <div className="absolute inset-3 rounded-full overflow-hidden">
220 {/* 渐变背景 */}
221 <div className="absolute inset-0 rounded-full bg-gradient-to-br from-purple-100 via-pink-50 to-orange-100 dark:from-purple-900/40 dark:via-pink-900/30 dark:to-orange-900/40" />
222 {/* 光泽效果 */}
223 <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-transparent via-white/40 to-transparent dark:via-white/10 animate-[spin_15s_linear_infinite]" />
224 {/* 核心 */}
225 <div className="absolute inset-1 rounded-full bg-white/90 dark:bg-background/90 shadow-[inset_0_0_20px_rgba(168,85,247,0.15),0_0_15px_rgba(244,114,182,0.1)] flex items-center justify-center">
226 <span className="text-foreground font-bold text-base lg:text-lg text-center px-2 drop-shadow-sm">
227 {t('ecosystem.consumer')}
228 </span>
229 </div>
230 {/* 脉动效果 */}
231 <div className="absolute inset-0 rounded-full border border-purple-200/30 dark:border-purple-500/20 animate-ping opacity-20" />
232 </div>
233
234 {/* 星星装饰 */}
235 <div className="absolute top-[5%] right-[10%] w-0.5 h-0.5 rounded-full bg-pink-400/80 animate-[twinkle_2s_ease-in-out_infinite]" />
236 <div className="absolute bottom-[10%] left-[5%] w-0.5 h-0.5 rounded-full bg-purple-400/80 animate-[twinkle_2.5s_ease-in-out_infinite_0.5s]" />
237 </div>
238 </div>
239 </div>
240
241 {/* 移动端布局 */}
242 <div className="md:hidden space-y-6">
243 {/* 内容创作者 - 宇宙行星风格 */}
244 <div className="flex justify-center">
245 <div className="relative w-40 h-40">
246 {/* 光晕 */}
247 <div className="absolute inset-[-10px] rounded-full bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-cyan-500/10 blur-xl animate-pulse" />
248
249 {/* 轨道环 */}
250 <div className="absolute inset-0 rounded-full border border-dashed border-blue-300/30 animate-[spin_40s_linear_infinite]">
251 <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full bg-blue-400/60 shadow-[0_0_6px_2px_rgba(59,130,246,0.4)]" />
252 </div>
253 <div className="absolute inset-4 rounded-full border border-purple-300/25 animate-[spin_30s_linear_infinite_reverse]">
254 <div className="absolute top-1/2 right-0 translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-cyan-400/60 shadow-[0_0_4px_2px_rgba(34,211,238,0.4)]" />
255 </div>
256
257 {/* 星球核心 */}
258 <div className="absolute inset-8 rounded-full bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 border border-blue-200/30 dark:border-blue-700/20 shadow-md">
259 <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-transparent via-white/30 to-transparent animate-[spin_20s_linear_infinite]" />
260 </div>
261 <div className="absolute inset-0 flex items-center justify-center">
262 <span className="text-foreground font-bold text-sm">
263 {t('ecosystem.creator')}
264 </span>
265 </div>
266 </div>
267 </div>
268
269 {/* 功能节点网格 */}
270 <div className="grid grid-cols-2 gap-2">
271 {nodeOrder.map(key => (
272 <MobileFunctionNode
273 key={key}
274 label={t(`ecosystem.${key}` as any)}
275 isActive={activeNode === key}
276 onClick={() => handleNodeClick(key)}
277 className={key === 'dashboard' ? 'col-span-2' : ''}
278 />
279 ))}
280 </div>
281
282 {/* 内容交易 */}
283 <div className="flex justify-center items-center gap-4">
284 <span className="text-muted-foreground text-xs">
285 {t('ecosystem.sell')}
286 {' '}
287
288 </span>
289 <div className="relative px-4 py-2 rounded-full bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 border border-blue-200/50 dark:border-blue-700/30 shadow-sm">
290 <span className="text-foreground font-medium text-sm">
291 {t('ecosystem.trade')}
292 </span>
293 </div>
294 <span className="text-muted-foreground text-xs">
295
296 {t('ecosystem.buy')}
297 </span>
298 </div>
299
300 {/* 内容需求者 - 小行星风格 */}
301 <div className="flex justify-center">
302 <div className="relative w-24 h-24">
303 {/* 光晕 */}
304 <div className="absolute inset-[-6px] rounded-full bg-gradient-to-br from-purple-500/10 via-pink-500/10 to-orange-500/10 blur-lg animate-pulse" />
305
306 {/* 轨道 */}
307 <div className="absolute inset-[-4px] rounded-full border border-dashed border-purple-300/30 animate-[spin_30s_linear_infinite]">
308 <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-orange-400/60" />
309 </div>
310
311 {/* 星球 */}
312 <div className="absolute inset-2 rounded-full bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/30 dark:to-pink-900/30 border border-purple-200/30 dark:border-purple-700/20 shadow-md flex items-center justify-center">
313 <span className="text-foreground font-bold text-xs text-center px-2">
314 {t('ecosystem.consumer')}
315 </span>
316 </div>
317 </div>
318 </div>
319 </div>
320 </div>
321
322 {/* 底部详情展示区域 - 固定高度,宽度与上方内容对齐 */}
323 <div className="relative z-10 px-6 md:px-12 pb-8 max-w-4xl mx-auto">
324 <DetailPanel
325 activeNode={activeNode}
326 details={activeNode ? getNodeDetails(activeNode) : []}
327 nodeLabel={activeNode ? t(`ecosystem.${activeNode}` as any) : ''}
328 />
329 </div>
330 </div>
331 </div>
332
333 {/* 全局样式 - 自定义动画 */}
334 <style jsx>
335 {`
336 @keyframes twinkle {
337 0%,
338 100% {
339 opacity: 0.3;
340 transform: scale(1);
341 }
342 50% {
343 opacity: 1;
344 transform: scale(1.2);
345 }
346 }
347 @keyframes energyFlow {
348 0% {
349 transform: translateX(0) translateY(-50%);
350 opacity: 0;
351 }
352 50% {
353 opacity: 1;
354 }
355 100% {
356 transform: translateX(20px) translateY(-50%);
357 opacity: 0;
358 }
359 }
360 @keyframes energyFlowReverse {
361 0% {
362 transform: translateX(0) translateY(-50%);
363 opacity: 0;
364 }
365 50% {
366 opacity: 1;
367 }
368 100% {
369 transform: translateX(-20px) translateY(-50%);
370 opacity: 0;
371 }
372 }
373 `}
374 </style>
375 </section>
376 )
377 }
378
379 /** 功能节点组件 - 使用角度和半径计算位置 */
380 interface FunctionNodeProps {
381 nodeKey: FunctionKey
382 label: string
383 /** 角度(度数),0° 为右侧,顺时针方向 */
384 angle: number
385 /** 半径百分比(相对于容器尺寸) */
386 radius: number
387 isActive: boolean
388 onClick: () => void
389 }
390
391 function FunctionNode({ label, angle, radius, isActive, onClick }: FunctionNodeProps) {
392 // 将角度转换为弧度,计算位置
393 const radian = (angle * Math.PI) / 180
394 const x = 50 + radius * Math.cos(radian) // 50% 是中心点
395 const y = 50 + radius * Math.sin(radian)
396
397 return (
398 <button
399 type="button"
400 className={cn(
401 'absolute cursor-pointer z-20',
402 // 居中定位节点
403 '-translate-x-1/2 -translate-y-1/2',
404 )}
405 style={{
406 left: `${x}%`,
407 top: `${y}%`,
408 }}
409 onClick={onClick}
410 >
411 <div
412 className={cn(
413 'px-5 py-2.5 rounded-full shadow-md border transition-all duration-300',
414 isActive
415 ? 'bg-gradient-back border-transparent text-gradient-foreground scale-110 shadow-lg shadow-primary/30'
416 : 'bg-white dark:bg-background border-border text-foreground hover:scale-105 hover:border-primary/50 hover:shadow-lg',
417 )}
418 >
419 <span className="font-medium text-sm whitespace-nowrap">{label}</span>
420 </div>
421 </button>
422 )
423 }
424
425 /** 移动端功能节点 */
426 function MobileFunctionNode({
427 label,
428 isActive,
429 onClick,
430 className,
431 }: {
432 label: string
433 isActive: boolean
434 onClick: () => void
435 className?: string
436 }) {
437 return (
438 <button
439 type="button"
440 className={cn(
441 'px-4 py-2.5 rounded-full shadow-sm border text-center transition-all duration-300 cursor-pointer',
442 isActive
443 ? 'bg-gradient-back border-transparent text-gradient-foreground shadow-primary/20'
444 : 'bg-white dark:bg-background border-border text-foreground hover:border-primary/50',
445 className,
446 )}
447 onClick={onClick}
448 >
449 <span className="font-medium text-sm">{label}</span>
450 </button>
451 )
452 }
453
454 /** 详情面板组件 - 固定高度避免页面抖动 */
455 function DetailPanel({
456 activeNode,
457 details,
458 nodeLabel,
459 }: {
460 activeNode: FunctionKey | null
461 details: string[]
462 nodeLabel: string
463 }) {
464 const hasContent = activeNode && details.length > 0
465
466 return (
467 <div
468 className={cn(
469 'rounded-2xl transition-all duration-500 ease-out',
470 // 固定高度,避免内容切换时高度变化导致抖动
471 'h-[180px] md:h-[160px]',
472 hasContent
473 ? 'bg-white/95 dark:bg-background/95 backdrop-blur-md p-4 md:p-6'
474 : 'bg-transparent',
475 )}
476 >
477 {hasContent ? (
478 <div className="h-full flex flex-col">
479 {/* 标题 */}
480 <div className="flex items-center gap-2 mb-3 shrink-0">
481 <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
482 <h3 className="font-semibold text-foreground">{nodeLabel}</h3>
483 </div>
484
485 {/* 详情列表 - 固定高度区域 */}
486 <div
487 className={cn(
488 'h-[220px] ',
489 'scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent',
490 )}
491 >
492 {details.map((detail, index) => (
493 <div key={`${activeNode}-${index}`} className="flex items-start gap-2 text-sm">
494 <span className="text-primary shrink-0 mt-0.5">•</span>
495 <span className="text-muted-foreground leading-relaxed">{detail}</span>
496 </div>
497 ))}
498 </div>
499
500 {/* 进度指示器 */}
501 <div className="flex justify-center gap-1.5 mt-4 shrink-0">
502 {(['publish', 'create', 'manage', 'analyze', 'dashboard'] as FunctionKey[]).map(
503 key => (
504 <div
505 key={key}
506 className={cn(
507 'h-2 rounded-full transition-all duration-300',
508 activeNode === key ? 'bg-primary w-6' : 'bg-muted w-2',
509 )}
510 />
511 ),
512 )}
513 </div>
514 </div>
515 ) : (
516 // 占位内容,保持高度一致
517 <div className="flex items-center justify-center h-full">
518 <div className="flex justify-center gap-1.5">
519 {(['publish', 'create', 'manage', 'analyze', 'dashboard'] as FunctionKey[]).map(
520 key => (
521 <div
522 key={key}
523 className="h-2 w-2 rounded-full bg-muted transition-all duration-300"
524 />
525 ),
526 )}
527 </div>
528 </div>
529 )}
530 </div>
531 )
532 }
533
534 export default EcosystemDiagram
535
535 lines Plain Text