Skip to content

Commit 8ec5a00

Browse files
committed
refactor: 引入 Zustand 状态管理,优化数据源切换体验与骨架屏反馈
- **新增 Zustand 依赖**: - 安装 `zustand@^4.5.2`,用于全局 UI 状态管理 - 新增 `useUiStore` Hook,管理数据源切换状态 (`isSwitching`) - **优化数据源切换逻辑**: - 新增 `handleDataSourceChange` 处理函数,切换时自动设置 `isSwitching` 状态 - 切换完成条件:历史数据就绪 + (MOCK 模式 或 LIVE 已连接/有数据) - 增加最小切换时长 (300ms)
1 parent f0f69d9 commit 8ec5a00

10 files changed

Lines changed: 527 additions & 150 deletions

File tree

package-lock.json

Lines changed: 42 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"lightweight-charts": "^5.1.0",
2020
"lucide-react": "^0.562.0",
2121
"react": "^18.3.1",
22-
"react-dom": "^18.3.1"
22+
"react-dom": "^18.3.1",
23+
"zustand": "^4.5.2"
2324
},
2425
"devDependencies": {
2526
"@eslint/js": "^9.17.0",
@@ -39,4 +40,4 @@
3940
"vite-plugin-top-level-await": "^1.4.4",
4041
"vite-plugin-wasm": "^3.3.0"
4142
}
42-
}
43+
}

src/App.tsx

Lines changed: 157 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import KLineChart, {
1414
} from './components/Dashboard/Chart';
1515
import ChartToolbar from './components/Dashboard/Chart/ChartToolbar';
1616
import { TradeForm, MobileTradebar } from './components/Dashboard/Trade';
17+
import { useUiStore } from './hooks/ui/useUiStore';
18+
import type { UiState } from './hooks/ui/useUiStore';
1719

