diff --git a/src/components/SubTabContainer/index.tsx b/src/components/SubTabContainer/index.tsx new file mode 100644 index 00000000..6cbb994c --- /dev/null +++ b/src/components/SubTabContainer/index.tsx @@ -0,0 +1,195 @@ +/** + * SubTabContainer - 二级导航容器组件 + * + * 用于模块内的子功能切换(如公司档案下的股权结构、管理团队等) + * 与 TabContainer(一级导航)区分:无 Card 包裹,直接融入父容器 + * + * @example + * ```tsx + * 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; +} + +/** + * 主题配置 + */ +export interface SubTabTheme { + bg: string; + borderColor: string; + tabSelectedBg: string; + tabSelectedColor: string; + tabUnselectedColor: string; + tabHoverBg: string; +} + +/** + * 预设主题 + */ +const THEME_PRESETS: Record = { + 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; + /** 默认选中的 Tab 索引 */ + defaultIndex?: number; + /** 受控模式下的当前索引 */ + index?: number; + /** Tab 变更回调 */ + onTabChange?: (index: number, tabKey: string) => void; + /** 主题预设 */ + themePreset?: 'blackGold' | 'default'; + /** 自定义主题(优先级高于预设) */ + theme?: Partial; + /** 内容区内边距 */ + contentPadding?: number; + /** 是否懒加载 */ + isLazy?: boolean; +} + +const SubTabContainer: React.FC = ({ + 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 ( + + + + {tabs.map((tab) => ( + + + {tab.icon && } + {tab.name} + + + ))} + + + + {tabs.map((tab) => { + const Component = tab.component; + return ( + + {Component ? : null} + + ); + })} + + + + ); +}; + +export default SubTabContainer; diff --git a/src/components/TabContainer/TabNavigation.tsx b/src/components/TabContainer/TabNavigation.tsx index ac6994b2..be43930e 100644 --- a/src/components/TabContainer/TabNavigation.tsx +++ b/src/components/TabContainer/TabNavigation.tsx @@ -15,30 +15,36 @@ const TabNavigation: React.FC = ({ }) => { return ( - {tabs.map((tab, index) => ( + {tabs.map((tab) => ( - {tab.icon && } - {tab.name} + {tab.icon && } + {tab.name} ))} diff --git a/src/components/TabContainer/index.tsx b/src/components/TabContainer/index.tsx index c40e3269..17bf6fc0 100644 --- a/src/components/TabContainer/index.tsx +++ b/src/components/TabContainer/index.tsx @@ -34,7 +34,6 @@ import { Tabs, TabPanels, TabPanel, - Divider, } from '@chakra-ui/react'; import TabNavigation from './TabNavigation'; @@ -113,8 +112,7 @@ const TabContainer: React.FC = ({ = ({ borderRadius={borderRadius} /> - {/* 分割线 */} - {showDivider && } - {/* Tab 内容面板 */} {renderTabPanels()} diff --git a/src/components/TabPanelContainer/index.tsx b/src/components/TabPanelContainer/index.tsx new file mode 100644 index 00000000..be5eaddb --- /dev/null +++ b/src/components/TabPanelContainer/index.tsx @@ -0,0 +1,100 @@ +/** + * TabPanelContainer - Tab 面板通用容器组件 + * + * 提供统一的: + * - Loading 状态处理 + * - VStack 布局 + * - 免责声明(可选) + * + * @example + * ```tsx + * + * + * + * ``` + */ + +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, +}) => ( +
+ + + + {message} + + +
+); + +/** + * 免责声明组件 + */ +const DisclaimerText: React.FC<{ text: string }> = ({ text }) => ( + + {text} + +); + +/** + * Tab 面板通用容器 + */ +const TabPanelContainer: React.FC = memo( + ({ + loading = false, + loadingMessage = '加载中...', + loadingHeight = '200px', + spacing = 6, + padding = 4, + showDisclaimer = false, + disclaimerText = DEFAULT_DISCLAIMER, + children, + }) => { + if (loading) { + return ; + } + + return ( + + + {children} + + {showDisclaimer && } + + ); + } +); + +TabPanelContainer.displayName = 'TabPanelContainer'; + +export default TabPanelContainer; diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx index 0343befe..4b9a4989 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx @@ -10,7 +10,7 @@ import { ConcentrationCard, ShareholdersTable, } from "../../components/shareholder"; -import TabPanelContainer from "./TabPanelContainer"; +import TabPanelContainer from "@components/TabPanelContainer"; interface ShareholderPanelProps { stockCode: string; diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/TabPanelContainer.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/TabPanelContainer.tsx deleted file mode 100644 index c54d8eee..00000000 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/TabPanelContainer.tsx +++ /dev/null @@ -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 - * - * - * - * ``` - */ -const TabPanelContainer: React.FC = memo(({ - loading = false, - loadingMessage = '加载中...', - spacing = 6, - children, -}) => { - if (loading) { - return ; - } - - return ( - - {children} - - ); -}); - -TabPanelContainer.displayName = 'TabPanelContainer'; - -export default TabPanelContainer; diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts index 6e91f2a8..e4abb538 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts @@ -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"; diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementPanel.tsx index fd004b80..bfac87b0 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementPanel.tsx +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementPanel.tsx @@ -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, diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx index 7c3ecca3..120f0175 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx @@ -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> = { 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 通用组件 * - 可配置显示哪些 Tab(enabledTabs) * - 黑金主题 - * - 懒加载(isLazy) + * - 懒加载 * - 支持 Tab 变更回调 */ const BasicInfoTab: React.FC = ({ @@ -60,79 +64,19 @@ const BasicInfoTab: React.FC = ({ 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 ; - } - return ; - }; + // 构建 tabs 配置(缓存避免重复计算) + const tabs = useMemo(() => buildTabsConfig(enabledTabs), [enabledTabs]); return ( - - - {tabs.map((tab) => ( - - - - {tab.name} - - - ))} - - - - {tabs.map((tab) => ( - - {renderTabContent(tab)} - - ))} - - + onTabChange={onTabChange} + themePreset="blackGold" + /> ); diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/index.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/index.tsx index 8d3f5944..865c4006 100644 --- a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/index.tsx +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/index.tsx @@ -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 = ({ comprehensiveData, valueChainData, @@ -40,74 +53,22 @@ const DeepAnalysisTab: React.FC = ({ } return ( - - {/* 核心定位卡片 */} - {comprehensiveData?.qualitative_analysis && ( - + + - )} - - {/* 战略分析 */} - {comprehensiveData?.qualitative_analysis?.strategy && ( - - )} - - {/* 竞争地位分析 */} - {comprehensiveData?.competitive_position && ( - - )} - - {/* 业务结构分析 */} - {comprehensiveData?.business_structure && - comprehensiveData.business_structure.length > 0 && ( - - )} - - {/* 业务板块详情 */} - {comprehensiveData?.business_segments && - comprehensiveData.business_segments.length > 0 && ( - - )} - - {/* 产业链分析 */} - {valueChainData && ( - - )} - - {/* 关键因素与发展时间线 */} - - - {keyFactorsData?.key_factors && ( - - )} - - - - {keyFactorsData?.development_timeline && ( - - )} - - - +
+ ); }; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/BusinessTab.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/BusinessTab.tsx new file mode 100644 index 00000000..75a9aa9a --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/BusinessTab.tsx @@ -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; + onToggleSegment: (index: number) => void; +} + +const BusinessTab: React.FC = ({ + comprehensiveData, + cardBg, + expandedSegments, + onToggleSegment, +}) => { + return ( + + {/* 业务结构分析 */} + {comprehensiveData?.business_structure && + comprehensiveData.business_structure.length > 0 && ( + + )} + + {/* 业务板块详情 */} + {comprehensiveData?.business_segments && + comprehensiveData.business_segments.length > 0 && ( + + )} + + ); +}; + +export default BusinessTab; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/DevelopmentTab.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/DevelopmentTab.tsx new file mode 100644 index 00000000..5fe59463 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/DevelopmentTab.tsx @@ -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 = ({ + keyFactorsData, + cardBg, +}) => { + return ( + + + + {keyFactorsData?.key_factors && ( + + )} + + + + {keyFactorsData?.development_timeline && ( + + )} + + + + ); +}; + +export default DevelopmentTab; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/StrategyTab.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/StrategyTab.tsx new file mode 100644 index 00000000..2d9d67a6 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/StrategyTab.tsx @@ -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 = ({ + comprehensiveData, + cardBg, +}) => { + return ( + + {/* 核心定位卡片 */} + {comprehensiveData?.qualitative_analysis && ( + + )} + + {/* 战略分析 */} + {comprehensiveData?.qualitative_analysis?.strategy && ( + + )} + + {/* 竞争地位分析 */} + {comprehensiveData?.competitive_position && ( + + )} + + ); +}; + +export default StrategyTab; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/ValueChainTab.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/ValueChainTab.tsx new file mode 100644 index 00000000..2c73fa12 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/ValueChainTab.tsx @@ -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 = ({ + valueChainData, + cardBg, +}) => { + return ( + + {valueChainData && ( + + )} + + ); +}; + +export default ValueChainTab; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/index.ts b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/index.ts new file mode 100644 index 00000000..2ef7c836 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/tabs/index.ts @@ -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';