- 添加 visitedTabs 状态记录已访问的 Tab 索引 - Tab 切换时更新已访问集合 - TabPanels 中实现条件渲染:只渲染当前或已访问过的 Tab 修复前:tabs.map() 会创建所有组件实例,导致 Hook 立即执行 修复后:仅首次访问 Tab 时才渲染组件,真正实现懒加载 效果:初始加载从 N 个请求减少到 1 个请求 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
233 lines
5.6 KiB
TypeScript
233 lines
5.6 KiB
TypeScript
/**
|
||
* 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, memo } from 'react';
|
||
import {
|
||
Box,
|
||
Tabs,
|
||
TabList,
|
||
TabPanels,
|
||
Tab,
|
||
TabPanel,
|
||
Icon,
|
||
HStack,
|
||
Text,
|
||
Spacer,
|
||
} 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;
|
||
/** TabList 右侧自定义内容 */
|
||
rightElement?: React.ReactNode;
|
||
}
|
||
|
||
const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||
tabs,
|
||
componentProps = {},
|
||
defaultIndex = 0,
|
||
index: controlledIndex,
|
||
onTabChange,
|
||
themePreset = 'blackGold',
|
||
theme: customTheme,
|
||
contentPadding = 4,
|
||
isLazy = true,
|
||
rightElement,
|
||
}) => {
|
||
// 内部状态(非受控模式)
|
||
const [internalIndex, setInternalIndex] = useState(defaultIndex);
|
||
|
||
// 当前索引
|
||
const currentIndex = controlledIndex ?? internalIndex;
|
||
|
||
// 记录已访问的 Tab 索引(用于真正的懒加载)
|
||
const [visitedTabs, setVisitedTabs] = useState<Set<number>>(
|
||
() => new Set([controlledIndex ?? defaultIndex])
|
||
);
|
||
|
||
// 合并主题
|
||
const theme: SubTabTheme = {
|
||
...THEME_PRESETS[themePreset],
|
||
...customTheme,
|
||
};
|
||
|
||
/**
|
||
* 处理 Tab 切换
|
||
*/
|
||
const handleTabChange = useCallback(
|
||
(newIndex: number) => {
|
||
const tabKey = tabs[newIndex]?.key || '';
|
||
onTabChange?.(newIndex, tabKey);
|
||
|
||
// 记录已访问的 Tab(用于懒加载)
|
||
setVisitedTabs(prev => {
|
||
if (prev.has(newIndex)) return prev;
|
||
return new Set(prev).add(newIndex);
|
||
});
|
||
|
||
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}
|
||
pl={0}
|
||
pr={2}
|
||
py={1.5}
|
||
flexWrap="nowrap"
|
||
gap={1}
|
||
alignItems="center"
|
||
overflowX="auto"
|
||
css={{
|
||
'&::-webkit-scrollbar': { display: 'none' },
|
||
scrollbarWidth: 'none',
|
||
}}
|
||
>
|
||
{tabs.map((tab) => (
|
||
<Tab
|
||
key={tab.key}
|
||
color={theme.tabUnselectedColor}
|
||
borderRadius="full"
|
||
px={2.5}
|
||
py={1.5}
|
||
fontSize="xs"
|
||
whiteSpace="nowrap"
|
||
flexShrink={0}
|
||
_selected={{
|
||
bg: theme.tabSelectedBg,
|
||
color: theme.tabSelectedColor,
|
||
fontWeight: 'bold',
|
||
}}
|
||
_hover={{
|
||
bg: theme.tabHoverBg,
|
||
}}
|
||
>
|
||
<HStack spacing={1}>
|
||
{tab.icon && <Icon as={tab.icon} boxSize={3} />}
|
||
<Text>{tab.name}</Text>
|
||
</HStack>
|
||
</Tab>
|
||
))}
|
||
{rightElement && (
|
||
<>
|
||
<Spacer />
|
||
<Box flexShrink={0}>{rightElement}</Box>
|
||
</>
|
||
)}
|
||
</TabList>
|
||
|
||
<TabPanels p={contentPadding}>
|
||
{tabs.map((tab, idx) => {
|
||
const Component = tab.component;
|
||
// 懒加载:只渲染已访问过的 Tab
|
||
const shouldRender = !isLazy || visitedTabs.has(idx);
|
||
|
||
return (
|
||
<TabPanel key={tab.key} p={0}>
|
||
{shouldRender && Component ? (
|
||
<Component {...componentProps} />
|
||
) : null}
|
||
</TabPanel>
|
||
);
|
||
})}
|
||
</TabPanels>
|
||
</Tabs>
|
||
</Box>
|
||
);
|
||
});
|
||
|
||
SubTabContainer.displayName = 'SubTabContainer';
|
||
|
||
export default SubTabContainer;
|