diff --git a/src/views/Company/components/DeepAnalysis/README.md b/src/views/Company/components/DeepAnalysis/README.md new file mode 100644 index 00000000..f00bd83c --- /dev/null +++ b/src/views/Company/components/DeepAnalysis/README.md @@ -0,0 +1,79 @@ +# DeepAnalysis 组件 + +深度分析模块,展示公司战略分析、业务分析、产业链分析和发展历程。 + +## 目录结构 + +``` +DeepAnalysis/ +├── index.tsx # 主组件入口(memo 优化) +├── types.ts # 类型定义 +├── README.md # 本文档 +│ +└── hooks/ + ├── index.ts # Hooks 导出 + └── useDeepAnalysisData.ts # 数据获取 Hook(懒加载、缓存) +``` + +## 功能特性 + +| 特性 | 说明 | +|------|------| +| Tab 懒加载 | 切换 Tab 时按需加载数据 | +| 数据缓存 | 已加载的数据不重复请求 | +| 竞态处理 | stockCode 变更时防止旧请求覆盖新数据 | +| memo 优化 | 避免不必要的重渲染 | + +## API 接口映射 + +| Tab | API Key | 接口 | +|-----|---------|------| +| 战略分析 | comprehensive | `/api/company/comprehensive-analysis` | +| 业务分析 | comprehensive | 同上(共用) | +| 产业链 | valueChain | `/api/company/value-chain-analysis` | +| 发展历程 | keyFactors | `/api/company/key-factors-timeline` | + +## 数据流 + +``` +DeepAnalysis +├── useDeepAnalysisData (Hook) +│ ├── data: { comprehensive, valueChain, keyFactors, industryRank } +│ ├── loading: { comprehensive, valueChain, keyFactors, industryRank } +│ └── loadTabData(tabKey) → loadApiData(apiKey) +│ +└── DeepAnalysisTab (展示组件) + ├── StrategyTab + ├── BusinessTab + ├── ValueChainTab + └── DevelopmentTab +``` + +## 使用示例 + +```tsx +import DeepAnalysis from '@views/Company/components/DeepAnalysis'; + + +``` + +## Hook 使用 + +```tsx +import { useDeepAnalysisData } from '@views/Company/components/DeepAnalysis/hooks'; + +const { data, loading, loadTabData, resetData } = useDeepAnalysisData(stockCode); + +// 手动加载某个 Tab +loadTabData('valueChain'); + +// 重置所有数据 +resetData(); +``` + +## 性能优化 + +- `React.memo` 包装主组件 +- `useCallback` 稳定化事件处理函数 +- `useRef` 追踪已加载状态(避免重复请求) +- 竞态条件检测(stockCode 变更时忽略旧请求) diff --git a/src/views/Company/components/DeepAnalysis/hooks/index.ts b/src/views/Company/components/DeepAnalysis/hooks/index.ts new file mode 100644 index 00000000..ac71614c --- /dev/null +++ b/src/views/Company/components/DeepAnalysis/hooks/index.ts @@ -0,0 +1 @@ +export { useDeepAnalysisData } from './useDeepAnalysisData'; diff --git a/src/views/Company/components/DeepAnalysis/hooks/useDeepAnalysisData.ts b/src/views/Company/components/DeepAnalysis/hooks/useDeepAnalysisData.ts new file mode 100644 index 00000000..dda2a894 --- /dev/null +++ b/src/views/Company/components/DeepAnalysis/hooks/useDeepAnalysisData.ts @@ -0,0 +1,150 @@ +/** + * useDeepAnalysisData Hook + * + * 管理深度分析模块的数据获取逻辑: + * - 按 Tab 懒加载数据 + * - 已加载数据缓存,避免重复请求 + * - 竞态条件处理 + */ + +import { useState, useCallback, useRef, useEffect } from 'react'; +import axios from '@utils/axiosConfig'; +import { logger } from '@utils/logger'; +import type { + ApiKey, + ApiLoadingState, + DataState, + UseDeepAnalysisDataReturn, +} from '../types'; +import { TAB_API_MAP } from '../types'; + +/** API 端点映射 */ +const API_ENDPOINTS: Record = { + comprehensive: '/api/company/comprehensive-analysis', + valueChain: '/api/company/value-chain-analysis', + keyFactors: '/api/company/key-factors-timeline', + industryRank: '/api/financial/industry-rank', +}; + +/** 初始数据状态 */ +const initialDataState: DataState = { + comprehensive: null, + valueChain: null, + keyFactors: null, + industryRank: null, +}; + +/** 初始 loading 状态 */ +const initialLoadingState: ApiLoadingState = { + comprehensive: false, + valueChain: false, + keyFactors: false, + industryRank: false, +}; + +/** + * 深度分析数据 Hook + * + * @param stockCode 股票代码 + * @returns 数据、loading 状态、加载函数 + */ +export const useDeepAnalysisData = (stockCode: string): UseDeepAnalysisDataReturn => { + // 数据状态 + const [data, setData] = useState(initialDataState); + + // Loading 状态 + const [loading, setLoading] = useState(initialLoadingState); + + // 已加载的接口记录 + const loadedApisRef = useRef>({ + comprehensive: false, + valueChain: false, + keyFactors: false, + industryRank: false, + }); + + // 当前 stockCode(用于竞态条件检测) + const currentStockCodeRef = useRef(stockCode); + + /** + * 加载指定 API 数据 + */ + const loadApiData = useCallback( + async (apiKey: ApiKey) => { + if (!stockCode) return; + + // 已加载则跳过 + if (loadedApisRef.current[apiKey]) return; + + // 设置 loading + setLoading((prev) => ({ ...prev, [apiKey]: true })); + + try { + const endpoint = `${API_ENDPOINTS[apiKey]}/${stockCode}`; + const { data: response } = await axios.get(endpoint); + + // 检查 stockCode 是否已变更(防止竞态) + if (currentStockCodeRef.current !== stockCode) return; + + if (response.success) { + setData((prev) => ({ ...prev, [apiKey]: response.data })); + loadedApisRef.current[apiKey] = true; + } + } catch (err) { + logger.error('DeepAnalysis', `loadApiData:${apiKey}`, err, { stockCode }); + } finally { + // 清除 loading(再次检查 stockCode) + if (currentStockCodeRef.current === stockCode) { + setLoading((prev) => ({ ...prev, [apiKey]: false })); + } + } + }, + [stockCode] + ); + + /** + * 根据 Tab 加载对应数据 + */ + const loadTabData = useCallback( + (tabKey: string) => { + const apiKey = TAB_API_MAP[tabKey]; + if (apiKey) { + loadApiData(apiKey); + } + }, + [loadApiData] + ); + + /** + * 重置所有数据 + */ + const resetData = useCallback(() => { + setData(initialDataState); + setLoading(initialLoadingState); + loadedApisRef.current = { + comprehensive: false, + valueChain: false, + keyFactors: false, + industryRank: false, + }; + }, []); + + // stockCode 变更时重置并加载默认数据 + useEffect(() => { + if (stockCode) { + currentStockCodeRef.current = stockCode; + resetData(); + // 只加载默认 Tab(comprehensive) + loadApiData('comprehensive'); + } + }, [stockCode, loadApiData, resetData]); + + return { + data, + loading, + loadTabData, + resetData, + }; +}; + +export default useDeepAnalysisData; diff --git a/src/views/Company/components/DeepAnalysis/index.js b/src/views/Company/components/DeepAnalysis/index.js deleted file mode 100644 index c21e69a3..00000000 --- a/src/views/Company/components/DeepAnalysis/index.js +++ /dev/null @@ -1,228 +0,0 @@ -// src/views/Company/components/DeepAnalysis/index.js -// 深度分析 - 独立一级 Tab 组件(懒加载版本) - -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { logger } from "@utils/logger"; -import axios from "@utils/axiosConfig"; - -// 复用原有的展示组件 -import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab"; - -/** - * Tab 与 API 接口映射 - * - strategy 和 business 共用 comprehensive 接口 - */ -const TAB_API_MAP = { - strategy: "comprehensive", - business: "comprehensive", - valueChain: "valueChain", - development: "keyFactors", -}; - -/** - * 深度分析组件 - * - * 功能: - * - 按 Tab 懒加载数据(默认只加载战略分析) - * - 已加载的数据缓存,切换 Tab 不重复请求 - * - 管理展开状态 - * - * @param {Object} props - * @param {string} props.stockCode - 股票代码 - */ -const DeepAnalysis = ({ stockCode }) => { - // 当前 Tab - const [activeTab, setActiveTab] = useState("strategy"); - - // 数据状态 - const [comprehensiveData, setComprehensiveData] = useState(null); - const [valueChainData, setValueChainData] = useState(null); - const [keyFactorsData, setKeyFactorsData] = useState(null); - const [industryRankData, setIndustryRankData] = useState(null); - - // 各接口独立的 loading 状态 - const [comprehensiveLoading, setComprehensiveLoading] = useState(false); - const [valueChainLoading, setValueChainLoading] = useState(false); - const [keyFactorsLoading, setKeyFactorsLoading] = useState(false); - const [industryRankLoading, setIndustryRankLoading] = useState(false); - - // 已加载的接口记录(用于缓存判断) - const loadedApisRef = useRef({ - comprehensive: false, - valueChain: false, - keyFactors: false, - industryRank: false, - }); - - // 业务板块展开状态 - const [expandedSegments, setExpandedSegments] = useState({}); - - // 用于追踪当前 stockCode,避免竞态条件 - const currentStockCodeRef = useRef(stockCode); - - // 切换业务板块展开状态 - const toggleSegmentExpansion = (segmentIndex) => { - setExpandedSegments((prev) => ({ - ...prev, - [segmentIndex]: !prev[segmentIndex], - })); - }; - - /** - * 加载指定接口的数据 - */ - const loadApiData = useCallback( - async (apiKey) => { - if (!stockCode) return; - - // 已加载则跳过 - if (loadedApisRef.current[apiKey]) return; - - try { - switch (apiKey) { - case "comprehensive": - setComprehensiveLoading(true); - const { data: comprehensiveRes } = await axios.get( - `/api/company/comprehensive-analysis/${stockCode}` - ); - // 检查 stockCode 是否已变更(防止竞态) - if (currentStockCodeRef.current === stockCode) { - if (comprehensiveRes.success) - setComprehensiveData(comprehensiveRes.data); - loadedApisRef.current.comprehensive = true; - } - break; - - case "valueChain": - setValueChainLoading(true); - const { data: valueChainRes } = await axios.get( - `/api/company/value-chain-analysis/${stockCode}` - ); - if (currentStockCodeRef.current === stockCode) { - if (valueChainRes.success) setValueChainData(valueChainRes.data); - loadedApisRef.current.valueChain = true; - } - break; - - case "keyFactors": - setKeyFactorsLoading(true); - const { data: keyFactorsRes } = await axios.get( - `/api/company/key-factors-timeline/${stockCode}` - ); - if (currentStockCodeRef.current === stockCode) { - if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data); - loadedApisRef.current.keyFactors = true; - } - break; - - case "industryRank": - setIndustryRankLoading(true); - const { data: industryRankRes } = await axios.get( - `/api/financial/industry-rank/${stockCode}` - ); - if (currentStockCodeRef.current === stockCode) { - if (industryRankRes.success) setIndustryRankData(industryRankRes.data); - loadedApisRef.current.industryRank = true; - } - break; - - default: - break; - } - } catch (err) { - logger.error("DeepAnalysis", `loadApiData:${apiKey}`, err, { - stockCode, - }); - } finally { - // 清除 loading 状态 - if (apiKey === "comprehensive") setComprehensiveLoading(false); - if (apiKey === "valueChain") setValueChainLoading(false); - if (apiKey === "keyFactors") setKeyFactorsLoading(false); - if (apiKey === "industryRank") setIndustryRankLoading(false); - } - }, - [stockCode] - ); - - /** - * 根据 Tab 加载对应的数据 - */ - const loadTabData = useCallback( - (tabKey) => { - const apiKey = TAB_API_MAP[tabKey]; - if (apiKey) { - loadApiData(apiKey); - } - }, - [loadApiData] - ); - - /** - * Tab 切换回调 - */ - const handleTabChange = useCallback( - (index, tabKey) => { - setActiveTab(tabKey); - loadTabData(tabKey); - }, - [loadTabData] - ); - - // stockCode 变更时重置并加载默认 Tab 数据 - useEffect(() => { - if (stockCode) { - // 更新 ref - currentStockCodeRef.current = stockCode; - - // 重置所有数据和状态 - setComprehensiveData(null); - setValueChainData(null); - setKeyFactorsData(null); - setIndustryRankData(null); - setExpandedSegments({}); - loadedApisRef.current = { - comprehensive: false, - valueChain: false, - keyFactors: false, - industryRank: false, - }; - - // 重置为默认 Tab 并加载数据 - setActiveTab("strategy"); - // 只加载默认 Tab 的核心数据(comprehensive),其他数据按需加载 - loadApiData("comprehensive"); - } - }, [stockCode, loadApiData]); - - // 计算当前 Tab 的 loading 状态 - const getCurrentLoading = () => { - const apiKey = TAB_API_MAP[activeTab]; - switch (apiKey) { - case "comprehensive": - return comprehensiveLoading; - case "valueChain": - return valueChainLoading; - case "keyFactors": - return keyFactorsLoading; - default: - return false; - } - }; - - return ( - - ); -}; - -export default DeepAnalysis; diff --git a/src/views/Company/components/DeepAnalysis/index.tsx b/src/views/Company/components/DeepAnalysis/index.tsx new file mode 100644 index 00000000..9dadeecb --- /dev/null +++ b/src/views/Company/components/DeepAnalysis/index.tsx @@ -0,0 +1,81 @@ +/** + * DeepAnalysis - 深度分析组件 + * + * 独立一级 Tab 组件,支持: + * - 按 Tab 懒加载数据 + * - 已加载数据缓存 + * - 业务板块展开状态管理 + */ + +import React, { useState, useCallback, useEffect, memo } from 'react'; +import DeepAnalysisTab from '../CompanyOverview/DeepAnalysisTab'; +import type { DeepAnalysisTabKey } from '../CompanyOverview/DeepAnalysisTab/types'; +import { useDeepAnalysisData } from './hooks'; +import { TAB_API_MAP } from './types'; +import type { DeepAnalysisProps } from './types'; + +/** + * 深度分析组件 + * + * @param stockCode 股票代码 + */ +const DeepAnalysis: React.FC = memo(({ stockCode }) => { + // 当前 Tab + const [activeTab, setActiveTab] = useState('strategy'); + + // 业务板块展开状态 + const [expandedSegments, setExpandedSegments] = useState>({}); + + // 数据获取 Hook + const { data, loading, loadTabData } = useDeepAnalysisData(stockCode); + + // stockCode 变更时重置 UI 状态 + useEffect(() => { + if (stockCode) { + setActiveTab('strategy'); + setExpandedSegments({}); + } + }, [stockCode]); + + // 切换业务板块展开状态 + const toggleSegmentExpansion = useCallback((segmentIndex: number) => { + setExpandedSegments((prev) => ({ + ...prev, + [segmentIndex]: !prev[segmentIndex], + })); + }, []); + + // Tab 切换回调 + const handleTabChange = useCallback( + (index: number, tabKey: DeepAnalysisTabKey) => { + setActiveTab(tabKey); + loadTabData(tabKey); + }, + [loadTabData] + ); + + // 获取当前 Tab 的 loading 状态 + const currentLoading = (() => { + const apiKey = TAB_API_MAP[activeTab]; + return apiKey ? loading[apiKey] : false; + })(); + + return ( + + ); +}); + +DeepAnalysis.displayName = 'DeepAnalysis'; + +export default DeepAnalysis; diff --git a/src/views/Company/components/DeepAnalysis/types.ts b/src/views/Company/components/DeepAnalysis/types.ts new file mode 100644 index 00000000..419a04d2 --- /dev/null +++ b/src/views/Company/components/DeepAnalysis/types.ts @@ -0,0 +1,72 @@ +/** + * DeepAnalysis 组件类型定义 + */ + +// 复用 DeepAnalysisTab 的数据类型 +export type { + ComprehensiveData, + ValueChainData, + KeyFactorsData, + IndustryRankData, + DeepAnalysisTabKey, +} from '../CompanyOverview/DeepAnalysisTab/types'; + +/** API 接口类型 */ +export type ApiKey = 'comprehensive' | 'valueChain' | 'keyFactors' | 'industryRank'; + +/** Tab 与 API 映射 */ +export const TAB_API_MAP: Record = { + strategy: 'comprehensive', + business: 'comprehensive', + valueChain: 'valueChain', + development: 'keyFactors', +} as const; + +/** API 加载状态 */ +export interface ApiLoadingState { + comprehensive: boolean; + valueChain: boolean; + keyFactors: boolean; + industryRank: boolean; +} + +/** API 已加载标记 */ +export interface ApiLoadedState { + comprehensive: boolean; + valueChain: boolean; + keyFactors: boolean; + industryRank: boolean; +} + +/** 数据状态 */ +export interface DataState { + comprehensive: ComprehensiveData | null; + valueChain: ValueChainData | null; + keyFactors: KeyFactorsData | null; + industryRank: IndustryRankData[] | null; +} + +/** Hook 返回值 */ +export interface UseDeepAnalysisDataReturn { + /** 各接口数据 */ + data: DataState; + /** 各接口 loading 状态 */ + loading: ApiLoadingState; + /** 加载指定 Tab 的数据 */ + loadTabData: (tabKey: string) => void; + /** 重置所有数据 */ + resetData: () => void; +} + +/** 组件 Props */ +export interface DeepAnalysisProps { + stockCode: string; +} + +// 导入类型用于内部使用 +import type { + ComprehensiveData, + ValueChainData, + KeyFactorsData, + IndustryRankData, +} from '../CompanyOverview/DeepAnalysisTab/types';