1820
/* ============================================
1921
Constants
@@ -39,13 +41,13 @@ function getInitialDataSource(): DataSource {
3941
if (urlSource === 'binance' || urlSource === 'mock') {
4042
return urlSource;
4143
}
42-
44+
4345
// 检查 localStorage
4446
const savedSource = localStorage.getItem(DATA_SOURCE_KEY);
4547
if (savedSource === 'binance' || savedSource === 'mock') {
4648
return savedSource;
4749
}
48-
50+
4951
// 默认使用模拟数据
5052
return 'mock';
5153
}
@@ -56,8 +58,15 @@ function getInitialDataSource(): DataSource {
5658

5759
function App() {
5860
// ========== 数据源状态 ==========
59-
const [dataSource, setDataSource] = useState<DataSource>(getInitialDataSource);
60-
61+
const [dataSource, setDataSource] =
62+
useState<DataSource>(getInitialDataSource);
63+
const isSwitching = useUiStore((s: UiState) => s.isSwitching);
64+
const setSwitching = useUiStore((s: UiState) => s.setSwitching);
65+
66+
const MIN_SWITCH_MS = 300;
67+
const [switchVisible, setSwitchVisible] = useState<boolean>(false);
68+
const switchStartRef = useRef<number | null>(null);
69+
6170
// 持久化数据源偏好
6271
useEffect(() => {
6372
localStorage.setItem(DATA_SOURCE_KEY, dataSource);
@@ -77,14 +86,15 @@ function App() {
7786
currentLiveCandle,
7887
indicatorData,
7988
currentTimeframe,
89+
historyReady,
90+
historyLoading,
8091

8192
// 数据流控制
8293
isRunning,
8394
priceTrend,
8495
priceColorClass,
8596
toggleFeed,
8697
setTimeframe,
87-
dataSource: engineDataSource,
8898
connectionStatus,
8999

90100
// 交易状态 (Rust 管理)
@@ -103,9 +113,67 @@ function App() {
103113
} = useWasmEngine({
104114
tickInterval: 100,
105115
dataSource,
106-
historyCount: 5000, // 获取更多历史数据(5000 根 1m K 线 ≈ 3.5 天,足够计算所有指标)
116+
historyCount: 1440, // 首屏优化:1440 根 1m K 线 ≈ 1 天,显著提速至“秒开”,指标计算仍充足
107117
});
108118

119+
// ========== 数据源切换处理 ==========
120+
const handleDataSourceChange = useCallback(
121+
(source: DataSource) => {
122+
if (source !== dataSource) {
123+
setSwitching(true);
124+
setDataSource(source);
125+
}
126+
},
127+
[dataSource, setSwitching],
128+
);
129+
130+
// 切换完成条件:历史数据就绪,且(MOCK)或(LIVE 已连接或已有最新数据)
131+
useEffect(() => {
132+
if (!isSwitching) return;
133+
if (
134+
historyReady &&
135+
(dataSource === 'mock' ||
136+
connectionStatus === 'connected' ||
137+
!!latestData)
138+
) {
139+
setSwitching(false);
140+
}
141+
}, [
142+
isSwitching,
143+
historyReady,
144+
dataSource,
145+
connectionStatus,
146+
latestData,
147+
setSwitching,
148+
]);
149+
150+
// TODO: AI待确认: 增强移动端触觉反馈(振动),可提供设置开关
151+
useEffect(() => {
152+
try {
153+
if (isSwitching) {
154+
(navigator as any)?.vibrate?.(30);
155+
} else {
156+
(navigator as any)?.vibrate?.(20);
157+
}
158+
} catch {}
159+
}, [isSwitching]);
160+
161+
useEffect(() => {
162+
if (isSwitching) {
163+
switchStartRef.current = performance.now();
164+
setSwitchVisible(true);
165+
return;
166+
}
167+
const started = switchStartRef.current ?? performance.now();
168+
const elapsed = performance.now() - started;
169+
const remaining = Math.max(0, MIN_SWITCH_MS - elapsed);
170+
const t = setTimeout(() => {
171+
setSwitchVisible(false);
172+
switchStartRef.current = null;
173+
}, remaining);
174+
return () => clearTimeout(t);
175+
}, [isSwitching]);
176+
109177
// ========== 图表引用 ==========
110178
const chartRef = useRef<KLineChartHandle>(null);
111179

@@ -127,6 +195,8 @@ function App() {
127195
'TradingView' | 'Depth'
128196
>('TradingView');
129197

198+
// TODO: AI待确认: 考虑将 isSwitching 抽离到 Zustand 全局 UI 状态,统一管理全局 UI 反馈
199+
130200
/**
131201
* 切换时间周期
132202
* 同步更新 UI 状态和 Rust 引擎
@@ -215,7 +285,7 @@ function App() {
215285
// Main Layout: Mobile-First Responsive Futures Terminal
216286
// 断点策略: Mobile (<768) | Tablet (768-1280) | Desktop (1280+) | 4K (2560+)
217287
return (
218-
<div className="h-screen w-screen bg-[var(--color-bg-surface-alt)] flex flex-col overflow-hidden">
288+
<div className="h-screen w-screen bg-bg-surface-alt flex flex-col overflow-hidden">
219289
<Header
220290
isRunning={isRunning}
221291
onToggle={dataSource === 'mock' ? toggleFeed : undefined} // LIVE 模式下禁用切换
@@ -224,8 +294,9 @@ function App() {
224294
priceTrend={priceTrend}
225295
priceColorClass={priceColorClass}
226296
dataSource={dataSource}
227-
onDataSourceChange={setDataSource}
297+
onDataSourceChange={handleDataSourceChange}
228298
connectionStatus={connectionStatus}
299+
isSwitching={isSwitching}
229300
/>
230301

231302
{/* ========== 主内容区域 ========== */}
@@ -255,7 +326,7 @@ function App() {
255326
className={`
256327
flex flex-col
257328
md:grid md:h-full
258-
gap-px bg-[var(--color-border-dark)]
329+
gap-px bg-border-dark
259330
${
260331
dataSource === 'mock'
261332
? 'md:grid-cols-[1fr_240px] xl:grid-cols-[1fr_240px_300px]'
@@ -279,7 +350,7 @@ function App() {
279350
{/* K-Line Chart Area - 移动端图表占高度*/}
280351
<div className="flex flex-col h-[60vh] md:flex-1 md:h-auto min-h-0">
281352
{/* Chart Sub-Header */}
282-
<div className="shrink-0 h-7 md:h-8 px-2 md:px-3 flex items-center justify-between border-b border-[var(--color-border-dark)] bg-[var(--color-bg-black)]">
353+
<div className="shrink-0 h-7 md:h-8 px-2 md:px-3 flex items-center justify-between border-b border-border-dark bg-bg-black">
283354
<div className="flex items-center gap-2 md:gap-3">
284355
<h2 className="text-[10px] md:text-[11px] font-medium text-gray-400 truncate max-w-[120px] md:max-w-none">
285356
{latestData?.symbol ?? 'BTC-USDT'} · Perp
@@ -290,15 +361,29 @@ function App() {
290361
</div>
291362
<div className="hidden sm:flex items-center gap-2 text-[10px] font-mono text-gray-500">
292363
<span className="flex items-center gap-1">
293-
<span className="w-2 h-2 bg-[var(--color-success)] rounded-sm" />
294-
<span className="w-2 h-2 bg-[var(--color-danger)] rounded-sm" />
364+
<span className="w-2 h-2 bg-success rounded-sm" />
365+
<span className="w-2 h-2 bg-danger rounded-sm" />
295366
</span>
296367
<span>OHLC</span>
297368
</div>
298369
</div>
299370

300371
{/* Chart Body */}
301-
<div className="flex-1 min-h-0 overflow-hidden">
372+
<div className="flex-1 min-h-0 overflow-hidden relative">
373+
{/* TODO: AI待确认: 完善图表区域 Skeleton(骨架屏样式/密度/渐变),支持不同布局 */}
374+
{/* TODO: AI待确认: 增强移动端切换视觉反馈(震动/Toast/顶部提示条),与桌面端一致 */}
375+
{switchVisible && (
376+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-bg-black/60 backdrop-blur-[1px]">
377+
<div className="flex flex-col items-center gap-2">
378+
<span className="w-6 h-6 border-2 border-border-dark border-t-success rounded-full animate-spin" />
379+
<span className="text-[11px] font-mono text-gray-400">
380+
{dataSource === 'binance'
381+
? 'LIVE 切换中…'
382+
: 'MOCK 切换中…'}
383+
</span>
384+
</div>
385+
</div>
386+
)}
302387
<KLineChart
303388
ref={chartRef}
304389
candleHistory={candleHistory}
@@ -323,18 +408,67 @@ function App() {
323408
{/* MOCK 模式: 固定宽度 240px | LIVE 模式: 自动宽度占满剩余空间 */}
324409
<section
325410
className={`
326-
h-[214px] md:h-full min-h-0 bg-terminal-bg border-t md:border-t-0 md:border-l border-[var(--color-border-dark)]
327-
${dataSource === 'binance' ? 'xl:min-w-[200px]' : ''}
411+
h-[214px] md:h-full min-h-0 bg-terminal-bg border-t md:border-t-0 md:border-l border-border-dark
412+
${dataSource === 'binance' ? 'xl:min-w-[320px]' : ''}
328413
`}
329414
>
330-
<OrderBook
331-
bids={latestData?.bids ?? []}
332-
asks={latestData?.asks ?? []}
333-
price={latestData?.price}
334-
priceTrend={priceTrend}
335-
priceColorClass={priceColorClass}
336-
timestamp={latestData?.timestamp}
337-
/>
415+
<div className="relative h-full">
416+
{/* 订单簿:切换/加载时使用骨架屏,不与图表遮罩重复 */}
417+
{switchVisible || historyLoading ? (
418+
<div className="h-full flex flex-col">
419+
{/* 表头占位 */}
420+
<div className="shrink-0 h-7 md:h-8 px-2 md:px-3 border-b border-border-dark bg-bg-black" />
421+
{/* 买/卖两块骨架 */}
422+
<div className="flex-1 grid grid-rows-2 gap-px bg-border-dark">
423+
{/* 卖单区骨架(底部对齐,reverse 视觉) */}
424+
<div className="bg-terminal-bg p-2 md:p-3">
425+
<div className="h-full flex flex-col justify-end">
426+
<div className="space-y-1 animate-pulse">
427+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
428+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
429+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
430+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
431+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
432+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
433+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
434+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
435+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
436+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
437+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
438+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
439+
</div>
440+
</div>
441+
</div>
442+
{/* 买单区骨架(顶部对齐) */}
443+
<div className="bg-terminal-bg p-2 md:p-3">
444+
<div className="space-y-1 animate-pulse">
445+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
446+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
447+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
448+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
449+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
450+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
451+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
452+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
453+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
454+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
455+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
456+
<div className="h-3 bg-gradient-to-r from-gray-700/40 via-gray-600/30 to-gray-700/40 rounded" />
457+
</div>
458+
</div>
459+
</div>
460+
</div>
461+
) : (
462+
<OrderBook
463+
bids={latestData?.bids ?? []}
464+
asks={latestData?.asks ?? []}
465+
price={latestData?.price}
466+
priceTrend={priceTrend}
467+
priceColorClass={priceColorClass}
468+
timestamp={latestData?.timestamp}
469+
/>
470+
)}
471+
</div>
338472
</section>
339473

340474
{/* ========== 交易表单区域 (仅 MOCK 模式下显示) ========== */}

0 commit comments

Comments
 (0)