| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- import React, { useRef, useState, useCallback, useEffect } from 'react';
- import { createRoot } from 'react-dom/client';
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
- import { useStockSocket } from './hooks/useStockSocketClient.ts';
- import { RouterProvider, createBrowserRouter, useSearchParams } from 'react-router';
- import { ToastContainer , 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";
- const queryClient = new QueryClient();
- function StockApp() {
- const chartRef = useRef<StockChartRef>(null);
- const [searchParams] = useSearchParams();
- const {
- connect,
- disconnect,
- subscribe,
- unsubscribe,
- currentPrice,
- lastUpdate,
- error,
- isConnected
- } = useStockSocket();
- const codeFromUrl = searchParams.get('code');
- const [stockCode, setStockCode] = useState(codeFromUrl || '001339' || undefined);//
- // const classroom = searchParams.get('classroom');
- // const { connected } = useSocketRoom(classroom);
- // const { sendNextQuestion } = useQuestionManagement(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) {
- updateCurrentDate(nextDate);
- }
- }, [moveToNextDay, updateCurrentDate]);
- // useEffect(() => {
- // const currentDate = profitSummary.dailyStats.date;
- // if (classroom && connected && currentDate && lastSentDateRef.current !== currentDate) {
- // lastSentDateRef.current = currentDate;
- // sendNextQuestion({
- // date: currentDate,
- // price: profitSummary.dailyStats.close
- // }).catch(() => {
- // message.error('发送题目失败');
- // });
- // }
- // }, [classroom, connected, profitSummary.dailyStats, sendNextQuestion]);
- 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();
- }, []);
- // 管理socket连接生命周期
- useEffect(() => {
- connect();
- if (stockCode) {
- subscribe(stockCode);
- }
- return () => {
- if (stockCode) {
- unsubscribe(stockCode);
- }
- disconnect();
- };
- }, [connect, disconnect, subscribe, unsubscribe, stockCode]);
- // 同步socket数据到组件状态
- useEffect(() => {
- if (currentPrice && profitSummary) {
- profitSummary.dailyStats.close = currentPrice;
- }
- }, [currentPrice, profitSummary]);
- // 错误处理
- 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 && (
- <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>
- );
- }
- const router = createBrowserRouter([
- {
- path: '*',
- element: <StockApp />,
- },
- ]);
- // 渲染应用
- const root = createRoot(document.getElementById('root') as HTMLElement);
- root.render(
- <QueryClientProvider client={queryClient}>
- <RouterProvider router={router} />
- <ToastContainer
- position="top-right"
- autoClose={500}
- hideProgressBar={false}
- newestOnTop={false}
- closeOnClick
- rtl={false}
- pauseOnFocusLoss
- draggable
- pauseOnHover
- />
- </QueryClientProvider>
- );
|