refactor(DeepAnalysis): TypeScript 重构,提取 useDeepAnalysisData Hook

- 新增 types.ts:API 类型定义、状态接口、Tab 映射常量
- 新增 hooks/useDeepAnalysisData.ts:提取数据获取逻辑
  - 懒加载:按 Tab 按需请求
  - 数据缓存:已加载数据不重复请求
  - 竞态处理:stockCode 变更时防止旧请求覆盖
- 重写 index.tsx:memo 优化,代码行数 229 → 81
- 新增 README.md:组件文档

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 13:42:16 +08:00
parent bff440ff8a
commit ce4da40ef6
6 changed files with 383 additions and 228 deletions

View File

@@ -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';
<DeepAnalysis stockCode="600000" />
```
## 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 变更时忽略旧请求)

View File

@@ -0,0 +1 @@
export { useDeepAnalysisData } from './useDeepAnalysisData';

View File

@@ -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<ApiKey, string> = {
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<DataState>(initialDataState);
// Loading 状态
const [loading, setLoading] = useState<ApiLoadingState>(initialLoadingState);
// 已加载的接口记录
const loadedApisRef = useRef<Record<ApiKey, boolean>>({
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();
// 只加载默认 Tabcomprehensive
loadApiData('comprehensive');
}
}, [stockCode, loadApiData, resetData]);
return {
data,
loading,
loadTabData,
resetData,
};
};
export default useDeepAnalysisData;

View File

@@ -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 (
<DeepAnalysisTab
comprehensiveData={comprehensiveData}
valueChainData={valueChainData}
keyFactorsData={keyFactorsData}
industryRankData={industryRankData}
loading={getCurrentLoading()}
cardBg="white"
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
activeTab={activeTab}
onTabChange={handleTabChange}
/>
);
};
export default DeepAnalysis;

View File

@@ -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<DeepAnalysisProps> = memo(({ stockCode }) => {
// 当前 Tab
const [activeTab, setActiveTab] = useState<DeepAnalysisTabKey>('strategy');
// 业务板块展开状态
const [expandedSegments, setExpandedSegments] = useState<Record<number, boolean>>({});
// 数据获取 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 (
<DeepAnalysisTab
comprehensiveData={data.comprehensive}
valueChainData={data.valueChain}
keyFactorsData={data.keyFactors}
industryRankData={data.industryRank}
loading={currentLoading}
cardBg="white"
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
activeTab={activeTab}
onTabChange={handleTabChange}
/>
);
});
DeepAnalysis.displayName = 'DeepAnalysis';
export default DeepAnalysis;

View File

@@ -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<string, ApiKey> = {
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';