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:
@@ -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,
|
||||
|
||||
@@ -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 类型 ====================
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user