refactor: 抽取通用 Tab 容器组件,重构 BasicInfoTab 和 DeepAnalysisTab

新增组件:
- TabPanelContainer: 三级容器,统一 loading 状态 + VStack 布局 + 免责声明
- SubTabContainer: 二级导航容器,支持黑金/默认主题预设

重构:
- BasicInfoTab: 使用 SubTabContainer 替代原有 Tabs 实现
- DeepAnalysisTab: 拆分为 4 个子 Tab(战略分析/业务结构/产业链/发展历程)
- TabContainer: 样式调整,与 SubTabContainer 保持一致

🤖 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 11:55:50 +08:00
parent 96fe919164
commit b8cd520014
15 changed files with 572 additions and 234 deletions

View File

@@ -10,7 +10,7 @@ import {
ConcentrationCard,
ShareholdersTable,
} from "../../components/shareholder";
import TabPanelContainer from "./TabPanelContainer";
import TabPanelContainer from "@components/TabPanelContainer";
interface ShareholderPanelProps {
stockCode: string;

View File

@@ -1,56 +0,0 @@
/**
* Tab 面板通用容器组件
*
* 提供统一的 loading 状态处理和布局包裹
* 用于 ShareholderPanel、ManagementPanel 等 Tab 面板
*/
import React, { memo } from 'react';
import { VStack } from '@chakra-ui/react';
import LoadingState from './LoadingState';
interface TabPanelContainerProps {
/** 是否处于加载状态 */
loading?: boolean;
/** 加载状态显示的文案 */
loadingMessage?: string;
/** 子组件间距,默认 6 */
spacing?: number;
/** 子组件 */
children: React.ReactNode;
}
/**
* Tab 面板通用容器
*
* 功能:
* 1. 统一处理 loading 状态,显示 LoadingState 组件
* 2. 提供 VStack 布局包裹,统一 spacing 和 align
*
* @example
* ```tsx
* <TabPanelContainer loading={loading} loadingMessage="加载数据...">
* <YourContent />
* </TabPanelContainer>
* ```
*/
const TabPanelContainer: React.FC<TabPanelContainerProps> = memo(({
loading = false,
loadingMessage = '加载中...',
spacing = 6,
children,
}) => {
if (loading) {
return <LoadingState message={loadingMessage} />;
}
return (
<VStack spacing={spacing} align="stretch">
{children}
</VStack>
);
});
TabPanelContainer.displayName = 'TabPanelContainer';
export default TabPanelContainer;

View File

@@ -2,7 +2,8 @@
// 组件导出
export { default as LoadingState } from "./LoadingState";
export { default as TabPanelContainer } from "./TabPanelContainer";
// TabPanelContainer 已提升为通用组件,从 @components/TabPanelContainer 导入
export { default as TabPanelContainer } from "@components/TabPanelContainer";
export { default as ShareholderPanel } from "./ShareholderPanel";
export { ManagementPanel } from "./management";
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";

View File

@@ -11,7 +11,7 @@ import {
import { useManagementData } from "../../../hooks/useManagementData";
import { THEME } from "../../config";
import TabPanelContainer from "../TabPanelContainer";
import TabPanelContainer from "@components/TabPanelContainer";
import CategorySection from "./CategorySection";
import type {
ManagementPerson,

View File

@@ -1,21 +1,11 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
// 基本信息 Tab 组件 - 可配置版本(黑金主题)
// 基本信息 Tab 组件 - 使用 SubTabContainer 通用组件
import React from "react";
import {
Card,
CardBody,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Icon,
HStack,
Text,
} from "@chakra-ui/react";
import React, { useMemo } from "react";
import { Card, CardBody } from "@chakra-ui/react";
import SubTabContainer, { type SubTabConfig } from "@components/SubTabContainer";
import { THEME, TAB_CONFIG, getEnabledTabs, type TabConfig } from "./config";
import { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
import {
ShareholderPanel,
ManagementPanel,
@@ -44,13 +34,27 @@ const TAB_COMPONENTS: Record<string, React.FC<any>> = {
business: BusinessInfoPanel,
};
/**
* 构建 SubTabContainer 所需的 tabs 配置
*/
const buildTabsConfig = (enabledKeys?: string[]): SubTabConfig[] => {
const enabledTabs = getEnabledTabs(enabledKeys);
return enabledTabs.map((tab) => ({
key: tab.key,
name: tab.name,
icon: tab.icon,
component: TAB_COMPONENTS[tab.key],
}));
};
/**
* 基本信息 Tab 组件
*
* 特性:
* - 使用 SubTabContainer 通用组件
* - 可配置显示哪些 TabenabledTabs
* - 黑金主题
* - 懒加载isLazy
* - 懒加载
* - 支持 Tab 变更回调
*/
const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
@@ -60,79 +64,19 @@ const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
defaultTabIndex = 0,
onTabChange,
}) => {
// 获取启用的 Tab 配置
const tabs = getEnabledTabs(enabledTabs);
// 处理 Tab 变更
const handleTabChange = (index: number) => {
if (onTabChange && tabs[index]) {
onTabChange(index, tabs[index].key);
}
};
// 渲染单个 Tab 内容
const renderTabContent = (tab: TabConfig) => {
const Component = TAB_COMPONENTS[tab.key];
if (!Component) return null;
// business Tab 需要 basicInfo其他需要 stockCode
if (tab.key === "business") {
return <Component basicInfo={basicInfo} />;
}
return <Component stockCode={stockCode} />;
};
// 构建 tabs 配置(缓存避免重复计算)
const tabs = useMemo(() => buildTabsConfig(enabledTabs), [enabledTabs]);
return (
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
<CardBody p={0}>
<Tabs
isLazy
variant="unstyled"
<SubTabContainer
tabs={tabs}
componentProps={{ stockCode, basicInfo }}
defaultIndex={defaultTabIndex}
onChange={handleTabChange}
>
<TabList
bg={THEME.bg}
borderBottom="1px solid"
borderColor={THEME.border}
px={4}
py={2}
flexWrap="wrap"
gap={2}
>
{tabs.map((tab) => (
<Tab
key={tab.key}
color={THEME.tabUnselected.color}
borderRadius="full"
px={4}
py={2}
fontSize="sm"
_selected={{
bg: THEME.tabSelected.bg,
color: THEME.tabSelected.color,
fontWeight: "bold",
}}
_hover={{
bg: THEME.tableHoverBg,
}}
>
<HStack spacing={2}>
<Icon as={tab.icon} boxSize={4} />
<Text>{tab.name}</Text>
</HStack>
</Tab>
))}
</TabList>
<TabPanels p={4}>
{tabs.map((tab) => (
<TabPanel key={tab.key} p={0}>
{renderTabContent(tab)}
</TabPanel>
))}
</TabPanels>
</Tabs>
onTabChange={onTabChange}
themePreset="blackGold"
/>
</CardBody>
</Card>
);

View File

@@ -1,23 +1,36 @@
/**
* 深度分析 Tab 主组件
*
* 组合所有子组件,显示公司深度分析内容
* 使用 SubTabContainer 二级导航组件,分为 4 个子 Tab
* 1. 战略分析 - 核心定位 + 战略分析 + 竞争地位
* 2. 业务结构 - 业务结构树 + 业务板块详情
* 3. 产业链 - 产业链分析(独立,含 Sankey 图)
* 4. 发展历程 - 关键因素 + 时间线
*/
import React from 'react';
import { VStack, Center, Text, Spinner, Grid, GridItem } from '@chakra-ui/react';
import {
CorePositioningCard,
CompetitiveAnalysisCard,
BusinessStructureCard,
ValueChainCard,
KeyFactorsCard,
TimelineCard,
BusinessSegmentsCard,
StrategyAnalysisCard,
} from './components';
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';
// 主题配置(与 BasicInfoTab 保持一致)
const THEME = {
cardBg: 'gray.900',
border: 'rgba(212, 175, 55, 0.3)',
};
/**
* Tab 配置
*/
const DEEP_ANALYSIS_TABS: SubTabConfig[] = [
{ key: 'strategy', name: '战略分析', icon: FaBrain, component: StrategyTab },
{ key: 'business', name: '业务结构', icon: FaBuilding, component: BusinessTab },
{ key: 'valueChain', name: '产业链', icon: FaLink, component: ValueChainTab },
{ key: 'development', name: '发展历程', icon: FaHistory, component: DevelopmentTab },
];
const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
comprehensiveData,
valueChainData,
@@ -40,74 +53,22 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
}
return (
<VStack spacing={6} align="stretch">
{/* 核心定位卡片 */}
{comprehensiveData?.qualitative_analysis && (
<CorePositioningCard
qualitativeAnalysis={comprehensiveData.qualitative_analysis}
cardBg={cardBg}
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
<CardBody p={0}>
<SubTabContainer
tabs={DEEP_ANALYSIS_TABS}
componentProps={{
comprehensiveData,
valueChainData,
keyFactorsData,
cardBg,
expandedSegments,
onToggleSegment,
}}
themePreset="blackGold"
/>
)}
{/* 战略分析 */}
{comprehensiveData?.qualitative_analysis?.strategy && (
<StrategyAnalysisCard
strategy={comprehensiveData.qualitative_analysis.strategy}
cardBg={cardBg}
/>
)}
{/* 竞争地位分析 */}
{comprehensiveData?.competitive_position && (
<CompetitiveAnalysisCard comprehensiveData={comprehensiveData} />
)}
{/* 业务结构分析 */}
{comprehensiveData?.business_structure &&
comprehensiveData.business_structure.length > 0 && (
<BusinessStructureCard
businessStructure={comprehensiveData.business_structure}
cardBg={cardBg}
/>
)}
{/* 业务板块详情 */}
{comprehensiveData?.business_segments &&
comprehensiveData.business_segments.length > 0 && (
<BusinessSegmentsCard
businessSegments={comprehensiveData.business_segments}
expandedSegments={expandedSegments}
onToggleSegment={onToggleSegment}
cardBg={cardBg}
/>
)}
{/* 产业链分析 */}
{valueChainData && (
<ValueChainCard valueChainData={valueChainData} cardBg={cardBg} />
)}
{/* 关键因素与发展时间线 */}
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.key_factors && (
<KeyFactorsCard
keyFactors={keyFactorsData.key_factors}
cardBg={cardBg}
/>
)}
</GridItem>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.development_timeline && (
<TimelineCard
developmentTimeline={keyFactorsData.development_timeline}
cardBg={cardBg}
/>
)}
</GridItem>
</Grid>
</VStack>
</CardBody>
</Card>
);
};

