stock_app.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import React, { useRef, useState, useCallback, useEffect } from 'react';
  2. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  3. import { StockChart, MemoToggle, TradePanel, useTradeRecords, useStockQueries, useProfitCalculator, ProfitDisplay, useStockDataFilter, DrawingToolbar } from './components/stock-chart/mod.ts';
  4. import type { StockChartRef } from './components/stock-chart/mod.ts';
  5. // import { useSocketRoom, useQuestionManagement } from './hooks/useSocketClient.ts';
  6. import { useSearchParams } from 'react-router';
  7. // import { message } from 'antd';
  8. import { ActiveType } from "./components/stock-chart/src/types/index.ts";
  9. const queryClient = new QueryClient();
  10. function StockApp() {
  11. const chartRef = useRef<StockChartRef>(null);
  12. // const lastSentDateRef = useRef('');
  13. const [searchParams] = useSearchParams();
  14. const codeFromUrl = searchParams.get('code');
  15. const [stockCode, setStockCode] = useState(codeFromUrl || undefined);// || '001339'
  16. // const classroom = searchParams.get('classroom');
  17. // const { connected } = useSocketRoom(classroom);
  18. // const { sendNextQuestion } = useQuestionManagement(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) {
  43. updateCurrentDate(nextDate);
  44. }
  45. }, [moveToNextDay, updateCurrentDate]);
  46. // useEffect(() => {
  47. // const currentDate = profitSummary.dailyStats.date;
  48. // if (classroom && connected && currentDate && lastSentDateRef.current !== currentDate) {
  49. // lastSentDateRef.current = currentDate;
  50. // sendNextQuestion({
  51. // date: currentDate,
  52. // price: profitSummary.dailyStats.close
  53. // }).catch(() => {
  54. // message.error('发送题目失败');
  55. // });
  56. // }
  57. // }, [classroom, connected, profitSummary.dailyStats, sendNextQuestion]);
  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 && stockCode.trim()) {
  67. fetchData().then(() => {
  68. initializeView();
  69. });
  70. }
  71. }, [stockCode, fetchData, initializeView]);
  72. const handleStartDrawing = useCallback((type: ActiveType) => {
  73. chartRef.current?.startDrawing(type);
  74. }, []);
  75. const handleStopDrawing = useCallback(() => {
  76. chartRef.current?.stopDrawing();
  77. }, []);
  78. const handleClearLines = useCallback(() => {
  79. chartRef.current?.clearDrawings();
  80. }, []);
  81. useEffect(() => {
  82. const handleKeyPress = (event: KeyboardEvent) => {
  83. switch(event.key.toLowerCase()) {
  84. case 'b':
  85. if (!hasBought) toggleTrade('BUY');
  86. break;
  87. case 's':
  88. if (hasBought) toggleTrade('SELL');
  89. break;
  90. case 'arrowright':
  91. handleNextDay();
  92. break;
  93. }
  94. };
  95. globalThis.addEventListener('keydown', handleKeyPress);
  96. return () => globalThis.removeEventListener('keydown', handleKeyPress);
  97. }, [hasBought, toggleTrade, handleNextDay]);
  98. useEffect(() => {
  99. if (codeFromUrl && codeFromUrl !== stockCode) {
  100. setStockCode(codeFromUrl);
  101. fetchData().then(() => {
  102. initializeView();
  103. });
  104. }
  105. }, [codeFromUrl, stockCode, fetchData, initializeView]);
  106. // if (isLoading) {
  107. // return (
  108. // <div className="flex items-center justify-center h-screen bg-gray-900">
  109. // <div className="text-white text-xl">加载中...</div>
  110. // </div>
  111. // );
  112. // }
  113. // if (error) {
  114. // return (
  115. // <div className="flex items-center justify-center h-screen bg-gray-900">
  116. // <div className="text-red-500 text-xl">错误: {error.message}</div>
  117. // </div>
  118. // );
  119. // }
  120. return (
  121. <div className="flex flex-col h-screen bg-gray-900">
  122. {/* 顶部行情和收益信息 */}
  123. <ProfitDisplay profitSummary={profitSummary} />
  124. {/* 主图表区域 */}
  125. <div className="flex-1 relative overflow-hidden">
  126. <StockChart
  127. ref={chartRef}
  128. stockData={stockData}
  129. memoData={memoData}
  130. trades={trades}
  131. />
  132. {/* 添加画线工具栏 */}
  133. <DrawingToolbar
  134. className="absolute top-4 right-4"
  135. onStartDrawing={handleStartDrawing}
  136. onStopDrawing={handleStopDrawing}
  137. onClearLines={handleClearLines}
  138. />
  139. </div>
  140. {/* 底部控制面板 */}
  141. <div className="flex items-center justify-between p-4 bg-gray-800 border-t border-gray-700">
  142. {/* 左侧区域 */}
  143. <div className="flex items-center space-x-6">
  144. {/* 查询输入框 */}
  145. <div className="flex items-center space-x-2">
  146. <input
  147. type="text"
  148. value={stockCode}
  149. onChange={(e) => setStockCode(e.target.value)}
  150. placeholder="输入股票代码"
  151. 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"
  152. />
  153. <button
  154. onClick={handleQuery}
  155. 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"
  156. >
  157. 查询
  158. </button>
  159. </div>
  160. {/* 交易面板 */}
  161. <TradePanel
  162. hasBought={hasBought}
  163. onToggleTrade={toggleTrade}
  164. />
  165. {/* 下一天按钮 */}
  166. <div className="flex items-center space-x-2">
  167. <button
  168. onClick={handleNextDay}
  169. disabled={!stockData.length}
  170. 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
  171. ${!stockData.length
  172. ? 'bg-gray-600 cursor-not-allowed'
  173. : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'}`}
  174. >
  175. 下一天
  176. </button>
  177. <span className="text-gray-400 text-xs">→</span>
  178. </div>
  179. {/* 天数快捷按钮组 */}
  180. <div className="flex items-center space-x-2">
  181. {[120, 30, 60].map((days) => (
  182. <button
  183. key={days}
  184. onClick={() => handleDayNumChange(days)}
  185. 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"
  186. >
  187. {days}天
  188. </button>
  189. ))}
  190. </div>
  191. </div>
  192. {/* 右侧快捷键说明和能按钮 */}
  193. <div className="flex items-center space-x-8">
  194. <div className="text-xs text-gray-400 leading-relaxed">
  195. <div>快捷键:</div>
  196. <div>B - 买入</div>
  197. <div>S - 卖出</div>
  198. {/* <div>ESC - 取消</div> */}
  199. <div>→ - 下一天</div>
  200. </div>
  201. <MemoToggle
  202. onToggle={(visible: boolean) => {
  203. chartRef.current?.toggleMemoVisibility(visible);
  204. }}
  205. 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"
  206. />
  207. </div>
  208. </div>
  209. </div>
  210. );
  211. }
  212. export default function App() {
  213. return (
  214. <QueryClientProvider client={queryClient}>
  215. <StockApp />
  216. </QueryClientProvider>
  217. );
  218. }