| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import React, { useRef, useState, useCallback, useEffect } from 'react';
- import { useStockSocket } from './hooks/useStockSocketClient.ts';
- import { useSearchParams } from 'react-router';
- import { toast} from 'react-toastify';
- import { StockChart, MemoToggle, TradePanel, useTradeRecords, useStockQueries, useProfitCalculator, ProfitDisplay, useStockDataFilter, DrawingToolbar } from './components/stock-chart/mod.ts';
- import type { StockChartRef } from './components/stock-chart/mod.ts';
- import { ActiveType } from "./components/stock-chart/src/types/index.ts";
- export function StockMain() {
- const chartRef = useRef<StockChartRef>(null);
- const [searchParams] = useSearchParams();
- const codeFromUrl = searchParams.get('code');
- const [stockCode, setStockCode] = useState(codeFromUrl || undefined);//|| '001339'
- const classroom = searchParams.get('classroom');
- const {
- pushExamData,
- error,
- isConnected
- } = useStockSocket(classroom);
-
- const {
- stockData: fullStockData,
- memoData,
- fetchData ,
- } = useStockQueries(stockCode);
- const {
- filteredData: stockData,
- moveToNextDay,
- setDayNum,
- initializeView,
- isInitialized
- } = useStockDataFilter(fullStockData);
- const {
- trades,
- toggleTrade,
- hasBought
- } = useTradeRecords(stockData);
- const { profitSummary, updateCurrentDate } = useProfitCalculator(
- stockData,
- trades
- );
- const handleNextDay = useCallback(async () => {
- const nextDate = await moveToNextDay();
- if (nextDate && profitSummary?.dailyStats) {
- updateCurrentDate(nextDate);
- }
- }, [moveToNextDay, updateCurrentDate, profitSummary, pushExamData]);
- useEffect(() => {
- const currentDate = profitSummary.dailyStats.date;
- if (classroom && isConnected && currentDate ) {
- pushExamData({
- roomId: classroom,
- question: {
- date: currentDate,
- price: profitSummary.dailyStats.close
- }
- });
- }
- }, [classroom, isConnected, profitSummary.dailyStats ]);
- const handleDayNumChange = useCallback((days: number) => {
- if (!isInitialized) {
- initializeView();
- } else {
- setDayNum(days);
- }
- }, [isInitialized, initializeView, setDayNum]);
- const handleQuery = useCallback(() => {
- if(!stockCode){
- toast.error('请先输入股票代码')
- return;
- }
- if (stockCode && stockCode.trim()) {
- fetchData().then(() => {
- initializeView();
- });
- }
- }, [stockCode, fetchData, initializeView]);
- const handleStartDrawing = useCallback((type: ActiveType) => {
- chartRef.current?.startDrawing(type);
- }, []);
- const handleStopDrawing = useCallback(() => {
- chartRef.current?.stopDrawing();
- }, []);
- const handleClearLines = useCallback(() => {
- chartRef.current?.clearDrawings();
- }, []);
- // 错误处理
- useEffect(() => {
- if (error) {
- toast.error(`Socket错误: ${error.message}`);
- }
- }, [error]);
- useEffect(() => {
- const handleKeyPress = (event: KeyboardEvent) => {
- switch(event.key.toLowerCase()) {
- case 'b':
- if (!hasBought) toggleTrade('BUY');
- break;
- case 's':
- if (hasBought) toggleTrade('SELL');
- break;
- case 'arrowright':
- handleNextDay();
- break;
- }
- };
- globalThis.addEventListener('keydown', handleKeyPress);
- return () => globalThis.removeEventListener('keydown', handleKeyPress);
- }, [hasBought, toggleTrade, handleNextDay]);
- useEffect(() => {
- if (codeFromUrl && codeFromUrl !== stockCode) {
- setStockCode(codeFromUrl);
- fetchData().then(() => {
- initializeView();
- });
- }
- }, [codeFromUrl, stockCode, fetchData, initializeView]);
- // if (isLoading) {
- // return (
- // <div className="flex items-center justify-center h-screen bg-gray-900">
- // <div className="text-white text-xl">加载中...</div>
- // </div>
- // );
- // }
- // if (error) {
- // return (
- // <div className="flex items-center justify-center h-screen bg-gray-900">
- // <div className="text-red-500 text-xl">错误: {error.message}</div>
- // </div>
- // );
- // }
- return (
- <div className="flex flex-col h-screen bg-gray-900">
- {!isConnected && classroom && (
- <div className="bg-yellow-600 text-white text-center py-1 text-sm">
- 正在尝试连接答题卡服务...
- </div>
- )}
- {/* 顶部行情和收益信息 */}
- <ProfitDisplay profitSummary={profitSummary} />
- {/* 主图表区域 */}
- <div className="flex-1 relative overflow-hidden">
- <StockChart
- ref={chartRef}
- stockData={stockData}
- memoData={memoData}
- trades={trades}
- />
-
- {/* 添加画线工具栏 */}
- <DrawingToolbar
- className="absolute top-4 right-4"
- onStartDrawing={handleStartDrawing}
- onStopDrawing={handleStopDrawing}
- onClearLines={handleClearLines}
- />
- </div>
- {/* 底部控制面板 */}
- <div className="flex items-center justify-between p-4 bg-gray-800 border-t border-gray-700">
- {/* 左侧区域 */}
- <div className="flex items-center space-x-6">
- {/* 查询输入框 */}
- <div className="flex items-center space-x-2">
- <input
- type="text"
- value={stockCode}
- onChange={(e) => setStockCode(e.target.value)}
- placeholder="输入股票代码"
- className="px-3 py-2 text-sm bg-gray-700 text-white rounded-md border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
- />
- <button
- onClick={handleQuery}
- className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
- >
- 查询
- </button>
- </div>
- {/* 交易面板 */}
- <TradePanel
- hasBought={hasBought}
- onToggleTrade={toggleTrade}
- />
- {/* 下一天按钮 */}
- <div className="flex items-center space-x-2">
- <button
- onClick={handleNextDay}
- disabled={!stockData.length}
- className={`px-4 py-2 text-sm font-medium text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors
- ${!stockData.length
- ? 'bg-gray-600 cursor-not-allowed'
- : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'}`}
- >
- 下一天
- </button>
- <span className="text-gray-400 text-xs">→</span>
- </div>
- {/* 天数快捷按钮组 */}
- <div className="flex items-center space-x-2">
- {[120, 30, 60].map((days) => (
- <button
- key={days}
- onClick={() => handleDayNumChange(days)}
- className="px-3 py-1 text-sm font-medium text-white bg-gray-700 rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
- >
- {days}天
- </button>
- ))}
- </div>
- </div>
- {/* 右侧快捷键说明和能按钮 */}
- <div className="flex items-center space-x-8">
- <div className="text-xs text-gray-400 leading-relaxed">
- <div>快捷键:</div>
- <div>B - 买入</div>
- <div>S - 卖出</div>
- {/* <div>ESC - 取消</div> */}
- <div>→ - 下一天</div>
- </div>
- <MemoToggle
- onToggle={(visible: boolean) => {
- chartRef.current?.toggleMemoVisibility(visible);
- }}
- className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors whitespace-nowrap"
- />
- </div>
- </div>
- </div>
- );
- }
|