Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 12x 12x 21x 16x 5x 12x 12x 12x 21x 21x 21x 21x 12x | import { forwardRef } from 'react';
import { View, StyleSheet } from 'react-native';
import { Button as TamaguiButton, Spinner, Text, styled, GetProps } from 'tamagui';
const StyledButton = styled(TamaguiButton, {
name: 'Button',
borderRadius: '$4',
pressStyle: { scale: 0.97, opacity: 0.9 },
animation: 'quick',
variants: {
variant: {
primary: {
backgroundColor: '$primary',
hoverStyle: { backgroundColor: '$primaryDark' },
},
secondary: {
backgroundColor: '$color3',
hoverStyle: { backgroundColor: '$color4' },
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '$borderColor',
hoverStyle: { backgroundColor: '$color2' },
},
ghost: {
backgroundColor: 'transparent',
hoverStyle: { backgroundColor: '$color2' },
},
danger: {
backgroundColor: '$red',
hoverStyle: { backgroundColor: '$red10' },
},
},
size: {
sm: { height: 42, paddingHorizontal: '$3' },
md: { height: 52, paddingHorizontal: '$4' },
lg: { height: 60, paddingHorizontal: '$5' },
},
fullWidth: {
true: { width: '100%' },
},
rounded: {
true: { borderRadius: 9999 },
},
} as const,
});
type StyledButtonProps = GetProps<typeof StyledButton>;
interface ButtonProps extends StyledButtonProps {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const getTextColor = (variant?: string) => {
switch (variant) {
case 'primary':
case 'danger':
return 'white';
default:
return '$color11';
}
};
// 映射 size 到 Tamagui 字体大小 token
const FONT_SIZE_MAP: Record<string, number> = {
sm: 14,
md: 16,
lg: 18,
};
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
iconLeft: {
marginRight: 8,
},
iconRight: {
marginLeft: 8,
},
});
export const Button = forwardRef<React.ElementRef<typeof StyledButton>, ButtonProps>(
({ children, loading, disabled, leftIcon, rightIcon, variant, size, testID, ...props }, ref) => {
const textColor = getTextColor(variant as string);
const fontSize = FONT_SIZE_MAP[size as string] || 16;
// 同时设置 testID 和 accessibilityLabel 以支持 Detox E2E 测试
const a11yProps = testID ? { testID, accessibilityLabel: testID, nativeID: testID } : {};
return (
<StyledButton
ref={ref}
disabled={disabled || loading}
opacity={disabled ? 0.5 : 1}
variant={variant}
size={size}
{...a11yProps}
{...props}
>
{loading ? (
<Spinner size="small" color={textColor} />
) : (
<View style={styles.row}>
{leftIcon ? <View style={styles.iconLeft}>{leftIcon}</View> : null}
{typeof children === 'string' ? (
<Text color={textColor} fontSize={fontSize} fontWeight="600">
{children}
</Text>
) : children ? (
<View>{children}</View>
) : null}
{rightIcon ? <View style={styles.iconRight}>{rightIcon}</View> : null}
</View>
)}
</StyledButton>
);
}
);
Button.displayName = 'Button';
|