View File

@@ -0,0 +1,50 @@
/**
* 业务结构 Tab
*
* 包含:业务结构分析 + 业务板块详情
*/
import React from 'react';
import TabPanelContainer from '@components/TabPanelContainer';
import { BusinessStructureCard, BusinessSegmentsCard } from '../components';
import type { ComprehensiveData } from '../types';
export interface BusinessTabProps {
comprehensiveData?: ComprehensiveData;
cardBg?: string;
expandedSegments: Record<number, boolean>;
onToggleSegment: (index: number) => void;
}
const BusinessTab: React.FC<BusinessTabProps> = ({
comprehensiveData,
cardBg,
expandedSegments,
onToggleSegment,
}) => {
return (
<TabPanelContainer showDisclaimer>
{/* 业务结构分析 */}
{comprehensiveData?.business_structure &&
comprehensiveData.business_structure.length > 0 && (
<BusinessStructureCard
businessStructure={comprehensiveData.business_structure}
cardBg={cardBg}
/>
)}
{/* 业务板块详情 */}
{comprehensiveData?.business_segments &&
comprehensiveData.business_segments.length > 0 && (
<BusinessSegmentsCard
businessSegments={comprehensiveData.business_segments}
expandedSegments={expandedSegments}
onToggleSegment={onToggleSegment}
cardBg={cardBg}
/>
)}
</TabPanelContainer>
);
};
export default BusinessTab;

