refactor(TabContainer): 抽取通用 Tab 容器组件

- 新增 src/components/TabContainer/ 通用组件
  - 支持受控/非受控模式
  - 支持多种主题预设(blackGold、default、dark、light)
  - 支持自定义主题颜色和样式配置
  - 使用 TypeScript 实现,类型完整
- 重构 CompanyTabs 使用通用 TabContainer
- 删除 CompanyTabs/TabNavigation.js(逻辑迁移到通用组件)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-11 16:59:17 +08:00
parent 13fa91a998
commit a47e0feed8
6 changed files with 362 additions and 109 deletions

View File

@@ -0,0 +1,49 @@
/**
* TabNavigation 通用导航组件
*
* 渲染 Tab 按钮列表,支持图标 + 文字
*/
import React from 'react';
import { TabList, Tab, HStack, Icon, Text } from '@chakra-ui/react';
import type { TabNavigationProps } from './types';
const TabNavigation: React.FC<TabNavigationProps> = ({
tabs,
themeColors,
borderRadius = 'lg',
}) => {
return (
<TabList
py={4}
bg={themeColors.bg}
borderTopLeftRadius={borderRadius}
borderTopRightRadius={borderRadius}
>
{tabs.map((tab, index) => (
<Tab
key={tab.key}
color={themeColors.unselectedText}
borderRadius="full"
px={4}
py={2}
_selected={{
bg: themeColors.selectedBg,
color: themeColors.selectedText,
}}
_hover={{
color: themeColors.selectedText,
}}
mr={index < tabs.length - 1 ? 2 : 0}
>
<HStack spacing={2}>
{tab.icon && <Icon as={tab.icon} boxSize="18px" />}
<Text fontSize="15px">{tab.name}</Text>
</HStack>
</Tab>
))}
</TabList>
);
};
export default TabNavigation;

View File

@@ -0,0 +1,56 @@
/**
* TabContainer 常量和主题预设
*/
import type { ThemeColors, ThemePreset } from './types';
/**
* 主题预设配置
*/
export const THEME_PRESETS: Record<ThemePreset, Required<ThemeColors>> = {
// 黑金主题(原 Company 模块风格)
blackGold: {
bg: '#1A202C',
selectedBg: '#C9A961',
selectedText: '#FFFFFF',
unselectedText: '#D4AF37',
dividerColor: 'gray.600',
},
// 默认主题Chakra 风格)
default: {
bg: 'white',
selectedBg: 'blue.500',
selectedText: 'white',
unselectedText: 'gray.600',
dividerColor: 'gray.200',
},
// 深色主题
dark: {
bg: 'gray.800',
selectedBg: 'blue.400',
selectedText: 'white',
unselectedText: 'gray.300',
dividerColor: 'gray.600',
},
// 浅色主题
light: {
bg: 'gray.50',
selectedBg: 'blue.500',
selectedText: 'white',
unselectedText: 'gray.700',
dividerColor: 'gray.300',
},
};
/**
* 默认配置
*/
export const DEFAULT_CONFIG = {
themePreset: 'blackGold' as ThemePreset,
isLazy: true,
size: 'lg' as const,
showDivider: true,
borderRadius: 'lg',
shadow: 'lg',
panelPadding: 0,
};

View File

