/** * Button 按钮组件 * 支持多种变体、尺寸、加载状态和图标 */ import React, { useMemo } from 'react'; import { TouchableOpacity, ActivityIndicator, StyleSheet, View, ViewStyle, TextStyle, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { borderRadius, spacing, fontSizes, useAppColors, type AppColors } from '../../theme'; import Text from './Text'; type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'danger'; type ButtonSize = 'sm' | 'md' | 'lg'; type IconPosition = 'left' | 'right'; interface ButtonProps { title: string; onPress: () => void; variant?: ButtonVariant; size?: ButtonSize; disabled?: boolean; loading?: boolean; icon?: string; iconPosition?: IconPosition; fullWidth?: boolean; style?: ViewStyle; } function createButtonStyles(colors: AppColors) { return StyleSheet.create({ base: { justifyContent: 'center', alignItems: 'center', borderRadius: borderRadius.md, }, content: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, size_sm: { paddingVertical: spacing.sm, paddingHorizontal: spacing.md, minHeight: 32, }, size_md: { paddingVertical: spacing.md, paddingHorizontal: spacing.lg, minHeight: 40, }, size_lg: { paddingVertical: spacing.lg, paddingHorizontal: spacing.xl, minHeight: 48, }, primary: { backgroundColor: colors.primary.main, }, secondary: { backgroundColor: colors.secondary.main, }, outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.primary.main, }, text: { backgroundColor: 'transparent', shadowColor: 'transparent', elevation: 0, }, danger: { backgroundColor: colors.error.main, }, disabled: { backgroundColor: colors.background.disabled, borderColor: colors.background.disabled, }, fullWidth: { width: '100%', }, textBase: { fontWeight: '600', }, textSize_sm: { fontSize: fontSizes.sm, }, textSize_md: { fontSize: fontSizes.md, }, textSize_lg: { fontSize: fontSizes.lg, }, iconLeft: { marginRight: spacing.sm, }, iconRight: { marginLeft: spacing.sm, }, }); } const Button: React.FC = ({ title, onPress, variant = 'primary', size = 'md', disabled = false, loading = false, icon, iconPosition = 'left', fullWidth = false, style, }) => { const colors = useAppColors(); const styles = useMemo(() => createButtonStyles(colors), [colors]); const getButtonStyle = (): ViewStyle[] => { const baseStyle: ViewStyle[] = [styles.base, styles[`size_${size}`]]; switch (variant) { case 'primary': baseStyle.push(styles.primary); break; case 'secondary': baseStyle.push(styles.secondary); break; case 'outline': baseStyle.push(styles.outline); break; case 'text': baseStyle.push(styles.text); break; case 'danger': baseStyle.push(styles.danger); break; } if (disabled || loading) { baseStyle.push(styles.disabled); } if (fullWidth) { baseStyle.push(styles.fullWidth); } return baseStyle; }; const getTextStyle = (): TextStyle => { const baseStyle: TextStyle = { ...styles.textBase, ...styles[`textSize_${size}`], }; switch (variant) { case 'primary': case 'danger': baseStyle.color = colors.text.inverse; break; case 'secondary': baseStyle.color = colors.text.inverse; break; case 'outline': case 'text': baseStyle.color = colors.primary.main; break; } if (disabled || loading) { baseStyle.color = colors.text.disabled; } return baseStyle; }; const getIconSize = (): number => { switch (size) { case 'sm': return 16; case 'md': return 20; case 'lg': return 24; } }; const getIconColor = (): string => { if (disabled || loading) { return colors.text.disabled; } switch (variant) { case 'primary': case 'danger': return colors.text.inverse; case 'secondary': return colors.text.inverse; case 'outline': case 'text': return colors.primary.main; } }; const isDisabled = disabled || loading; return ( {loading ? ( ) : ( {icon && iconPosition === 'left' && ( )} {title} {icon && iconPosition === 'right' && ( )} )} ); }; export default Button;