瀏覽代碼

✨ feat(stock): 添加固定日期范围训练模式

- 支持通过URL参数(startDate/endDate)指定训练日期范围
- 新增固定范围模式检测与索引计算逻辑
- 实现固定范围下的训练进度控制与数据过滤
- 优化"下一天"按钮状态显示,训练完成时显示"训练完成"
- 固定模式下禁用天数选择器,防止用户修改范围

🐛 fix(stock): 修复训练进度控制逻辑

- 修复固定日期范围模式下训练进度计算错误
- 完善日期范围无效时的回退机制
- 修复移动到下一天时的日期索引计算问题

♻️ refactor(stock): 重构股票数据过滤钩子

- 新增TrainingInfo接口定义,规范训练进度信息结构
- 拆分数据过滤逻辑,区分固定范围和普通模式
- 优化useCallback依赖数组,避免不必要的重计算
- 提取公共逻辑到独立函数,提高代码可维护性

💄 style(stock): 优化训练控制区UI

- 根据训练状态动态调整按钮样式和文本
- 固定模式下禁用天数选择按钮并调整样式
- 修复按钮禁用状态显示不一致问题
yourname 6 月之前
父節點
當前提交
900bf520d3

+ 110 - 15
src/client/mobile/components/stock/components/stock-chart/src/hooks/useStockDataFilter.ts

@@ -1,26 +1,84 @@
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useEffect } from 'react';
 import type { StockData } from '../types/index';
 