@@ -0,0 +1,140 @@
/**
* TabContainer 通用 Tab 容器组件
*
* 功能:
* - 管理 Tab 切换状态(支持受控/非受控模式)
* - 动态渲染 Tab 导航和内容
* - 支持多种主题预设(黑金、默认、深色、浅色)
* - 支持自定义主题颜色
* - 支持懒加载
*
* @example
* // 基础用法(传入 components
* <TabContainer
* tabs={[
* { key: 'tab1', name: 'Tab 1', icon: FaHome, component: Tab1Content },
* { key: 'tab2', name: 'Tab 2', icon: FaUser, component: Tab2Content },
* ]}
* componentProps={{ userId: '123' }}
* onTabChange={(index, key) => console.log('切换到', key)}
* />
*
* @example
* // 自定义渲染用法(使用 children
* <TabContainer tabs={tabs} themePreset="dark">
* <TabPanel>自定义内容 1</TabPanel>
* <TabPanel>自定义内容 2</TabPanel>
* </TabContainer>
*/
import React, { useState, useCallback, useMemo } from 'react';
import {
Card,
CardBody,
Tabs,
TabPanels,
TabPanel,
Divider,
} from '@chakra-ui/react';
import TabNavigation from './TabNavigation';
import { THEME_PRESETS, DEFAULT_CONFIG } from './constants';
import type { TabContainerProps, ThemeColors } from './types';
// 导出类型和常量
export type { TabConfig, ThemeColors, ThemePreset, TabContainerProps } from './types';
export { THEME_PRESETS } from './constants';
const TabContainer: React.FC<TabContainerProps> = ({
tabs,
componentProps = {},
onTabChange,
defaultIndex = 0,
index: controlledIndex,
themePreset = DEFAULT_CONFIG.themePreset,
themeColors: customThemeColors,
isLazy = DEFAULT_CONFIG.isLazy,
size = DEFAULT_CONFIG.size,
showDivider = DEFAULT_CONFIG.showDivider,
borderRadius = DEFAULT_CONFIG.borderRadius,
shadow = DEFAULT_CONFIG.shadow,
panelPadding = DEFAULT_CONFIG.panelPadding,
children,
}) => {
// 内部状态(非受控模式)
const [internalIndex, setInternalIndex] = useState(defaultIndex);
// 当前索引(支持受控/非受控)
const currentIndex = controlledIndex ?? internalIndex;
// 合并主题颜色(自定义颜色优先)
const themeColors: Required<ThemeColors> = useMemo(() => ({
...THEME_PRESETS[themePreset],
...customThemeColors,
}), [themePreset, customThemeColors]);
/**
* 处理 Tab 切换
*/
const handleTabChange = useCallback((newIndex: number) => {
const tabKey = tabs[newIndex]?.key || '';
// 触发回调
onTabChange?.(newIndex, tabKey, currentIndex);
// 非受控模式下更新内部状态
if (controlledIndex === undefined) {
setInternalIndex(newIndex);
}
}, [tabs, onTabChange, currentIndex, controlledIndex]);
/**
* 渲染 Tab 内容
*/
const renderTabPanels = () => {
// 如果传入了 children直接渲染 children
if (children) {
return children;
}
// 否则根据 tabs 配置渲染
return tabs.map((tab) => {
const Component = tab.component;
return (
<TabPanel key={tab.key} px={panelPadding} py={panelPadding}>
{Component ? <Component {...componentProps} /> : null}
</TabPanel>
);
});
};
return (
<Card shadow={shadow} bg={themeColors.bg} borderRadius={borderRadius}>
<CardBody p={0}>
<Tabs
isLazy={isLazy}
variant="soft-rounded"
colorScheme="blue"
size={size}
index={currentIndex}
onChange={handleTabChange}
>
{/* Tab 导航 */}
<TabNavigation
tabs={tabs}
themeColors={themeColors}
borderRadius={borderRadius}
/>
{/* 分割线 */}
{showDivider && <Divider borderColor={themeColors.dividerColor} />}
{/* Tab 内容面板 */}
<TabPanels>{renderTabPanels()}</TabPanels>
</Tabs>
</CardBody>
</Card>
);
};
export default TabContainer;

View File

@@ -0,0 +1,87 @@
/**
* TabContainer 通用 Tab 容器组件类型定义
*/
import type { ComponentType, ReactNode } from 'react';
import type { IconType } from 'react-icons';
/**
* Tab 配置项
*/
export interface TabConfig {
/** Tab 唯一标识 */
key: string;
/** Tab 显示名称 */
name: string;
/** Tab 图标(可选) */
icon?: IconType | ComponentType;
/** Tab 内容组件(可选,如果不传则使用 children 渲染) */
component?: ComponentType<any>;
}
/**
* 主题颜色配置
*/
export interface ThemeColors {
/** 容器背景色 */
bg?: string;
/** 选中 Tab 背景色 */
selectedBg?: string;
/** 选中 Tab 文字颜色 */
selectedText?: string;
/** 未选中 Tab 文字颜色 */
unselectedText?: string;
/** 分割线颜色 */
dividerColor?: string;
}
/**
* 预设主题类型
*/
export type ThemePreset = 'blackGold' | 'default' | 'dark' | 'light';
/**
* TabContainer 组件 Props
*/
export interface TabContainerProps {
/** Tab 配置数组 */
tabs: TabConfig[];
/** 传递给 Tab 内容组件的通用 props */
componentProps?: Record<string, any>;
/** Tab 变更回调 */
onTabChange?: (index: number, tabKey: string, prevIndex: number) => void;
/** 默认选中的 Tab 索引 */
defaultIndex?: number;
/** 受控模式下的当前索引 */
index?: number;
/** 主题预设 */
themePreset?: ThemePreset;
/** 自定义主题颜色(优先级高于预设) */
themeColors?: ThemeColors;
/** 是否启用懒加载 */
isLazy?: boolean;
/** Tab 尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 是否显示分割线 */
showDivider?: boolean;
/** 容器圆角 */
borderRadius?: string;
/** 容器阴影 */
shadow?: string;
/** 自定义 Tab 面板内边距 */
panelPadding?: number | string;
/** 子元素(用于自定义渲染 Tab 内容) */
children?: ReactNode;
}
/**
* TabNavigation 组件 Props
*/
export interface TabNavigationProps {
/** Tab 配置数组 */
tabs: TabConfig[];
/** 主题颜色 */
themeColors: Required<ThemeColors>;
/** 容器圆角 */
borderRadius?: string;
}

