stock_main.tsx 8.7 KB

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