-export function useStockDataFilter(fullData: StockData[]) {
+export function useStockDataFilter(
+  fullData: StockData[], 
+  fixedDateRange?: { startDate?: string; endDate?: string }
+) {
   const [selectedDays, setSelectedDays] = useState(120); // 默认120天
   const [trainingProgress, setTrainingProgress] = useState(0); // 训练日进度
   const [isInitialized, setIsInitialized] = useState(false);
+  const [isFixedRangeMode, setIsFixedRangeMode] = useState(false);
+  const [fixedRangeIndices, setFixedRangeIndices] = useState<{ startIndex: number; endIndex: number } | null>(null);
+
+  // 检测固定日期范围模式并计算索引
+  useEffect(() => {
+    console.log('fixedDateRange',fixedDateRange)
+    if (fixedDateRange?.startDate && fixedDateRange?.endDate && fullData.length > 0) {
+      setIsFixedRangeMode(true);
+      
+      const startIndex = fullData.findIndex(item => item.d === fixedDateRange.startDate);
+      const endIndex = fullData.findIndex(item => item.d === fixedDateRange.endDate);
+      console.log('startIndex',startIndex)
+      console.log('endIndex',endIndex)
+      console.log('fullData',fullData)
+      
+      if (startIndex !== -1 && endIndex !== -1 && startIndex <= endIndex) {
+        const calculatedDays = endIndex - startIndex + 1;
+        setFixedRangeIndices({ startIndex, endIndex });
+        setSelectedDays(calculatedDays);
+        // 保持 trainingProgress 为0,让用户从头开始训练
+      } else {
+        // 日期不在数据范围内,回退到普通模式
+        setIsFixedRangeMode(false);
+        setFixedRangeIndices(null);
+      }
+    } else {
+      setIsFixedRangeMode(false);
+      setFixedRangeIndices(null);
+    }
+  }, [fixedDateRange?.startDate, fixedDateRange?.endDate, fullData.length]);
 
   const filterData = useCallback(() => {
     if (!isInitialized || fullData.length === 0) {
       return []; // 未初始化或数据为空时返回空数组
     }
 
+    // 固定日期范围模式
+    if (isFixedRangeMode && fixedRangeIndices) {
+      const { startIndex, endIndex } = fixedRangeIndices;
+      const totalDays = endIndex - startIndex + 1;
+      
+      // 计算训练结束索引
+      const trainingEndIndex = Math.min(
+        startIndex + trainingProgress,
+        endIndex + 1 // +1 因为 slice 是 [start, end)
+      );
+      
+      // 计算历史数据部分:从训练开始往前推至少365天或可用数据
+      const historyEndIndex = startIndex;
+      const historyStartIndex = Math.max(0, historyEndIndex - Math.max(totalDays, 365));
+      
+      if (historyStartIndex >= historyEndIndex) {
+        // 没有历史数据,只返回训练数据
+        return fullData.slice(startIndex, trainingEndIndex);
+      }
+      
+      // 合并历史数据和训练数据
+      const historyData = fullData.slice(historyStartIndex, historyEndIndex);
+      const trainingData = fullData.slice(startIndex, trainingEndIndex);
+      
+      return [...historyData, ...trainingData];
+    }
+
+    // 原有天数选择模式(向后兼容)
     const totalDays = fullData.length;
-    
-    // 计算训练日部分:从当前日期往前推选择的天数到当前日期
     const trainingStartIndex = totalDays - selectedDays;
     const trainingEndIndex = Math.min(
       trainingStartIndex + trainingProgress,
       totalDays
     );
     
-    // 计算历史天数部分:从训练日开始日期往前推至少365天
     const historyEndIndex = trainingStartIndex;
     const historyStartIndex = Math.max(0, historyEndIndex - Math.max(selectedDays, 365));
     
@@ -34,12 +92,30 @@ export function useStockDataFilter(fullData: StockData[]) {
     const trainingData = fullData.slice(trainingStartIndex, trainingEndIndex);
     
     return [...historyData, ...trainingData];
-  }, [fullData, selectedDays, trainingProgress, isInitialized]);
+  }, [fullData, selectedDays, trainingProgress, isInitialized, isFixedRangeMode, fixedRangeIndices]);
 
   const moveToNextDay = useCallback(() => {
-    return new Promise<string>((resolve) => {
+    return new Promise<string>((resolve, reject) => {
       setTrainingProgress((prev) => {
         const newProgress = prev + 1;
+        
+        // 固定日期范围模式
+        if (isFixedRangeMode && fixedRangeIndices) {
+          const { startIndex, endIndex } = fixedRangeIndices;
+          const totalTrainingDays = endIndex - startIndex + 1;
+          
+          if (newProgress > totalTrainingDays) {
+            reject(new Error('已到达训练结束日期'));
+            return prev; // 不更新进度
+          }
+          
+          const nextDateIndex = startIndex + newProgress - 1;
+          const nextDate = fullData[nextDateIndex]?.d || '';
+          resolve(nextDate);
+          return newProgress;
+        }
+        
+        // 原有天数选择模式
         const totalDays = fullData.length;
         const trainingStartIndex = totalDays - selectedDays;
         const trainingEndIndex = Math.min(
@@ -47,22 +123,24 @@ export function useStockDataFilter(fullData: StockData[]) {
           totalDays
         );
         
-        // 返回最新日期
         const nextDate = fullData[trainingEndIndex - 1]?.d || '';
         resolve(nextDate);
         return newProgress;
       });
     });
-  }, [fullData, selectedDays]);
+  }, [fullData, selectedDays, isFixedRangeMode, fixedRangeIndices]);
 
   const resetTrainingProgress = useCallback(() => {
     setTrainingProgress(0);
   }, []);
 
   const setDayNumWithTraining = useCallback((days: number) => {
-    setSelectedDays(days);
-    setTrainingProgress(0); // 重置训练进度
-  }, []);
+    // 固定模式下禁用天数修改
+    if (!isFixedRangeMode) {
+      setSelectedDays(days);
+      setTrainingProgress(0); // 重置训练进度
+    }
+  }, [isFixedRangeMode]);
 
   const initializeView = useCallback(() => {
     setIsInitialized(true);
@@ -70,6 +148,21 @@ export function useStockDataFilter(fullData: StockData[]) {
 
   // 获取当前训练进度信息
   const getTrainingInfo = useCallback(() => {
+    // 固定日期范围模式
+    if (isFixedRangeMode && fixedRangeIndices) {
+      const { startIndex, endIndex } = fixedRangeIndices;
+      const totalTrainingDays = endIndex - startIndex + 1;
+      
+      return {
+        currentProgress: trainingProgress,
+        totalTrainingDays,
+        remainingDays: totalTrainingDays - trainingProgress,
+        isTrainingComplete: trainingProgress >= totalTrainingDays,
+        isFixedRangeMode: true
+      };
+    }
+
+    // 原有天数选择模式
     const totalDays = fullData.length;
     const trainingStartIndex = totalDays - selectedDays;
     const totalTrainingDays = Math.min(selectedDays, totalDays - trainingStartIndex);
@@ -78,9 +171,10 @@ export function useStockDataFilter(fullData: StockData[]) {
       currentProgress: trainingProgress,
       totalTrainingDays,
       remainingDays: totalTrainingDays - trainingProgress,
-      isTrainingComplete: trainingProgress >= totalTrainingDays
+      isTrainingComplete: trainingProgress >= totalTrainingDays,
+      isFixedRangeMode: false
     };
-  }, [fullData, selectedDays, trainingProgress]);
+  }, [fullData, selectedDays, trainingProgress, isFixedRangeMode, fixedRangeIndices]);
 
   return {
     filteredData: filterData(),
@@ -89,6 +183,7 @@ export function useStockDataFilter(fullData: StockData[]) {
     setDayNum: setDayNumWithTraining,
     initializeView,
     isInitialized,
-    trainingInfo: getTrainingInfo()
+    trainingInfo: getTrainingInfo(),
+    isFixedRangeMode
   };
 }

+ 20 - 0
src/client/mobile/components/stock/components/stock-chart/src/types/index.ts

@@ -207,4 +207,24 @@ export interface ProfitSummary {
     low: number;
     change: number;  // 日涨幅
   };
+}
+
+// 添加训练信息类型定义
+export interface TrainingInfo {
+  currentProgress: number;
+  totalTrainingDays: number;
+  remainingDays: number;
+  isTrainingComplete: boolean;
+  isFixedRangeMode: boolean;
+}
+
+export interface UseStockDataFilterReturn {
+  filteredData: StockData[];
+  moveToNextDay: () => Promise<string>;
+  resetTrainingProgress: () => void;
+  setDayNum: (days: number) => void;
+  initializeView: () => void;
+  isInitialized: boolean;
+  trainingInfo: TrainingInfo;
+  isFixedRangeMode: boolean;
 } 