View File

@@ -1,55 +0,0 @@
// src/views/Company/components/CompanyTabs/TabNavigation.js
// Tab 导航组件 - 动态渲染 Tab 按钮(黑金主题)
import React from 'react';
import {
TabList,
Tab,
HStack,
Icon,
Text,
} from '@chakra-ui/react';
import { COMPANY_TABS } from '../../constants';
// 黑金主题颜色配置
const THEME_COLORS = {
bg: '#1A202C', // 背景纯黑
selectedBg: '#C9A961', // 选中项金色背景
selectedText: '#FFFFFF', // 选中项白色文字
unselectedText: '#D4AF37', // 未选中项金色
};
/**
* Tab 导航组件(黑金主题)
*/
const TabNavigation = () => {
return (
<TabList py={4} bg={THEME_COLORS.bg} borderTopLeftRadius="16px" borderTopRightRadius="16px">
{COMPANY_TABS.map((tab, index) => (
<Tab
key={tab.key}
color={THEME_COLORS.unselectedText}
borderRadius="full"
px={4}
py={2}
_selected={{
bg: THEME_COLORS.selectedBg,
color: THEME_COLORS.selectedText,
}}
_hover={{
color: THEME_COLORS.selectedText,
}}
mr={index < COMPANY_TABS.length - 1 ? 2 : 0}
>
<HStack spacing={2}>
<Icon as={tab.icon} boxSize="18px" />
<Text fontSize="15px">{tab.name}</Text>
</HStack>
</Tab>
))}
</TabList>
);
};
export default TabNavigation;

View File

@@ -1,17 +1,8 @@
// src/views/Company/components/CompanyTabs/index.js
// Tab 容器组件 - 管理 Tab 切换和内容渲染
// Tab 容器组件 - 使用通用 TabContainer 组件
import React, { useState } from 'react';
import {
Card,
CardBody,
Tabs,
TabPanels,
TabPanel,
Divider,
} from '@chakra-ui/react';
import TabNavigation from './TabNavigation';
import React from 'react';
import TabContainer from '@components/TabContainer';
import { COMPANY_TABS, getTabNameByIndex } from '../../constants';
// 子组件导入Tab 内容组件)
@@ -24,7 +15,6 @@ import DynamicTracking from '../DynamicTracking';
/**
* Tab 组件映射
* key 与 COMPANY_TABS 中的 key 对应
*/
const TAB_COMPONENTS = {
overview: CompanyOverview,
@@ -36,11 +26,25 @@ const TAB_COMPONENTS = {
};
/**
* Tab 容器组件
* 构建 TabContainer 所需的 tabs 配置
* 合并 COMPANY_TABS 和对应的组件
*/
const buildTabsConfig = () => {
return COMPANY_TABS.map((tab) => ({
...tab,
component: TAB_COMPONENTS[tab.key],
}));
};
// 预构建 tabs 配置(避免每次渲染重新计算)
const TABS_CONFIG = buildTabsConfig();
/**
* 公司详情 Tab 容器组件
*
* 功能:
* - 管理 Tab 切换状态
* - 动态渲染 Tab 导航和内容
* - 使用通用 TabContainer 组件
* - 保持黑金主题风格
* - 触发 Tab 变更追踪
*
* @param {Object} props
@@ -48,51 +52,23 @@ const TAB_COMPONENTS = {
* @param {Function} props.onTabChange - Tab 变更回调 (index, tabName, prevIndex) => void
*/
const CompanyTabs = ({ stockCode, onTabChange }) => {
const [currentIndex, setCurrentIndex] = useState(0);
/**
* 处理 Tab 切换
* 转换 tabKey 为 tabName 以保持原有回调格式
*/
const handleTabChange = (index) => {
const handleTabChange = (index, tabKey, prevIndex) => {
const tabName = getTabNameByIndex(index);
// 触发追踪回调
onTabChange?.(index, tabName, currentIndex);
// 更新状态
setCurrentIndex(index);
onTabChange?.(index, tabName, prevIndex);
};
return (
<Card shadow="lg" bg='#1A202C'>
<CardBody p={0}>
<Tabs
isLazy
variant="soft-rounded"
colorScheme="blue"
size="lg"
index={currentIndex}
onChange={handleTabChange}
>
{/* Tab 导航(黑金主题) */}
<TabNavigation />
<Divider />
{/* Tab 内容面板 */}
<TabPanels>
{COMPANY_TABS.map((tab) => {
const Component = TAB_COMPONENTS[tab.key];
return (
<TabPanel key={tab.key} px={0}>
<Component stockCode={stockCode} />
</TabPanel>
);
})}
</TabPanels>
</Tabs>
</CardBody>
</Card>
<TabContainer
tabs={TABS_CONFIG}
componentProps={{ stockCode }}
onTabChange={handleTabChange}
themePreset="blackGold"
borderRadius="16px"
/>
);
};