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. 业务结构 - 业务结构树 + 业务板块详情
|
* 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 (
|
||||||
|
<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">
|
<Center h="200px">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
<Spinner size="xl" color="blue.500" />
|
<Spinner size="xl" color="blue.500" />
|
||||||
<Text>加载深度分析数据...</Text>
|
<Text color="gray.400">加载数据中...</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</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,
|
||||||
|
|||||||
@@ -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 类型 ====================
|
||||||
|
|||||||
@@ -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 () => {
|
* 加载指定接口的数据
|
||||||
|
*/
|
||||||
|
const loadApiData = useCallback(
|
||||||
|
async (apiKey) => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
|
|
||||||
setLoading(true);
|
// 已加载则跳过
|
||||||
|
if (loadedApisRef.current[apiKey]) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requests = [
|
switch (apiKey) {
|
||||||
fetch(
|
case "comprehensive":
|
||||||
|
setComprehensiveLoading(true);
|
||||||
|
const comprehensiveRes = await fetch(
|
||||||
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
|
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
|
||||||
).then((r) => r.json()),
|
).then((r) => r.json());
|
||||||
fetch(
|
// 检查 stockCode 是否已变更(防止竞态)
|
||||||
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
|
if (currentStockCodeRef.current === stockCode) {
|
||||||
).then((r) => r.json()),
|
if (comprehensiveRes.success)
|
||||||
fetch(
|
setComprehensiveData(comprehensiveRes.data);
|
||||||
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
|
loadedApisRef.current.comprehensive = true;
|
||||||
).then((r) => r.json()),
|
|
||||||
];
|
|
||||||
|
|
||||||
const [comprehensiveRes, valueChainRes, keyFactorsRes] =
|
|
||||||
await Promise.all(requests);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
break;
|
||||||
|
|
||||||
// stockCode 变更时重新加载数据
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user