stock_app.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import React, { useRef, useState, useCallback, useEffect } from 'react';
  2. import { createRoot } from 'react-dom/client';
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  4. import { useStockSocket } from './hooks/useStockSocketClient.ts';
  5. import { RouterProvider, createBrowserRouter, useSearchParams } from 'react-router';
  6. import { ToastContainer , toast} from 'react-toastify';
  7. import { StockChart, MemoToggle, TradePanel, useTradeRecords, useStockQueries, useProfitCalculator, ProfitDisplay, useStockDataFilter, DrawingToolbar } from './components/stock-chart/mod.ts';
  8. import type { StockChartRef } from './components/stock-chart/mod.ts';
  9. import { ActiveType } from "./components/stock-chart/src/types/index.ts";
  10. const queryClient = new QueryClient();
  11. function StockApp() {
  12. const chartRef = useRef<StockChartRef>(null);
  13. const [searchParams] = useSearchParams();
  14. const {
  15. connect,
  16. disconnect,
  17. subscribe,
  18. unsubscribe,
  19. currentPrice,
  20. lastUpdate,
  21. error,
  22. isConnected
  23. } = useStockSocket();
  24. const codeFromUrl = searchParams.get('code');
  25. const [stockCode, setStockCode] = useState(codeFromUrl || '001339' || undefined);//
  26. // const classroom = searchParams.get('classroom');
  27. // const { connected } = useSocketRoom(classroom);
  28. // const { sendNextQuestion } = useQuestionManagement(classroom);
  29. const {
  30. stockData: fullStockData,
  31. memoData,
  32. fetchData ,
  33. } = useStockQueries(stockCode);
  34. const {
  35. filteredData: stockData,
  36. moveToNextDay,
  37. setDayNum,
  38. initializeView,
  39. isInitialized
  40. } = useStockDataFilter(fullStockData);
  41. const {
  42. trades,
  43. toggleTrade,
  44. hasBought
  45. } = useTradeRecords(stockData);
  46. const { profitSummary, updateCurrentDate } = useProfitCalculator(
  47. stockData,
  48. trades
  49. );
  50. const handleNextDay = useCallback(async () => {
  51. const nextDate = await moveToNextDay();
  52. if (nextDate) {
  53. updateCurrentDate(nextDate);
  54. }
  55. }, [moveToNextDay, updateCurrentDate]);
  56. // useEffect(() => {
  57. // const currentDate = profitSummary.dailyStats.date;
  58. // if (classroom && connected && currentDate && lastSentDateRef.current !== currentDate) {
  59. // lastSentDateRef.current = currentDate;
  60. // sendNextQuestion({
  61. // date: currentDate,
  62. // price: profitSummary.dailyStats.close
  63. // }).catch(() => {
  64. // message.error('发送题目失败');
  65. // });
  66. // }
  67. // }, [classroom, connected, profitSummary.dailyStats, sendNextQuestion]);
  68. const handleDayNumChange = useCallback((days: number) => {
  69. if (!isInitialized) {
  70. initializeView();
  71. } else {
  72. setDayNum(days);
  73. }
  74. }, [isInitialized, initializeView, setDayNum]);
  75. const handleQuery = useCallback(() => {
  76. if(!stockCode){
  77. toast.error('请先输入股票代码')
  78. return;
  79. }
  80. if (stockCode && stockCode.trim()) {
  81. fetchData().then(() => {
  82. initializeView();
  83. });
  84. }
  85. }, [stockCode, fetchData, initializeView]);
  86. const handleStartDrawing = useCallback((type: ActiveType) => {
  87. chartRef.current?.startDrawing(type);
  88. }, []);
  89. const handleStopDrawing = useCallback(() => {
  90. chartRef.current?.stopDrawing();
  91. }, []);
  92. const handleClearLines = useCallback(() => {
  93. chartRef.current?.clearDrawings();
  94. }, []);
  95. // 管理socket连接生命周期
  96. useEffect(() => {
  97. connect();
  98. if (stockCode) {
  99. subscribe(stockCode);
  100. }
  101. return () => {
  102. if (stockCode) {
  103. unsubscribe(stockCode);
  104. }
  105. disconnect();
  106. };
  107. }, [connect, disconnect, subscribe, unsubscribe, stockCode]);
  108. // 同步socket数据到组件状态
  109. useEffect(() => {
  110. if (currentPrice && profitSummary) {
  111. profitSummary.dailyStats.close = currentPrice;
  112. }
  113. }, [currentPrice, profitSummary]);
  114. // 错误处理
  115. useEffect(() => {
  116. if (error) {
  117. toast.error(`Socket错误: ${error.message}`);
  118. }
  119. }, [error]);
  120. useEffect(() => {
  121. const handleKeyPress = (event: KeyboardEvent) => {
  122. switch(event.key.toLowerCase()) {
  123. case 'b':
  124. if (!hasBought) toggleTrade('BUY');
  125. break;
  126. case 's':
  127. if (hasBought) toggleTrade('SELL');
  128. break;
  129. case 'arrowright':
  130. handleNextDay();
  131. break;
  132. }
  133. };
  134. globalThis.addEventListener('keydown', handleKeyPress);
  135. return () => globalThis.removeEventListener('keydown', handleKeyPress);
  136. }, [hasBought, toggleTrade, handleNextDay]);
  137. useEffect(() => {
  138. if (codeFromUrl && codeFromUrl !== stockCode) {
  139. setStockCode(codeFromUrl);
  140. fetchData().then(() => {
  141. initializeView();
  142. });
  143. }
  144. }, [codeFromUrl, stockCode, fetchData, initializeView]);
  145. // if (isLoading) {
  146. // return (
  147. // <div className="flex items-center justify-center h-screen bg-gray-900">
  148. // <div className="text-white text-xl">加载中...</div>
  149. // </div>
  150. // );
  151. // }
  152. // if (error) {
  153. // return (
  154. // <div className="flex items-center justify-center h-screen bg-gray-900">
  155. // <div className="text-red-500 text-xl">错误: {error.message}</div>
  156. // </div>
  157. // );
  158. // }
  159. return (
  160. <div className="flex flex-col h-screen bg-gray-900">
  161. {!isConnected && (
  162. <div className="bg-yellow-600 text-white text-center py-1 text-sm">
  163. 正在尝试连接行情服务...
  164. </div>
  165. )}
  166. {/* 顶部行情和收益信息 */}
  167. <ProfitDisplay profitSummary={profitSummary} />
  168. {/* 主图表区域 */}
  169. <div className="flex-1 relative overflow-hidden">
  170. <StockChart
  171. ref={chartRef}
  172. stockData={stockData}
  173. memoData={memoData}
  174. trades={trades}
  175. />
  176. {/* 添加画线工具栏 */}
  177. <DrawingToolbar
  178. className="absolute top-4 right-4"
  179. onStartDrawing={handleStartDrawing}
  180. onStopDrawing={handleStopDrawing}
  181. onClearLines={handleClearLines}
  182. />
  183. </div>
  184. {/* 底部控制面板 */}
  185. <div className="flex items-center justify-between p-4 bg-gray-800 border-t border-gray-700">
  186. {/* 左侧区域 */}
  187. <div className="flex items-center space-x-6">
  188. {/* 查询输入框 */}
  189. <div className="flex items-center space-x-2">
  190. <input
  191. type="text"
  192. value={stockCode}
  193. onChange={(e) => setStockCode(e.target.value)}
  194. placeholder="输入股票代码"
  195. 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"
  196. />
  197. <button
  198. onClick={handleQuery}
  199. 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"
  200. >
  201. 查询
  202. </button>
  203. </div>
  204. {/* 交易面板 */}
  205. <TradePanel
  206. hasBought={hasBought}
  207. onToggleTrade={toggleTrade}
  208. />
  209. {/* 下一天按钮 */}
  210. <div className="flex items-center space-x-2">
  211. <button
  212. onClick={handleNextDay}
  213. disabled={!stockData.length}
  214. 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
  215. ${!stockData.length
  216. ? 'bg-gray-600 cursor-not-allowed'
  217. : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'}`}
  218. >
  219. 下一天
  220. </button>
  221. <span className="text-gray-400 text-xs">→</span>
  222. </div>
  223. {/* 天数快捷按钮组 */}
  224. <div className="flex items-center space-x-2">
  225. {[120, 30, 60].map((days) => (
  226. <button
  227. key={days}
  228. onClick={() => handleDayNumChange(days)}
  229. 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"
  230. >
  231. {days}天
  232. </button>
  233. ))}
  234. </div>
  235. </div>
  236. {/* 右侧快捷键说明和能按钮 */}
  237. <div className="flex items-center space-x-8">
  238. <div className="text-xs text-gray-400 leading-relaxed">
  239. <div>快捷键:</div>
  240. <div>B - 买入</div>
  241. <div>S - 卖出</div>
  242. {/* <div>ESC - 取消</div> */}
  243. <div>→ - 下一天</div>
  244. </div>
  245. <MemoToggle
  246. onToggle={(visible: boolean) => {
  247. chartRef.current?.toggleMemoVisibility(visible);
  248. }}
  249. 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"
  250. />
  251. </div>
  252. </div>
  253. </div>
  254. );
  255. }
  256. const router = createBrowserRouter([
  257. {
  258. path: '*',
  259. element: <StockApp />,
  260. },
  261. ]);
  262. // 渲染应用
  263. const root = createRoot(document.getElementById('root') as HTMLElement);
  264. root.render(
  265. <QueryClientProvider client={queryClient}>
  266. <RouterProvider router={router} />
  267. <ToastContainer
  268. position="top-right"
  269. autoClose={500}
  270. hideProgressBar={false}
  271. newestOnTop={false}
  272. closeOnClick
  273. rtl={false}
  274. pauseOnFocusLoss
  275. draggable
  276. pauseOnHover
  277. />
  278. </QueryClientProvider>
  279. );