feat(web): 落地页“启动输出”加入打字机效果(Typewriter)

This commit is contained in:
Ember
2025-11-01 23:45:27 +08:00
parent e747e449da
commit daba1bc113
2 changed files with 77 additions and 8 deletions
+14 -8
View File
@@ -15,6 +15,7 @@ import {
Cpu,
} from 'lucide-react'
import { CryptoFeatureCard } from './CryptoFeatureCard'
import Typewriter from './Typewriter'
// Animation variants
const fadeInUp = {
@@ -515,14 +516,19 @@ export function LandingPage() {
boxShadow: '0 20px 60px rgba(240, 185, 11, 0.2)',
}}
>
<pre className='text-sm font-mono' style={{ color: '#0ECB81' }}>
{`$ git clone https://github.com/...
$ docker compose up -d
🚀 NOFX 已启动
✓ AI 代理运行中
✓ 交易所已连接
✓ 策略已激活`}
</pre>
<Typewriter
lines={[
'$ git clone https://github.com/tinkle-community/nofx',
'$ docker compose up -d',
'🚀 NOFX 已启动',
'✓ AI 代理运行中',
'✓ 交易所已连接',
'✓ 策略已激活',
]}
typingSpeed={24}
lineDelay={350}
className='text-sm font-mono'
/>
</motion.div>
</motion.div>
</div>
+63
View File
@@ -0,0 +1,63 @@
import { useEffect, useRef, useState } from 'react'
interface TypewriterProps {
lines: string[]
typingSpeed?: number // 毫秒/字符
lineDelay?: number // 每行结束的额外等待
className?: string
}
export default function Typewriter({
lines,
typingSpeed = 25,
lineDelay = 400,
className,
}: TypewriterProps) {
const [text, setText] = useState('')
const [showCursor, setShowCursor] = useState(true)
const lineIndexRef = useRef(0)
const charIndexRef = useRef(0)
const timerRef = useRef<number | null>(null)
const blinkRef = useRef<number | null>(null)
useEffect(() => {
function typeNext() {
const currentLine = lines[lineIndexRef.current] ?? ''
if (charIndexRef.current < currentLine.length) {
setText((t) => t + currentLine[charIndexRef.current])
charIndexRef.current += 1
timerRef.current = window.setTimeout(typeNext, typingSpeed)
} else {
// 行结束
if (lineIndexRef.current < lines.length - 1) {
setText((t) => t + '\n')
lineIndexRef.current += 1
charIndexRef.current = 0
timerRef.current = window.setTimeout(typeNext, lineDelay)
} else {
// 最后一行输入完毕
timerRef.current = null
}
}
}
typeNext()
// 光标闪烁
blinkRef.current = window.setInterval(() => {
setShowCursor((v) => !v)
}, 500)
return () => {
if (timerRef.current) window.clearTimeout(timerRef.current)
if (blinkRef.current) window.clearInterval(blinkRef.current)
}
}, [lines, typingSpeed, lineDelay])
return (
<pre className={className}>
{text}
<span style={{ opacity: showCursor ? 1 : 0 }}> </span>
</pre>
)
}