stock_app.tsx 8.6 KB

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