View File

@@ -0,0 +1,47 @@
/**
* 发展历程 Tab
*
* 包含:关键因素 + 发展时间线Grid 布局)
*/
import React from 'react';
import { Grid, GridItem } from '@chakra-ui/react';
import TabPanelContainer from '@components/TabPanelContainer';
import { KeyFactorsCard, TimelineCard } from '../components';
import type { KeyFactorsData } from '../types';
export interface DevelopmentTabProps {
keyFactorsData?: KeyFactorsData;
cardBg?: string;
}
const DevelopmentTab: React.FC<DevelopmentTabProps> = ({
keyFactorsData,
cardBg,
}) => {
return (
<TabPanelContainer showDisclaimer>
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.key_factors && (
<KeyFactorsCard
keyFactors={keyFactorsData.key_factors}
cardBg={cardBg}
/>
)}
</GridItem>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.development_timeline && (
<TimelineCard
developmentTimeline={keyFactorsData.development_timeline}
cardBg={cardBg}
/>
)}
</GridItem>
</Grid>
</TabPanelContainer>
);
};
export default DevelopmentTab;

View File

@@ -0,0 +1,51 @@
/**
* 战略分析 Tab
*
* 包含:核心定位 + 战略分析 + 竞争地位分析
*/
import React from 'react';
import TabPanelContainer from '@components/TabPanelContainer';
import {
CorePositioningCard,
StrategyAnalysisCard,
CompetitiveAnalysisCard,
} from '../components';
import type { ComprehensiveData } from '../types';
export interface StrategyTabProps {
comprehensiveData?: ComprehensiveData;
cardBg?: string;
}
const StrategyTab: React.FC<StrategyTabProps> = ({
comprehensiveData,
cardBg,
}) => {
return (
<TabPanelContainer showDisclaimer>
{/* 核心定位卡片 */}
{comprehensiveData?.qualitative_analysis && (
<CorePositioningCard
qualitativeAnalysis={comprehensiveData.qualitative_analysis}
cardBg={cardBg}
/>
)}
{/* 战略分析 */}
{comprehensiveData?.qualitative_analysis?.strategy && (
<StrategyAnalysisCard
strategy={comprehensiveData.qualitative_analysis.strategy}
cardBg={cardBg}
/>
)}
{/* 竞争地位分析 */}
{comprehensiveData?.competitive_position && (
<CompetitiveAnalysisCard comprehensiveData={comprehensiveData} />
)}
</TabPanelContainer>
);
};
export default StrategyTab;

