Files
vf_react/src/components/SubTabContainer/index.tsx
zdl 9b8983869c style: 子 Tab 紧凑模式,移除多余边距
- SubTabContainer: 添加 compact 属性
  - 移除 TabList 的 mx/mb 外边距
  - 移除圆角和阴影
  - 减小垂直内边距
- BasicInfoTab: 启用 compact 模式,移除 Card 包裹

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 17:54:56 +08:00

327 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;