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:
195
src/components/SubTabContainer/index.tsx
Normal file
195
src/components/SubTabContainer/index.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* SubTabContainer - 二级导航容器组件
|
||||||
|
*
|
||||||
|
* 用于模块内的子功能切换(如公司档案下的股权结构、管理团队等)
|
||||||
|
* 与 TabContainer(一级导航)区分:无 Card 包裹,直接融入父容器
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <SubTabContainer
|
||||||
|
* tabs={[
|
||||||
|
* { key: 'tab1', name: 'Tab 1', icon: FaHome, component: Tab1 },
|
||||||
|
* { key: 'tab2', name: 'Tab 2', icon: FaUser, component: Tab2 },
|
||||||
|
* ]}
|
||||||
|
* componentProps={{ stockCode: '000001' }}
|
||||||
|
* onTabChange={(index, key) => console.log('切换到', key)}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
TabPanels,
|
||||||
|
Tab,
|
||||||
|
TabPanel,
|
||||||
|
Icon,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import type { ComponentType } from 'react';
|
||||||
|
import type { IconType } from 'react-icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab 配置项
|
||||||
|
*/
|
||||||
|
export interface SubTabConfig {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
icon?: IconType | ComponentType;
|
||||||
|
component?: ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题配置
|
||||||
|
*/
|
||||||
|
export interface SubTabTheme {
|
||||||
|
bg: string;
|
||||||
|
borderColor: string;
|
||||||
|
tabSelectedBg: string;
|
||||||
|
tabSelectedColor: string;
|
||||||
|
tabUnselectedColor: string;
|
||||||
|
tabHoverBg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预设主题
|
||||||
|
*/
|
||||||
|
const THEME_PRESETS: Record<string, SubTabTheme> = {
|
||||||
|
blackGold: {
|
||||||
|
bg: 'gray.900',
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
tabSelectedBg: '#D4AF37',
|
||||||
|
tabSelectedColor: 'gray.900',
|
||||||
|
tabUnselectedColor: '#D4AF37',
|
||||||
|
tabHoverBg: 'gray.600',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
bg: 'white',
|
||||||
|
borderColor: 'gray.200',
|
||||||
|
tabSelectedBg: 'blue.500',
|
||||||
|
tabSelectedColor: 'white',
|
||||||
|
tabUnselectedColor: 'gray.600',
|
||||||
|
tabHoverBg: 'gray.100',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SubTabContainerProps {
|
||||||
|
/** Tab 配置数组 */
|
||||||
|
tabs: SubTabConfig[];
|
||||||
|
/** 传递给 Tab 内容组件的 props */
|
||||||
|
componentProps?: Record<string, any>;
|
||||||
|
/** 默认选中的 Tab 索引 */
|
||||||
|
defaultIndex?: number;
|
||||||
|
/** 受控模式下的当前索引 */
|
||||||
|
index?: number;
|
||||||
|
/** Tab 变更回调 */
|
||||||
|
onTabChange?: (index: number, tabKey: string) => void;
|
||||||
|
/** 主题预设 */
|
||||||
|
themePreset?: 'blackGold' | 'default';
|
||||||
|
/** 自定义主题(优先级高于预设) */
|
||||||
|
theme?: Partial<SubTabTheme>;
|
||||||
|
/** 内容区内边距 */
|
||||||
|
contentPadding?: number;
|
||||||
|
/** 是否懒加载 */
|
||||||
|
isLazy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubTabContainer: React.FC<SubTabContainerProps> = ({
|
||||||
|
tabs,
|
||||||
|
componentProps = {},
|
||||||
|
defaultIndex = 0,
|
||||||
|
index: controlledIndex,
|
||||||
|
onTabChange,
|
||||||
|
themePreset = 'blackGold',
|
||||||
|
theme: customTheme,
|
||||||
|
contentPadding = 4,
|
||||||
|
isLazy = true,
|
||||||
|
}) => {
|
||||||
|
// 内部状态(非受控模式)
|
||||||
|
const [internalIndex, setInternalIndex] = useState(defaultIndex);
|
||||||
|
|
||||||
|
// 当前索引
|
||||||
|
const currentIndex = controlledIndex ?? internalIndex;
|
||||||
|
|
||||||
|
// 合并主题
|
||||||
|
const theme: SubTabTheme = {
|
||||||
|
...THEME_PRESETS[themePreset],
|
||||||
|
...customTheme,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 Tab 切换
|
||||||
|
*/
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(newIndex: number) => {
|
||||||
|
const tabKey = tabs[newIndex]?.key || '';
|
||||||
|
onTabChange?.(newIndex, tabKey);
|
||||||
|
|
||||||
|
if (controlledIndex === undefined) {
|
||||||
|
setInternalIndex(newIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tabs, onTabChange, controlledIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Tabs
|
||||||
|
isLazy={isLazy}
|
||||||
|
variant="unstyled"
|
||||||
|
index={currentIndex}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
bg={theme.bg}
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor={theme.borderColor}
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
color={theme.tabUnselectedColor}
|
||||||
|
borderRadius="full"
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
fontSize="sm"
|
||||||
|
_selected={{
|
||||||
|
bg: theme.tabSelectedBg,
|
||||||
|
color: theme.tabSelectedColor,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
_hover={{
|
||||||
|
bg: theme.tabHoverBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{tab.icon && <Icon as={tab.icon} boxSize={4} />}
|
||||||
|
<Text>{tab.name}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels p={contentPadding}>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Component = tab.component;
|
||||||
|
return (
|
||||||
|
<TabPanel key={tab.key} p={0}>
|
||||||
|
{Component ? <Component {...componentProps} /> : null}
|
||||||
|
</TabPanel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubTabContainer;
|
||||||
@@ -15,30 +15,36 @@ const TabNavigation: React.FC<TabNavigationProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<TabList
|
<TabList
|
||||||
py={4}
|
|
||||||
bg={themeColors.bg}
|
bg={themeColors.bg}
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor={themeColors.dividerColor}
|
||||||
borderTopLeftRadius={borderRadius}
|
borderTopLeftRadius={borderRadius}
|
||||||
borderTopRightRadius={borderRadius}
|
borderTopRightRadius={borderRadius}
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
color={themeColors.unselectedText}
|
color={themeColors.unselectedText}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
px={4}
|
px={4}
|
||||||
py={2}
|
py={2}
|
||||||
|
fontSize="sm"
|
||||||
_selected={{
|
_selected={{
|
||||||
bg: themeColors.selectedBg,
|
bg: themeColors.selectedBg,
|
||||||
color: themeColors.selectedText,
|
color: themeColors.selectedText,
|
||||||
|
fontWeight: 'bold',
|
||||||
}}
|
}}
|
||||||
_hover={{
|
_hover={{
|
||||||
color: themeColors.selectedText,
|
bg: 'whiteAlpha.100',
|
||||||
}}
|
}}
|
||||||
mr={index < tabs.length - 1 ? 2 : 0}
|
|
||||||
>
|
>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{tab.icon && <Icon as={tab.icon} boxSize="18px" />}
|
{tab.icon && <Icon as={tab.icon} boxSize={4} />}
|
||||||
<Text fontSize="15px">{tab.name}</Text>
|
<Text>{tab.name}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
Divider,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import TabNavigation from './TabNavigation';
|
import TabNavigation from './TabNavigation';
|
||||||
@@ -113,8 +112,7 @@ const TabContainer: React.FC<TabContainerProps> = ({
|
|||||||
<CardBody p={0}>
|
<CardBody p={0}>
|
||||||
<Tabs
|
<Tabs
|
||||||
isLazy={isLazy}
|
isLazy={isLazy}
|
||||||
variant="soft-rounded"
|
variant="unstyled"
|
||||||
colorScheme="blue"
|
|
||||||
size={size}
|
size={size}
|
||||||
index={currentIndex}
|
index={currentIndex}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
@@ -126,9 +124,6 @@ const TabContainer: React.FC<TabContainerProps> = ({
|
|||||||
borderRadius={borderRadius}
|
borderRadius={borderRadius}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 分割线 */}
|
|
||||||
{showDivider && <Divider borderColor={themeColors.dividerColor} />}
|
|
||||||
|
|
||||||
{/* Tab 内容面板 */}
|
{/* Tab 内容面板 */}
|
||||||
<TabPanels>{renderTabPanels()}</TabPanels>
|
<TabPanels>{renderTabPanels()}</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
100
src/components/TabPanelContainer/index.tsx
Normal file
100
src/components/TabPanelContainer/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* TabPanelContainer - Tab 面板通用容器组件
|
||||||
|
*
|
||||||
|
* 提供统一的:
|
||||||
|
* - Loading 状态处理
|
||||||
|
* - VStack 布局
|
||||||
|
* - 免责声明(可选)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <TabPanelContainer loading={loading} showDisclaimer>
|
||||||
|
* <YourContent />
|
||||||
|
* </TabPanelContainer>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { VStack, Center, Spinner, Text, Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
// 默认免责声明文案
|
||||||
|
const DEFAULT_DISCLAIMER =
|
||||||
|
'免责声明:本内容由AI模型基于新闻、公告、研报等公开信息自动分析和生成,未经许可严禁转载。所有内容仅供参考,不构成任何投资建议,请投资者注意风险,独立审慎决策。';
|
||||||
|
|
||||||
|
export interface TabPanelContainerProps {
|
||||||
|
/** 是否处于加载状态 */
|
||||||
|
loading?: boolean;
|
||||||
|
/** 加载状态显示的文案 */
|
||||||
|
loadingMessage?: string;
|
||||||
|
/** 加载状态高度 */
|
||||||
|
loadingHeight?: string;
|
||||||
|
/** 子组件间距,默认 6 */
|
||||||
|
spacing?: number;
|
||||||
|
/** 内边距,默认 4 */
|
||||||
|
padding?: number;
|
||||||
|
/** 是否显示免责声明,默认 false */
|
||||||
|
showDisclaimer?: boolean;
|
||||||
|
/** 自定义免责声明文案 */
|
||||||
|
disclaimerText?: string;
|
||||||
|
/** 子组件 */
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载状态组件
|
||||||
|
*/
|
||||||
|
const LoadingState: React.FC<{ message: string; height: string }> = ({
|
||||||
|
message,
|
||||||
|
height,
|
||||||
|
}) => (
|
||||||
|
<Center h={height}>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Spinner size="lg" color="blue.500" thickness="3px" />
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 免责声明组件
|
||||||
|
*/
|
||||||
|
const DisclaimerText: React.FC<{ text: string }> = ({ text }) => (
|
||||||
|
<Text mt={4} color="gray.500" fontSize="12px" lineHeight="1.5">
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab 面板通用容器
|
||||||
|
*/
|
||||||
|
const TabPanelContainer: React.FC<TabPanelContainerProps> = memo(
|
||||||
|
({
|
||||||
|
loading = false,
|
||||||
|
loadingMessage = '加载中...',
|
||||||
|
loadingHeight = '200px',
|
||||||
|
spacing = 6,
|
||||||
|
padding = 4,
|
||||||
|
showDisclaimer = false,
|
||||||
|
disclaimerText = DEFAULT_DISCLAIMER,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingState message={loadingMessage} height={loadingHeight} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p={padding}>
|
||||||
|
<VStack spacing={spacing} align="stretch">
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
{showDisclaimer && <DisclaimerText text={disclaimerText} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TabPanelContainer.displayName = 'TabPanelContainer';
|
||||||
|
|
||||||
|
export default TabPanelContainer;
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ConcentrationCard,
|
ConcentrationCard,
|
||||||
ShareholdersTable,
|
ShareholdersTable,
|
||||||
} from "../../components/shareholder";
|
} from "../../components/shareholder";
|
||||||
import TabPanelContainer from "./TabPanelContainer";
|
import TabPanelContainer from "@components/TabPanelContainer";
|
||||||
|
|
||||||
interface ShareholderPanelProps {
|
interface ShareholderPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
// 组件导出
|
// 组件导出
|
||||||
|
|
||||||
export { default as LoadingState } from "./LoadingState";
|
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 { default as ShareholderPanel } from "./ShareholderPanel";
|
||||||
export { ManagementPanel } from "./management";
|
export { ManagementPanel } from "./management";
|
||||||
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
|
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
|
|
||||||
import { useManagementData } from "../../../hooks/useManagementData";
|
import { useManagementData } from "../../../hooks/useManagementData";
|
||||||
import { THEME } from "../../config";
|
import { THEME } from "../../config";
|
||||||
import TabPanelContainer from "../TabPanelContainer";
|
import TabPanelContainer from "@components/TabPanelContainer";
|
||||||
import CategorySection from "./CategorySection";
|
import CategorySection from "./CategorySection";
|
||||||
import type {
|
import type {
|
||||||
ManagementPerson,
|
ManagementPerson,
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
// src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
|
||||||
// 基本信息 Tab 组件 - 可配置版本(黑金主题)
|
// 基本信息 Tab 组件 - 使用 SubTabContainer 通用组件
|
||||||
|
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import {
|
import { Card, CardBody } from "@chakra-ui/react";
|
||||||
Card,
|
import SubTabContainer, { type SubTabConfig } from "@components/SubTabContainer";
|
||||||
CardBody,
|
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
Icon,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { THEME, TAB_CONFIG, getEnabledTabs, type TabConfig } from "./config";
|
import { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
|
||||||
import {
|
import {
|
||||||
ShareholderPanel,
|
ShareholderPanel,
|
||||||
ManagementPanel,
|
ManagementPanel,
|
||||||
@@ -44,13 +34,27 @@ const TAB_COMPONENTS: Record<string, React.FC<any>> = {
|
|||||||
business: BusinessInfoPanel,
|
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 组件
|
* 基本信息 Tab 组件
|
||||||
*
|
*
|
||||||
* 特性:
|
* 特性:
|
||||||
|
* - 使用 SubTabContainer 通用组件
|
||||||
* - 可配置显示哪些 Tab(enabledTabs)
|
* - 可配置显示哪些 Tab(enabledTabs)
|
||||||
* - 黑金主题
|
* - 黑金主题
|
||||||
* - 懒加载(isLazy)
|
* - 懒加载
|
||||||
* - 支持 Tab 变更回调
|
* - 支持 Tab 变更回调
|
||||||
*/
|
*/
|
||||||
const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
|
const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
|
||||||
@@ -60,79 +64,19 @@ const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
|
|||||||
defaultTabIndex = 0,
|
defaultTabIndex = 0,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
}) => {
|
}) => {
|
||||||
// 获取启用的 Tab 配置
|
// 构建 tabs 配置(缓存避免重复计算)
|
||||||
const tabs = getEnabledTabs(enabledTabs);
|
const tabs = useMemo(() => buildTabsConfig(enabledTabs), [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} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
|
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
|
||||||
<CardBody p={0}>
|
<CardBody p={0}>
|
||||||
<Tabs
|
<SubTabContainer
|
||||||
isLazy
|
tabs={tabs}
|
||||||
variant="unstyled"
|
componentProps={{ stockCode, basicInfo }}
|
||||||
defaultIndex={defaultTabIndex}
|
defaultIndex={defaultTabIndex}
|
||||||
onChange={handleTabChange}
|
onTabChange={onTabChange}
|
||||||
>
|
themePreset="blackGold"
|
||||||
<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>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* 深度分析 Tab 主组件
|
* 深度分析 Tab 主组件
|
||||||
*
|
*
|
||||||
* 组合所有子组件,显示公司深度分析内容
|
* 使用 SubTabContainer 二级导航组件,分为 4 个子 Tab:
|
||||||
|
* 1. 战略分析 - 核心定位 + 战略分析 + 竞争地位
|
||||||
|
* 2. 业务结构 - 业务结构树 + 业务板块详情
|
||||||
|
* 3. 产业链 - 产业链分析(独立,含 Sankey 图)
|
||||||
|
* 4. 发展历程 - 关键因素 + 时间线
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { VStack, Center, Text, Spinner, Grid, GridItem } from '@chakra-ui/react';
|
import { Card, CardBody, Center, VStack, Spinner, Text } from '@chakra-ui/react';
|
||||||
import {
|
import { FaBrain, FaBuilding, FaLink, FaHistory } from 'react-icons/fa';
|
||||||
CorePositioningCard,
|
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
|
||||||
CompetitiveAnalysisCard,
|
import { StrategyTab, BusinessTab, ValueChainTab, DevelopmentTab } from './tabs';
|
||||||
BusinessStructureCard,
|
|
||||||
ValueChainCard,
|
|
||||||
KeyFactorsCard,
|
|
||||||
TimelineCard,
|
|
||||||
BusinessSegmentsCard,
|
|
||||||
StrategyAnalysisCard,
|
|
||||||
} from './components';
|
|
||||||
import type { DeepAnalysisTabProps } from './types';
|
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> = ({
|
const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
|
||||||
comprehensiveData,
|
comprehensiveData,
|
||||||
valueChainData,
|
valueChainData,
|
||||||
@@ -40,74 +53,22 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={6} align="stretch">
|
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
|
||||||
{/* 核心定位卡片 */}
|
<CardBody p={0}>
|
||||||
{comprehensiveData?.qualitative_analysis && (
|
<SubTabContainer
|
||||||
<CorePositioningCard
|
tabs={DEEP_ANALYSIS_TABS}
|
||||||
qualitativeAnalysis={comprehensiveData.qualitative_analysis}
|
componentProps={{
|
||||||
cardBg={cardBg}
|
comprehensiveData,
|
||||||
|
valueChainData,
|
||||||
|
keyFactorsData,
|
||||||
|
cardBg,
|
||||||
|
expandedSegments,
|
||||||
|
onToggleSegment,
|
||||||
|
}}
|
||||||
|
themePreset="blackGold"
|
||||||
/>
|
/>
|
||||||
)}
|
</CardBody>
|
||||||
|
</Card>
|
||||||
{/* 战略分析 */}
|
|
||||||
{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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user