View File

@@ -0,0 +1,30 @@
/**
* 产业链 Tab
*
* 包含:产业链分析(层级视图 + Sankey 流向图)
*/
import React from 'react';
import TabPanelContainer from '@components/TabPanelContainer';
import { ValueChainCard } from '../components';
import type { ValueChainData } from '../types';
export interface ValueChainTabProps {
valueChainData?: ValueChainData;
cardBg?: string;
}
const ValueChainTab: React.FC<ValueChainTabProps> = ({
valueChainData,
cardBg,
}) => {
return (
<TabPanelContainer showDisclaimer>
{valueChainData && (
<ValueChainCard valueChainData={valueChainData} cardBg={cardBg} />
)}
</TabPanelContainer>
);
};
export default ValueChainTab;

View File

@@ -0,0 +1,14 @@
/**
* DeepAnalysisTab - Tab 组件导出
*/
export { default as StrategyTab } from './StrategyTab';
export { default as BusinessTab } from './BusinessTab';
export { default as ValueChainTab } from './ValueChainTab';
export { default as DevelopmentTab } from './DevelopmentTab';
// 导出类型
export type { StrategyTabProps } from './StrategyTab';
export type { BusinessTabProps } from './BusinessTab';
export type { ValueChainTabProps } from './ValueChainTab';
export type { DevelopmentTabProps } from './DevelopmentTab';