mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 23:36:55 +08:00
feat(web): 落地页“启动输出”加入打字机效果(Typewriter)
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CryptoFeatureCard } from './CryptoFeatureCard'
|
import { CryptoFeatureCard } from './CryptoFeatureCard'
|
||||||
|
import Typewriter from './Typewriter'
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
@@ -515,14 +516,19 @@ export function LandingPage() {
|
|||||||
boxShadow: '0 20px 60px rgba(240, 185, 11, 0.2)',
|
boxShadow: '0 20px 60px rgba(240, 185, 11, 0.2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className='text-sm font-mono' style={{ color: '#0ECB81' }}>
|
<Typewriter
|
||||||
{`$ git clone https://github.com/...
|
lines={[
|
||||||
$ docker compose up -d
|
'$ git clone https://github.com/tinkle-community/nofx',
|
||||||
🚀 NOFX 已启动
|
'$ docker compose up -d',
|
||||||
✓ AI 代理运行中
|
'🚀 NOFX 已启动',
|
||||||
✓ 交易所已连接
|
'✓ AI 代理运行中',
|
||||||
✓ 策略已激活`}
|
'✓ 交易所已连接',
|
||||||
</pre>
|
'✓ 策略已激活',
|
||||||
|
]}
|
||||||
|
typingSpeed={24}
|
||||||
|
lineDelay={350}
|
||||||
|
className='text-sm font-mono'
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
63
web/src/components/Typewriter.tsx
Normal file
63
web/src/components/Typewriter.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user