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

View File

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

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/DeepAnalysis/index.js // 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 { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig"; import { getApiBase } from "@utils/apiConfig";
@@ -10,27 +10,55 @@ import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab";
const API_BASE_URL = getApiBase(); 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 {Object} props
* @param {string} props.stockCode - 股票代码 * @param {string} props.stockCode - 股票代码
*/ */
const DeepAnalysis = ({ stockCode }) => { const DeepAnalysis = ({ stockCode }) => {
// 当前 Tab
const [activeTab, setActiveTab] = useState("strategy");
// 数据状态 // 数据状态
const [comprehensiveData, setComprehensiveData] = useState(null); const [comprehensiveData, setComprehensiveData] = useState(null);
const [valueChainData, setValueChainData] = useState(null); const [valueChainData, setValueChainData] = useState(null);
const [keyFactorsData, setKeyFactorsData] = 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({}); const [expandedSegments, setExpandedSegments] = useState({});
// 用于追踪当前 stockCode避免竞态条件
const currentStockCodeRef = useRef(stockCode);
// 切换业务板块展开状态 // 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => { const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({ 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 { try {
const requests = [ switch (apiKey) {
fetch( case "comprehensive":
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}` setComprehensiveLoading(true);
).then((r) => r.json()), const comprehensiveRes = await fetch(
fetch( `${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}` ).then((r) => r.json());
).then((r) => r.json()), // 检查 stockCode 是否已变更(防止竞态)
fetch( if (currentStockCodeRef.current === stockCode) {
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}` if (comprehensiveRes.success)
).then((r) => r.json()), setComprehensiveData(comprehensiveRes.data);
]; loadedApisRef.current.comprehensive = true;
}
break;
const [comprehensiveRes, valueChainRes, keyFactorsRes] = case "valueChain":
await Promise.all(requests); 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); case "keyFactors":
if (valueChainRes.success) setValueChainData(valueChainRes.data); setKeyFactorsLoading(true);
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data); const keyFactorsRes = await fetch(
} catch (err) { `${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
logger.error("DeepAnalysis", "loadDeepAnalysisData", err, { stockCode }); ).then((r) => r.json());
} finally { if (currentStockCodeRef.current === stockCode) {
setLoading(false); 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(() => { useEffect(() => {
if (stockCode) { if (stockCode) {
// 重置数据 // 更新 ref
currentStockCodeRef.current = stockCode;
// 重置所有数据和状态
setComprehensiveData(null); setComprehensiveData(null);
setValueChainData(null); setValueChainData(null);
setKeyFactorsData(null); setKeyFactorsData(null);
setExpandedSegments({}); setExpandedSegments({});
// 加载新数据 loadedApisRef.current = {
loadDeepAnalysisData(); 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 ( return (
<DeepAnalysisTab <DeepAnalysisTab
comprehensiveData={comprehensiveData} comprehensiveData={comprehensiveData}
valueChainData={valueChainData} valueChainData={valueChainData}
keyFactorsData={keyFactorsData} keyFactorsData={keyFactorsData}
loading={loading} loading={getCurrentLoading()}
cardBg="white" cardBg="white"
expandedSegments={expandedSegments} expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion} onToggleSegment={toggleSegmentExpansion}
activeTab={activeTab}
onTabChange={handleTabChange}
/> />
); );
}; };