flowchart TD
Sub[WebSocket 订阅<br/>solana_account_sub] --> Event[Account/Slot 变动事件]
Event --> Act[查询 RPC 获取最新数据]
Act --> Opt{数据有变更?}
Opt -->|是| Anim[React 动画过渡<br/>react-spring / GSAP]
Anim --> UI[更新展示组件<br/>Chart/表格]
Opt -->|否| Idle[丢弃更新]
UI --> Sub
style Anim fill:#ffe0b2
style UI fill:#c8e6c9
数据变化需要视觉反馈。投票后条形图应平滑增长,票数应闪烁更新。
17.7.1 React 动画方案
tsx
// CSS 过渡动画
// components/VoteBar.tsx
import { useEffect, useState } from 'react';
export const VoteBar = ({ current, previous, label }: { current: number; previous: number; label: string; }) => {
const [display, setDisplay] = useState(previous);
// 数字递增动画
useEffect(() => {
if (current <= display) { setDisplay(current); return; }
const step = Math.max(1, Math.ceil((current - display) / 20));
const timer = setInterval(() => {
setDisplay(d => {
if (d >= current) { clearInterval(timer); return d; }
return Math.min(d + step, current);
});
}, 50);
return () => clearInterval(timer);
}, [current]);
return (
<div className="vote-bar">
<div className="label">{label}</div>
<div className="bar-container">
<div className="bar-fill" style={{
width: `${display}%`,
transition: 'width 1s ease-out',
}} />
</div>
<div className="count">{display} 票</div>
</div>
);
};17.7.2 事件监听(WebSocket 替代轮询)
typescript
// 使用 Solana 的 WebSocket 订阅
import { Connection, clusterApiUrl, PublicKey } from '@solana/web3.js';
const wsConnection = new Connection(clusterApiUrl('devnet'), 'confirmed');
export function subscribeToPoll(pollAddress: string, onUpdate: (data: any) => void) {
const pubKey = new PublicKey(pollAddress);
// 订阅账户变化
const subId = wsConnection.onAccountChange(pubKey, (accountInfo) => {
// 反序列化数据
onUpdate(accountInfo.data);
});
return () => wsConnection.removeAccountChangeListener(subId);
}17.7.3 动画 UX 原则
| 动画 | 时长 | 缓动 |
|---|---|---|
| 票数变化 | 300ms | ease-out |
| 条形增长 | 1000ms | cubic-bezier(0.4, 0, 0.2, 1) |
| 新投票出现 | 500ms | slide-down + fade |
| 投票成功 | 800ms | scale(1.05) → 1.0 |
, 前往 → 17.8 测试 |*
评论
0评论加载中…