feat(DeepAnalysis): 实现 Tab 懒加载,按需请求数据

- DeepAnalysis/index.js: 重构为懒加载模式
  - 添加 TAB_API_MAP 映射 Tab 与接口关系
  - 战略分析/业务结构共享 comprehensive-analysis 接口
  - 产业链/发展历程按需加载对应接口
  - 使用 loadedApisRef 缓存已加载状态,避免重复请求
  - 各接口独立 loading 状态管理
  - 添加 stockCode 竞态条件保护

- DeepAnalysisTab/index.tsx: 支持受控模式
  - 新增 activeTab/onTabChange props
  - loading 状态下保持 Tab 导航可切换

- types.ts: 新增 DeepAnalysisTabKey 类型和相关 props

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-12 15:20:37 +08:00
parent 942dd16800
commit b89837d22e
3 changed files with 200 additions and 46 deletions

View File

@@ -6,14 +6,16 @@
* 2. 业务结构 - 业务结构树 + 业务板块详情
* 3. 产业链 - 产业链分析(独立,含 Sankey 图)
* 4. 发展历程 - 关键因素 + 时间线
*
* 支持懒加载:通过 activeTab 和 onTabChange 实现按需加载数据
*/
import React from 'react';
import React, { useMemo } from 'react';
import { Card, CardBody, Center, VStack, Spinner, Text } from '@chakra-ui/react';
import { FaBrain, FaBuilding, FaLink, FaHistory } from 'react-icons/fa';
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
import { StrategyTab, BusinessTab, ValueChainTab, DevelopmentTab } from './tabs';
import type { DeepAnalysisTabProps } from './types';
import type { DeepAnalysisTabProps, DeepAnalysisTabKey } from './types';
// 主题配置(与 BasicInfoTab 保持一致)
const THEME = {
@@ -31,6 +33,16 @@ const DEEP_ANALYSIS_TABS: SubTabConfig[] = [
{ key: 'development', name: '发展历程', icon: FaHistory, component: DevelopmentTab },
];
/**
* Tab key 到 index 的映射
*/
const TAB_KEY_TO_INDEX: Record<DeepAnalysisTabKey, number> = {
strategy: 0,
business: 1,
valueChain: 2,
development: 3,
};
const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
comprehensiveData,
valueChainData,
@@ -39,16 +51,37 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
cardBg,
expandedSegments,
onToggleSegment,
activeTab,
onTabChange,
}) => {
// 计算当前 Tab 索引(受控模式)
const currentIndex = useMemo(() => {
if (activeTab) {
return TAB_KEY_TO_INDEX[activeTab] ?? 0;
}
return undefined; // 非受控模式
}, [activeTab]);
// 加载状态
if (loading) {
return (
<Center h="200px">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" />
<Text>...</Text>
</VStack>
</Center>
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
<CardBody p={0}>
<SubTabContainer
tabs={DEEP_ANALYSIS_TABS}
index={currentIndex}
onTabChange={onTabChange}
componentProps={{}}
themePreset="blackGold"
/>
<Center h="200px">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" />
<Text color="gray.400">...</Text>
</VStack>
</Center>
</CardBody>
</Card>
);
}
@@ -57,6 +90,8 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
<CardBody p={0}>
<SubTabContainer
tabs={DEEP_ANALYSIS_TABS}
index={currentIndex}
onTabChange={onTabChange}
componentProps={{
comprehensiveData,
valueChainData,

View File

@@ -267,6 +267,9 @@ export interface KeyFactorsData {
// ==================== 主组件 Props 类型 ====================
/** Tab 类型 */
export type DeepAnalysisTabKey = 'strategy' | 'business' | 'valueChain' | 'development';
export interface DeepAnalysisTabProps {
comprehensiveData?: ComprehensiveData;
valueChainData?: ValueChainData;
@@ -275,6 +278,10 @@ export interface DeepAnalysisTabProps {
cardBg?: string;
expandedSegments: Record<number, boolean>;
onToggleSegment: (index: number) => void;
/** 当前激活的 Tab受控模式 */
activeTab?: DeepAnalysisTabKey;
/** Tab 切换回调(懒加载触发) */
onTabChange?: (index: number, tabKey: string) => void;
}
// ==================== 子组件 Props 类型 ====================

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/DeepAnalysis/index.js
// 深度分析 - 独立一级 Tab 组件
// 深度分析 - 独立一级 Tab 组件(懒加载版本)
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
@@ -10,27 +10,55 @@ import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab";
const API_BASE_URL = getApiBase();
/**
* Tab 与 API 接口映射
* - strategy 和 business 共用 comprehensive 接口
*/
const TAB_API_MAP = {
strategy: "comprehensive",
business: "comprehensive",
valueChain: "valueChain",
development: "keyFactors",
};
/**
* 深度分析组件
*
* 功能:
* - 加载深度分析数据3个接口
* - 按 Tab 懒加载数据(默认只加载战略分析
* - 已加载的数据缓存,切换 Tab 不重复请求
* - 管理展开状态
* - 渲染 DeepAnalysisTab 展示组件
*
* @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 [loading, setLoading] = useState(false);
// 各接口独立的 loading 状态
const [comprehensiveLoading, setComprehensiveLoading] = useState(false);
const [valueChainLoading, setValueChainLoading] = useState(false);
const [keyFactorsLoading, setKeyFactorsLoading] = useState(false);
// 已加载的接口记录(用于缓存判断)
const loadedApisRef = useRef({
comprehensive: false,
valueChain: false,
keyFactors: false,
});
// 业务板块展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 用于追踪当前 stockCode避免竞态条件
const currentStockCodeRef = useRef(stockCode);
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({
@@ -39,60 +67,144 @@ const DeepAnalysis = ({ stockCode }) => {
}));
};
// 加载深度分析数据3个接口
const loadDeepAnalysisData = async () => {
if (!stockCode) return;
/**
* 加载指定接口的数据
*/
const loadApiData = useCallback(
async (apiKey) => {
if (!stockCode) return;
setLoading(true);
// 已加载则跳过
if (loadedApisRef.current[apiKey]) return;
try {
const requests = [
fetch(
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
).then((r) => r.json()),
];
try {
switch (apiKey) {
case "comprehensive":
setComprehensiveLoading(true);
const comprehensiveRes = await fetch(
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
).then((r) => r.json());
// 检查 stockCode 是否已变更(防止竞态)
if (currentStockCodeRef.current === stockCode) {
if (comprehensiveRes.success)
setComprehensiveData(comprehensiveRes.data);
loadedApisRef.current.comprehensive = true;
}
break;
const [comprehensiveRes, valueChainRes, keyFactorsRes] =
await Promise.all(requests);
case "valueChain":
setValueChainLoading(true);
const valueChainRes = await fetch(
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
).then((r) => r.json());
if (currentStockCodeRef.current === stockCode) {
if (valueChainRes.success) setValueChainData(valueChainRes.data);
loadedApisRef.current.valueChain = true;
}
break;
if (comprehensiveRes.success) setComprehensiveData(comprehensiveRes.data);
if (valueChainRes.success) setValueChainData(valueChainRes.data);
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
} catch (err) {
logger.error("DeepAnalysis", "loadDeepAnalysisData", err, { stockCode });
} finally {
setLoading(false);
}
};
case "keyFactors":
setKeyFactorsLoading(true);
const keyFactorsRes = await fetch(
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
).then((r) => r.json());
if (currentStockCodeRef.current === stockCode) {
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
loadedApisRef.current.keyFactors = true;
}
break;
// stockCode 变更时重新加载数据
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);
}
},
[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);
setExpandedSegments({});
// 加载新数据
loadDeepAnalysisData();
loadedApisRef.current = {
comprehensive: false,
valueChain: false,
keyFactors: false,
};
// 重置为默认 Tab 并加载数据
setActiveTab("strategy");
// 加载默认 Tab 的数据
loadApiData("comprehensive");
}
}, [stockCode]);
}, [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}
loading={loading}
loading={getCurrentLoading()}
cardBg="white"
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
activeTab={activeTab}
onTabChange={handleTabChange}
/>
);
};