+ 18 - 7
src/client/mobile/components/stock/stock_main.tsx

@@ -10,6 +10,8 @@ export function StockMain() {
   const chartRef = useRef<StockChartRef>(null);
   const [searchParams] = useSearchParams();
   const codeFromUrl = searchParams.get('code');
+  const startDateFromUrl = searchParams.get('startDate');
+  const endDateFromUrl = searchParams.get('endDate');
   const [stockCode, setStockCode] = useState(codeFromUrl || undefined);//|| '001339'
   const [showCode, setShowCode] = useState(false);
   const classroom = searchParams.get('classroom');
@@ -29,8 +31,13 @@ export function StockMain() {
     moveToNextDay,
     setDayNum,
     initializeView,
-    isInitialized
-  } = useStockDataFilter(fullStockData);
+    isInitialized,
+    trainingInfo,
+    isFixedRangeMode
+  } = useStockDataFilter(fullStockData, {
+    startDate: startDateFromUrl || undefined,
+    endDate: endDateFromUrl || undefined
+  });
   const { 
     trades, 
     toggleTrade,
@@ -164,7 +171,7 @@ export function StockMain() {
           memoData={memoData}
           trades={trades}
           width="100%"
-          scaleBarFullWidth={true}
+          // scaleBarFullWidth={true}
         />
         
         {/* 添加画线工具栏 */}
@@ -217,13 +224,13 @@ export function StockMain() {
           <div className="flex items-center space-x-2">
             <button 
               onClick={handleNextDay}
-              disabled={!stockData.length}
+              disabled={!stockData.length || (isFixedRangeMode && trainingInfo.isTrainingComplete)}
               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
-                ${!stockData.length 
+                ${!stockData.length || (isFixedRangeMode && trainingInfo.isTrainingComplete) 
                   ? 'bg-gray-600 cursor-not-allowed' 
                   : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'}`}
             >
-              下一天
+              {isFixedRangeMode && trainingInfo.isTrainingComplete ? '训练完成' : '下一天'}
             </button>
             <span className="text-gray-400 text-xs">→</span>
           </div>
@@ -234,7 +241,11 @@ export function StockMain() {
               <button
                 key={days}
                 onClick={() => handleDayNumChange(days)}
-                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"
+                disabled={isFixedRangeMode}
+                className={`px-3 py-1 text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors
+                  ${isFixedRangeMode 
+                    ? 'bg-gray-600 text-gray-400 cursor-not-allowed' 
+                    : 'text-white bg-gray-700 hover:bg-gray-600 focus:ring-gray-500'}`}
               >
                 {days}天
               </button>