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:
146
src/components/common/Avatar.tsx
Normal file
146
src/components/common/Avatar.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Avatar 头像组件
|
||||
* 支持图片URL、本地图片、首字母显示、在线状态徽章
|
||||
* 使用 expo-image 实现内存+磁盘双级缓存,头像秒加载
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { colors, borderRadius } from '../../theme';
|
||||
import Text from './Text';
|
||||
|
||||
type AvatarSource = string | { uri: string } | number | null;
|
||||
|
||||
interface AvatarProps {
|
||||
source?: AvatarSource;
|
||||
size?: number; // 默认40
|
||||
name?: string; // 用于显示首字母
|
||||
onPress?: () => void;
|
||||
showBadge?: boolean;
|
||||
badgeColor?: string;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const Avatar: React.FC<AvatarProps> = ({
|
||||
source,
|
||||
size = 40,
|
||||
name,
|
||||
onPress,
|
||||
showBadge = false,
|
||||
badgeColor = colors.success.main,
|
||||
style,
|
||||
}) => {
|
||||
// 获取首字母
|
||||
const getInitial = (): string => {
|
||||
if (!name) return '?';
|
||||
const firstChar = name.charAt(0).toUpperCase();
|
||||
// 中文字符
|
||||
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
|
||||
return firstChar;
|
||||
}
|
||||
return firstChar;
|
||||
};
|
||||
|
||||
// 渲染头像内容
|
||||
const renderAvatarContent = () => {
|
||||
// 如果有图片源
|
||||
if (source) {
|
||||
const imageSource =
|
||||
typeof source === 'string' ? { uri: source } : source;
|
||||
|
||||
return (
|
||||
<ExpoImage
|
||||
source={imageSource}
|
||||
style={[
|
||||
styles.image,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
},
|
||||
]}
|
||||
contentFit="cover"
|
||||
cachePolicy="memory"
|
||||
transition={150}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示首字母
|
||||
const fontSize = size / 2;
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.placeholder,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text color={colors.text.inverse} style={{ fontSize, lineHeight: fontSize * 1.2 }}>
|
||||
{getInitial()}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const avatarContainer = (
|
||||
<View style={[styles.container, style]}>
|
||||
{renderAvatarContent()}
|
||||
{showBadge && (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{
|
||||
backgroundColor: badgeColor,
|
||||
width: size * 0.3,
|
||||
height: size * 0.3,
|
||||
borderRadius: (size * 0.3) / 2,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||
{avatarContainer}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return avatarContainer;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
},
|
||||
image: {
|
||||
backgroundColor: colors.background.disabled,
|
||||
},
|
||||
placeholder: {
|
||||
backgroundColor: colors.primary.main,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.background.paper,
|
||||
},
|
||||
});
|
||||
|
||||
export default Avatar;
|
||||
Reference in New Issue
Block a user