stock_main.tsx 8.0 KB

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