Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
347
src/components/business/TabBar.tsx
Normal file
347
src/components/business/TabBar.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* TabBar 标签栏组件 - 美化版
|
||||
* 用于切换不同标签页,支持多种样式变体
|
||||
* 新增胶囊式、分段式等现代设计风格
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import Text from '../common/Text';
|
||||
|
||||
type TabBarVariant = 'default' | 'pill' | 'segmented' | 'modern';
|
||||
|
||||
interface TabBarProps {
|
||||
tabs: string[];
|
||||
activeIndex: number;
|
||||
onTabChange: (index: number) => void;
|
||||
scrollable?: boolean;
|
||||
rightContent?: ReactNode;
|
||||
variant?: TabBarVariant;
|
||||
icons?: string[];
|
||||
}
|
||||
|
||||
const TabBar: React.FC<TabBarProps> = ({
|
||||
tabs,
|
||||
activeIndex,
|
||||
onTabChange,
|
||||
scrollable = false,
|
||||
rightContent,
|
||||
variant = 'default',
|
||||
icons,
|
||||
}) => {
|
||||
const renderTabs = () => {
|
||||
return tabs.map((tab, index) => {
|
||||
const isActive = index === activeIndex;
|
||||
const icon = icons?.[index];
|
||||
|
||||
if (variant === 'modern') {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.modernTab, isActive && styles.modernTabActive]}
|
||||
onPress={() => onTabChange(index)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={styles.modernTabContent}>
|
||||
{icon && (
|
||||
<MaterialCommunityIcons
|
||||
name={icon as any}
|
||||
size={18}
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
style={styles.modernTabIcon}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
style={isActive ? [styles.modernTabText, styles.modernTabTextActive] : styles.modernTabText}
|
||||
>
|
||||
{tab}
|
||||
</Text>
|
||||
</View>
|
||||
{isActive && <View style={styles.modernTabIndicator} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'pill') {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.pillTab, isActive && styles.pillTabActive]}
|
||||
onPress={() => onTabChange(index)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={styles.pillTabText}
|
||||
>
|
||||
{tab}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'segmented') {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === tabs.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.segmentedTab,
|
||||
isActive && styles.segmentedTabActive,
|
||||
isFirst && styles.segmentedTabFirst,
|
||||
isLast && styles.segmentedTabLast,
|
||||
]}
|
||||
onPress={() => onTabChange(index)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.segmentedTabContent}>
|
||||
{icon && (
|
||||
<MaterialCommunityIcons
|
||||
name={icon as any}
|
||||
size={16}
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
style={styles.segmentedTabIcon}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
style={styles.segmentedTabText}
|
||||
>
|
||||
{tab}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// default variant
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.tab, isActive && styles.activeTab]}
|
||||
onPress={() => onTabChange(index)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
style={isActive ? [styles.tabText, styles.activeTabText] : styles.tabText}
|
||||
>
|
||||
{tab}
|
||||
</Text>
|
||||
{isActive && <View style={styles.activeIndicator} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getContainerStyle = () => {
|
||||
switch (variant) {
|
||||
case 'pill':
|
||||
return styles.pillContainer;
|
||||
case 'segmented':
|
||||
return styles.segmentedContainer;
|
||||
case 'modern':
|
||||
return styles.modernContainer;
|
||||
default:
|
||||
return styles.container;
|
||||
}
|
||||
};
|
||||
|
||||
if (scrollable) {
|
||||
return (
|
||||
<View style={getContainerStyle()}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollableContainer}
|
||||
>
|
||||
{renderTabs()}
|
||||
</ScrollView>
|
||||
{rightContent && <View style={styles.rightContent}>{rightContent}</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={getContainerStyle()}>
|
||||
{renderTabs()}
|
||||
{rightContent && <View style={styles.rightContent}>{rightContent}</View>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// Default variant
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
alignItems: 'center',
|
||||
paddingRight: spacing.xs,
|
||||
},
|
||||
scrollableContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
flex: 1,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
position: 'relative',
|
||||
},
|
||||
activeTab: {
|
||||
// 激活状态样式
|
||||
},
|
||||
tabText: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
activeTabText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
activeIndicator: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '25%',
|
||||
right: '25%',
|
||||
height: 3,
|
||||
backgroundColor: colors.primary.main,
|
||||
borderTopLeftRadius: borderRadius.sm,
|
||||
borderTopRightRadius: borderRadius.sm,
|
||||
},
|
||||
rightContent: {
|
||||
paddingLeft: spacing.sm,
|
||||
},
|
||||
|
||||
// Pill variant
|
||||
pillContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
padding: spacing.sm,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
pillTab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
pillTabActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
pillTabText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Segmented variant
|
||||
segmentedContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
padding: spacing.xs,
|
||||
marginHorizontal: spacing.md,
|
||||
marginVertical: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
},
|
||||
segmentedTab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
segmentedTabActive: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.md,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
segmentedTabFirst: {
|
||||
borderTopLeftRadius: borderRadius.md,
|
||||
borderBottomLeftRadius: borderRadius.md,
|
||||
},
|
||||
segmentedTabLast: {
|
||||
borderTopRightRadius: borderRadius.md,
|
||||
borderBottomRightRadius: borderRadius.md,
|
||||
},
|
||||
segmentedTabText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
segmentedTabContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
segmentedTabIcon: {
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
|
||||
// Modern variant - 现代化标签栏
|
||||
modernContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.xl,
|
||||
marginHorizontal: spacing.lg,
|
||||
marginVertical: spacing.md,
|
||||
padding: spacing.xs,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
},
|
||||
modernTab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
position: 'relative',
|
||||
},
|
||||
modernTabActive: {
|
||||
backgroundColor: colors.primary.main + '15', // 10% opacity
|
||||
},
|
||||
modernTabContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modernTabIcon: {
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
modernTabText: {
|
||||
fontWeight: '500',
|
||||
fontSize: fontSizes.md,
|
||||
},
|
||||
modernTabTextActive: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
modernTabIndicator: {
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
width: 20,
|
||||
height: 3,
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: borderRadius.full,
|
||||
},
|
||||
});
|
||||
|
||||
export default TabBar;
|
||||
Reference in New Issue
Block a user