- SubTabContainer: 添加 compact 属性 - 移除 TabList 的 mx/mb 外边距 - 移除圆角和阴影 - 减小垂直内边距 - BasicInfoTab: 启用 compact 模式,移除 Card 包裹 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
8.7 KiB
TypeScript
327 lines
8.7 KiB
TypeScript
/**
|
||
* SubTabContainer - 二级导航容器组件
|
||
*
|
||
* 深空 FUI 设计风格(Glassmorphism + Ash Thorp + James Turrell)
|
||
* - 玻璃态导航栏,漂浮感
|
||
* - 选中态发光效果,科幻数据终端感
|
||
* - 流畅的过渡动画
|
||
*
|
||
* @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>;
|
||
}
|
||
|
||
/**
|
||
* 深空 FUI 主题配置
|
||
*/
|
||
const DEEP_SPACE = {
|
||
// 背景
|
||
bgGlass: 'rgba(12, 14, 28, 0.6)',
|
||
bgGlassHover: 'rgba(18, 22, 42, 0.7)',
|
||
|
||
// 边框
|
||
borderGold: 'rgba(212, 175, 55, 0.2)',
|
||
borderGoldHover: 'rgba(212, 175, 55, 0.5)',
|
||
borderGlass: 'rgba(255, 255, 255, 0.06)',
|
||
|
||
// 发光
|
||
glowGold: '0 0 30px rgba(212, 175, 55, 0.25), 0 4px 20px rgba(0, 0, 0, 0.3)',
|
||
innerGlow: 'inset 0 1px 0 rgba(255, 255, 255, 0.08)',
|
||
|
||
// 文字
|
||
textWhite: 'rgba(255, 255, 255, 0.95)',
|
||
textMuted: 'rgba(255, 255, 255, 0.6)',
|
||
textGold: '#F4D03F',
|
||
textDark: '#0A0A14',
|
||
|
||
// 选中态
|
||
selectedBg: 'linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(184, 150, 12, 0.95) 100%)',
|
||
|
||
// 圆角
|
||
radius: '12px',
|
||
radiusLG: '16px',
|
||
|
||
// 动画
|
||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||
};
|
||
|
||
/**
|
||
* 主题配置
|
||
*/
|
||
export interface SubTabTheme {
|
||
bg: string;
|
||
borderColor: string;
|
||
tabSelectedBg: string;
|
||
tabSelectedColor: string;
|
||
tabUnselectedColor: string;
|
||
tabHoverBg: string;
|
||
}
|
||
|
||
/**
|
||
* 预设主题 - 深空 FUI 风格
|
||
*/
|
||
const THEME_PRESETS: Record<string, SubTabTheme> = {
|
||
blackGold: {
|
||
bg: DEEP_SPACE.bgGlass,
|
||
borderColor: DEEP_SPACE.borderGold,
|
||
tabSelectedBg: DEEP_SPACE.selectedBg,
|
||
tabSelectedColor: DEEP_SPACE.textDark,
|
||
tabUnselectedColor: DEEP_SPACE.textWhite,
|
||
tabHoverBg: DEEP_SPACE.bgGlassHover,
|
||
},
|
||
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;
|
||
/** 紧凑模式 - 移除 TabList 的外边距 */
|
||
compact?: boolean;
|
||
}
|
||
|
||
const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||
tabs,
|
||
componentProps = {},
|
||
defaultIndex = 0,
|
||
index: controlledIndex,
|
||
onTabChange,
|
||
themePreset = 'blackGold',
|
||
theme: customTheme,
|
||
contentPadding = 4,
|
||
isLazy = true,
|
||
rightElement,
|
||
compact = false,
|
||
}) => {
|
||
// 内部状态(非受控模式)
|
||
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 - 玻璃态导航栏 */}
|
||
<TabList
|
||
bg={theme.bg}
|
||
backdropFilter="blur(20px)"
|
||
sx={{ WebkitBackdropFilter: 'blur(20px)' }}
|
||
borderBottom="1px solid"
|
||
borderColor={theme.borderColor}
|
||
borderRadius={compact ? 0 : DEEP_SPACE.radiusLG}
|
||
mx={compact ? 0 : 2}
|
||
mb={compact ? 0 : 2}
|
||
px={3}
|
||
py={compact ? 2 : 3}
|
||
flexWrap="nowrap"
|
||
gap={2}
|
||
alignItems="center"
|
||
overflowX="auto"
|
||
position="relative"
|
||
boxShadow={compact ? 'none' : DEEP_SPACE.innerGlow}
|
||
css={{
|
||
'&::-webkit-scrollbar': { display: 'none' },
|
||
scrollbarWidth: 'none',
|
||
}}
|
||
>
|
||
{/* 顶部金色光条 */}
|
||
<Box
|
||
position="absolute"
|
||
top={0}
|
||
left="50%"
|
||
transform="translateX(-50%)"
|
||
width="50%"
|
||
height="1px"
|
||
background={`linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent)`}
|
||
/>
|
||
|
||
{tabs.map((tab, idx) => {
|
||
const isSelected = idx === currentIndex;
|
||
|
||
return (
|
||
<Tab
|
||
key={tab.key}
|
||
color={theme.tabUnselectedColor}
|
||
borderRadius={DEEP_SPACE.radius}
|
||
px={6}
|
||
py={3}
|
||
fontSize="15px"
|
||
fontWeight="500"
|
||
whiteSpace="nowrap"
|
||
flexShrink={0}
|
||
border="1px solid transparent"
|
||
position="relative"
|
||
letterSpacing="0.03em"
|
||
transition={DEEP_SPACE.transition}
|
||
_before={{
|
||
content: '""',
|
||
position: 'absolute',
|
||
bottom: '-1px',
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
width: isSelected ? '70%' : '0%',
|
||
height: '2px',
|
||
bg: '#D4AF37',
|
||
borderRadius: 'full',
|
||
transition: 'width 0.3s ease',
|
||
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
|
||
}}
|
||
_selected={{
|
||
bg: theme.tabSelectedBg,
|
||
color: theme.tabSelectedColor,
|
||
fontWeight: '700',
|
||
boxShadow: DEEP_SPACE.glowGold,
|
||
border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
|
||
transform: 'translateY(-2px)',
|
||
}}
|
||
_hover={{
|
||
bg: isSelected ? undefined : theme.tabHoverBg,
|
||
border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`,
|
||
transform: 'translateY(-1px)',
|
||
}}
|
||
_active={{
|
||
transform: 'translateY(0)',
|
||
}}
|
||
>
|
||
<HStack spacing={2}>
|
||
{tab.icon && (
|
||
<Icon
|
||
as={tab.icon}
|
||
boxSize={4}
|
||
opacity={isSelected ? 1 : 0.7}
|
||
transition="opacity 0.2s"
|
||
/>
|
||
)}
|
||
<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;
|