@@ -14,6 +14,8 @@ import KLineChart, {
1414} from './components/Dashboard/Chart' ;
1515import ChartToolbar from './components/Dashboard/Chart/ChartToolbar' ;
1616import { 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
5759function 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