feat(ui): unify design system to Twitter/X style across screens and improve message handling
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 3m9s
Frontend CI / ota-android (push) Successful in 10m29s
Frontend CI / build-android-apk (push) Successful in 43m4s

Redesign multiple screens and components from card-based responsive design to Twitter/X flat design:
- UserProfileHeader: adopt Twitter/X style with larger icons, simplified buttons, and block functionality
- AppBackButton: update to chevron-left icon with larger size, remove background styling
- Update SettingsScreen, EditProfileScreen, and UserProfileScreen with flat layout approach
- ConversationListRow: convert from bordered cards to flat list rows with updated typography

Improve message system with WebSocket notification handling:
- Add real-time system unread count updates via WebSocket
- Track lastSystemMessageAt for accurate system message timestamps
- Add notification deduplication and read-status filtering
- Combine system and conversation unread counts in background sync
- Clear system notifications from notification center on mark as read
This commit is contained in:
lafay
2026-04-25 00:39:40 +08:00
parent e0d28535f4
commit ad19bc2af7
26 changed files with 950 additions and 1121 deletions

View File

@@ -1,50 +1,50 @@
/**
* UserProfileHeader 用户资料头部组件 - 美化版(响应式适配)
* UserProfileHeader 用户资料头部组件 - Twitter/X 风格
* 显示用户封面、头像、昵称、简介、关注/粉丝数
* 采用现代卡片式设计,渐变封面,悬浮头像
* 采用 Twitter/X 扁平化设计,左对齐布局
* 支持互关状态显示
* 在宽屏下显示更大的头像和封面
*/
import React, { useMemo } from 'react';
import React, { useMemo, useState, useRef, useCallback } from 'react';
import {
View,
Image,
TouchableOpacity,
StyleSheet,
Modal,
Pressable,
Dimensions,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { User } from '../../types';
import Text from '../common/Text';
import Button from '../common/Button';
import Avatar from '../common/Avatar';
import { useResponsive } from '../../hooks';
interface UserProfileHeaderProps {
user: User;
isCurrentUser?: boolean;
isBlocked?: boolean;
onFollow: () => void;
onSettings?: () => void;
onEditProfile?: () => void;
onMessage?: () => void;
onMore?: () => void; // 点击更多按钮
onPostsPress?: () => void; // 点击帖子数(可选)
onFollowingPress?: () => void; // 点击关注数
onFollowersPress?: () => void; // 点击粉丝数
onAvatarPress?: () => void; // 点击头像编辑按钮
onBlock?: () => void;
onFollowingPress?: () => void;
onFollowersPress?: () => void;
onAvatarPress?: () => void;
}
const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
user,
isCurrentUser = false,
isBlocked = false,
onFollow,
onSettings,
onEditProfile,
onMessage,
onMore,
onPostsPress,
onBlock,
onFollowingPress,
onFollowersPress,
onAvatarPress,
@@ -52,108 +52,118 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
const colors = useAppColors();
const styles = useMemo(() => createUserProfileHeaderStyles(colors), [colors]);
const { isWideScreen, isDesktop, width } = useResponsive();
const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const moreButtonRef = useRef<View>(null);
// 格式化数字
const formatCount = (count: number | undefined): string => {
if (count === undefined || count === null) {
return '0';
}
if (count >= 10000) {
return `${(count / 10000).toFixed(1)}w`;
}
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`;
}
if (count === undefined || count === null) return '0';
if (count >= 10000) return `${(count / 10000).toFixed(1)}w`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
// 获取帖子数量
const getPostsCount = (): number => {
return user.posts_count ?? 0;
};
const getPostsCount = (): number => user.posts_count ?? 0;
const getFollowersCount = (): number => user.followers_count ?? 0;
const getFollowingCount = (): number => user.following_count ?? 0;
const getIsFollowing = (): boolean => user.is_following ?? false;
const getIsFollowingMe = (): boolean => user.is_following_me ?? false;
// 获取粉丝数量
const getFollowersCount = (): number => {
return user.followers_count ?? 0;
};
// 获取关注数量
const getFollowingCount = (): number => {
return user.following_count ?? 0;
};
// 检查是否关注
const getIsFollowing = (): boolean => {
return user.is_following ?? false;
};
// 检查对方是否关注了我
const getIsFollowingMe = (): boolean => {
return user.is_following_me ?? false;
};
// 获取按钮配置类似B站的互关逻辑
const getButtonConfig = (): { title: string; variant: 'primary' | 'outline'; icon?: string } => {
const getButtonConfig = (): { title: string; variant: 'primary' | 'outline' | 'ghost'; icon?: string } => {
const isFollowing = getIsFollowing();
const isFollowingMe = getIsFollowingMe();
if (isFollowing && isFollowingMe) {
// 已互关
return { title: '互相关注', variant: 'outline', icon: 'account-check' };
} else if (isFollowing) {
// 已关注但对方未回关
return { title: '已关注', variant: 'outline', icon: 'check' };
} else if (isFollowingMe) {
// 对方关注了我,但我没关注对方 - 显示回关
return { title: '回关', variant: 'primary', icon: 'plus' };
} else {
// 互不关注
return { title: '关注', variant: 'primary', icon: 'plus' };
}
};
// 根据屏幕尺寸计算封面高度
const coverHeight = isDesktop ? 240 : isWideScreen ? 200 : (width * 9) / 16;
// 根据屏幕尺寸计算头像大小
const avatarSize = isDesktop ? 120 : isWideScreen ? 100 : 90;
const coverHeight = isDesktop ? 280 : isWideScreen ? 240 : (width * 10) / 16;
const avatarSize = isDesktop ? 140 : isWideScreen ? 120 : 88;
const renderStatItem = ({
value,
label,
onPress,
}: {
value: string;
label: string;
onPress?: () => void;
}) => {
const content = (
<View style={styles.statContent}>
<Text variant="h3" style={styles.statNumber}>{value}</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.statLabel}>
{label}
</Text>
</View>
);
const handleMorePress = useCallback(() => {
moreButtonRef.current?.measure((_fx, _fy, _w, h, px, py) => {
const screenWidth = Dimensions.get('window').width;
const menuWidth = 180;
let x = px + _w / 2 - menuWidth / 2;
if (x + menuWidth > screenWidth - 12) {
x = screenWidth - menuWidth - 12;
}
if (x < 12) x = 12;
setMenuPosition({ x, y: py + h + 4 });
setMenuVisible(true);
});
}, []);
if (onPress) {
const handleBlockPress = useCallback(() => {
setMenuVisible(false);
onBlock?.();
}, [onBlock]);
const renderStatLink = (count: number, label: string, onPress?: () => void) => {
if (!onPress) {
return (
<TouchableOpacity
style={[styles.statItem, styles.statItemTouchable]}
onPress={onPress}
activeOpacity={0.75}
>
{content}
</TouchableOpacity>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{formatCount(count)}</Text>
<Text style={styles.statLabel}>{label}</Text>
</View>
);
}
return <View style={styles.statItem}>{content}</View>;
return (
<TouchableOpacity style={styles.statItem} onPress={onPress} activeOpacity={0.7}>
<Text style={styles.statNumber}>{formatCount(count)}</Text>
<Text style={styles.statLabel}>{label}</Text>
</TouchableOpacity>
);
};
const renderDropdownMenu = () => (
<Modal
visible={menuVisible}
transparent
animationType="fade"
onRequestClose={() => setMenuVisible(false)}
>
<Pressable style={styles.menuBackdrop} onPress={() => setMenuVisible(false)}>
<View
style={[
styles.menuContainer,
{ left: menuPosition.x, top: menuPosition.y },
]}
>
<TouchableOpacity
style={styles.menuItem}
onPress={handleBlockPress}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name={isBlocked ? 'account-check-outline' : 'account-off-outline'}
size={18}
color={isBlocked ? colors.success.main : colors.error.main}
/>
<Text
style={[
styles.menuItemText,
{ color: isBlocked ? colors.success.main : colors.error.main },
]}
>
{isBlocked ? '取消拉黑' : '拉黑用户'}
</Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
);
return (
<View style={styles.container}>
{/* 渐变封面背景 */}
{/* 全宽封面背景 */}
<View style={[styles.coverContainer, { height: coverHeight }]}>
<View style={styles.coverTouchable}>
{user.cover_url ? (
@@ -163,382 +173,351 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
resizeMode="cover"
/>
) : (
<LinearGradient
colors={['#FF8F66', '#FF6B35', '#E5521D']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.gradient}
/>
<View style={[styles.coverFallback, { backgroundColor: colors.primary.main }]}>
<MaterialCommunityIcons name="image-outline" size={48} color="rgba(255,255,255,0.4)" />
</View>
)}
</View>
{/* 设置按钮 */}
{isCurrentUser && onSettings && (
<TouchableOpacity style={styles.settingsButton} onPress={onSettings}>
<MaterialCommunityIcons name="cog-outline" size={22} color={colors.text.inverse} />
</TouchableOpacity>
)}
{/* 装饰性波浪 */}
<View style={styles.waveDecoration}>
<View style={styles.wave} />
{/* 顶部导航栏按钮 */}
<View style={styles.topBar}>
{isCurrentUser && onSettings && (
<TouchableOpacity style={styles.iconButton} onPress={onSettings}>
<MaterialCommunityIcons name="cog-outline" size={20} color={colors.text.primary} />
</TouchableOpacity>
)}
</View>
</View>
{/* 用户信息卡片 */}
<View style={[
styles.profileCard,
isWideScreen && styles.profileCardWide,
]}>
{/* 悬浮头像 */}
<View style={[
styles.avatarWrapper,
isWideScreen && styles.avatarWrapperWide,
]}>
<View style={styles.avatarContainer}>
<Avatar
source={user.avatar}
size={avatarSize}
name={user.nickname}
/>
{isCurrentUser && onAvatarPress && (
<TouchableOpacity style={styles.editAvatarButton} onPress={onAvatarPress}>
<MaterialCommunityIcons name="camera" size={14} color={colors.text.inverse} />
{/* 用户信息 */}
<View style={styles.infoSection}>
{/* 头像与操作按钮行 */}
<View style={styles.avatarActionRow}>
<View style={[styles.avatarWrapper, { marginTop: -avatarSize / 2 }]}>
<View style={[styles.avatarContainer, { width: avatarSize, height: avatarSize }]}>
<Avatar
source={user.avatar}
size={avatarSize - 6}
name={user.nickname}
/>
{isCurrentUser && onAvatarPress && (
<TouchableOpacity style={styles.editAvatarButton} onPress={onAvatarPress}>
<MaterialCommunityIcons name="camera" size={14} color={colors.text.inverse} />
</TouchableOpacity>
)}
</View>
</View>
<View style={styles.actionButtons}>
{isCurrentUser ? (
<TouchableOpacity style={styles.editProfileButton} onPress={onEditProfile} activeOpacity={0.8}>
<Text style={styles.editProfileText}></Text>
</TouchableOpacity>
) : (
<View style={styles.buttonRow}>
<View ref={moreButtonRef} collapsable={false}>
<TouchableOpacity style={styles.iconButtonSmall} onPress={handleMorePress}>
<MaterialCommunityIcons name="dots-horizontal" size={20} color={colors.text.primary} />
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.iconButtonSmall} onPress={onMessage}>
<MaterialCommunityIcons name="email-outline" size={18} color={colors.text.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.followBtn,
getButtonConfig().variant === 'outline' && styles.followBtnOutline,
getButtonConfig().variant === 'primary' && styles.followBtnPrimary,
]}
onPress={onFollow}
activeOpacity={0.8}
>
<Text
style={[
styles.followBtnText,
...(getButtonConfig().variant === 'primary' ? [styles.followBtnTextPrimary] : []),
]}
>
{getButtonConfig().title}
</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
{/* 用户名和简介 */}
<View style={styles.userInfo}>
<Text variant="h2" style={[
styles.nickname,
isWideScreen ? styles.nicknameWide : {},
]}>
{user.nickname}
</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.username}>
@{user.username}
</Text>
{user.bio ? (
<Text variant="body" color={colors.text.secondary} style={[
styles.bio,
isWideScreen ? styles.bioWide : {},
]}>
{user.bio}
</Text>
) : (
<Text variant="body" color={colors.text.hint} style={styles.bioPlaceholder}>
~
</Text>
)}
{/* 用户名 */}
<View style={styles.nameSection}>
<Text style={styles.nickname}>{user.nickname}</Text>
<Text style={styles.username}>@{user.username}</Text>
</View>
{/* 个人信息标签 */}
{/* 简介 */}
{user.bio ? (
<Text style={styles.bio}>{user.bio}</Text>
) : null}
{/* 元信息 */}
<View style={styles.metaInfo}>
{user.location?.trim() ? (
<View style={styles.metaTag}>
<MaterialCommunityIcons name="map-marker-outline" size={12} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} style={styles.metaTagText}>
{user.location}
</Text>
<View style={styles.metaItem}>
<MaterialCommunityIcons name="map-marker-outline" size={14} color={colors.text.hint} />
<Text style={styles.metaText}>{user.location}</Text>
</View>
) : null}
{user.website?.trim() ? (
<View style={styles.metaTag}>
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
{user.website.replace(/^https?:\/\//, '')}
</Text>
<View style={styles.metaItem}>
<MaterialCommunityIcons name="link-variant" size={14} color={colors.info.main} />
<Text style={[styles.metaText, styles.metaLink]}>{user.website.replace(/^https?:\/\//, '')}</Text>
</View>
) : null}
<View style={styles.metaTag}>
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
<View style={styles.metaItem}>
<MaterialCommunityIcons name="calendar-outline" size={14} color={colors.text.hint} />
<Text style={styles.metaText}>
{new Date(user.created_at || Date.now()).getFullYear()}
{new Date(user.created_at || Date.now()).getMonth() + 1}
</Text>
</View>
</View>
{/* 统计数据 - 卡片式 */}
<View style={[
styles.statsCard,
isWideScreen && styles.statsCardWide,
]}>
{renderStatItem({
value: formatCount(getPostsCount()),
label: '帖子',
onPress: onPostsPress,
})}
<View style={styles.statDivider} />
{renderStatItem({
value: formatCount(getFollowingCount()),
label: '关注',
onPress: onFollowingPress,
})}
<View style={styles.statDivider} />
{renderStatItem({
value: formatCount(getFollowersCount()),
label: '粉丝',
onPress: onFollowersPress,
})}
</View>
{/* 操作按钮 */}
<View style={styles.actionButtons}>
{isCurrentUser ? (
<View style={styles.buttonRow} />
) : (
<View style={StyleSheet.flatten([
styles.buttonRow,
isWideScreen && styles.buttonRowWide,
])}>
<Button
title={getButtonConfig().title}
onPress={onFollow}
variant={getButtonConfig().variant}
style={StyleSheet.flatten([
styles.followButton,
isWideScreen && styles.followButtonWide,
])}
icon={getButtonConfig().icon}
/>
<TouchableOpacity style={styles.messageButton} onPress={onMessage}>
<MaterialCommunityIcons name="message-text-outline" size={20} color={colors.primary.main} />
</TouchableOpacity>
<TouchableOpacity style={styles.moreButton} onPress={onMore}>
<MaterialCommunityIcons name="dots-horizontal" size={24} color={colors.text.secondary} />
</TouchableOpacity>
</View>
)}
{/* 统计数据 - Twitter 风格内联 */}
<View style={styles.statsRow}>
{renderStatLink(getFollowingCount(), '正在关注', onFollowingPress)}
{renderStatLink(getFollowersCount(), '关注者', onFollowersPress)}
</View>
</View>
{renderDropdownMenu()}
</View>
);
};
function createUserProfileHeaderStyles(colors: AppColors) {
return StyleSheet.create({
container: {
backgroundColor: colors.background.default,
},
coverContainer: {
position: 'relative',
overflow: 'hidden',
},
coverTouchable: {
width: '100%',
height: '100%',
},
coverImage: {
width: '100%',
height: '100%',
},
gradient: {
width: '100%',
height: '100%',
},
settingsButton: {
position: 'absolute',
top: spacing.lg,
right: spacing.lg,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
},
waveDecoration: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 40,
},
wave: {
width: '100%',
height: '100%',
backgroundColor: colors.background.default,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
},
profileCard: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.md,
marginTop: -50,
borderRadius: borderRadius.xl,
padding: spacing.lg,
...shadows.md,
},
profileCardWide: {
marginHorizontal: spacing.lg,
marginTop: -60,
padding: spacing.xl,
},
avatarWrapper: {
alignItems: 'center',
marginTop: -60,
marginBottom: spacing.md,
},
avatarWrapperWide: {
marginTop: -80,
marginBottom: spacing.lg,
},
avatarContainer: {
position: 'relative',
padding: 4,
backgroundColor: colors.background.paper,
borderRadius: 50,
},
editAvatarButton: {
position: 'absolute',
bottom: 0,
right: 0,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.primary.main,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
userInfo: {
alignItems: 'center',
marginBottom: spacing.md,
},
nickname: {
marginBottom: spacing.xs,
fontWeight: '700',
},
nicknameWide: {
fontSize: fontSizes['3xl'],
},
username: {
marginBottom: spacing.sm,
},
bio: {
textAlign: 'center',
marginTop: spacing.sm,
lineHeight: 20,
},
bioWide: {
fontSize: fontSizes.md,
lineHeight: 24,
maxWidth: 600,
},
bioPlaceholder: {
textAlign: 'center',
marginTop: spacing.sm,
fontStyle: 'italic',
},
metaInfo: {
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
marginBottom: spacing.md,
gap: spacing.sm,
},
metaTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.md,
},
metaTagText: {
marginLeft: spacing.xs,
fontSize: fontSizes.xs,
},
statsCard: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: 'transparent',
paddingHorizontal: spacing.xs,
paddingVertical: spacing.xs,
marginBottom: spacing.md,
},
statsCardWide: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
marginBottom: spacing.lg,
},
statItem: {
flex: 1,
minHeight: 58,
justifyContent: 'center',
},
statItemTouchable: {
borderRadius: borderRadius.md,
},
statContent: {
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.xs,
},
statNumber: {
fontWeight: '600',
marginBottom: 0,
},
statLabel: {
fontSize: fontSizes.xs,
marginTop: 2,
},
statDivider: {
width: 1,
height: 24,
backgroundColor: colors.divider + '55',
},
actionButtons: {
marginTop: spacing.sm,
},
buttonRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
buttonRowWide: {
justifyContent: 'center',
gap: spacing.md,
},
editButton: {
flex: 1,
},
followButton: {
flex: 1,
},
followButtonWide: {
flex: 0,
minWidth: 120,
},
messageButton: {
width: 44,
height: 44,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
},
moreButton: {
width: 44,
height: 44,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
},
settingsButtonOnly: {
alignSelf: 'center',
padding: spacing.sm,
},
container: {
backgroundColor: colors.background.paper,
},
// 封面
coverContainer: {
position: 'relative',
backgroundColor: colors.background.default,
},
coverTouchable: {
width: '100%',
height: '100%',
},
coverImage: {
width: '100%',
height: '100%',
},
coverFallback: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
// 顶部栏
topBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
flexDirection: 'row',
justifyContent: 'flex-end',
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
},
iconButton: {
width: 36,
height: 36,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.85)',
justifyContent: 'center',
alignItems: 'center',
...shadows.sm,
},
// 信息区
infoSection: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.sm,
paddingBottom: spacing.lg,
},
avatarActionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
marginBottom: spacing.sm,
},
avatarWrapper: {
// 负边距在 inline style 中计算
},
avatarContainer: {
borderRadius: borderRadius.full,
backgroundColor: colors.background.paper,
padding: 3,
justifyContent: 'center',
alignItems: 'center',
},
editAvatarButton: {
position: 'absolute',
bottom: 4,
right: 4,
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.primary.main,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
// 操作按钮
actionButtons: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
buttonRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
iconButtonSmall: {
width: 38,
height: 38,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
},
editProfileButton: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.divider,
backgroundColor: colors.background.paper,
},
editProfileText: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.text.primary,
},
followBtn: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
minWidth: 80,
alignItems: 'center',
justifyContent: 'center',
},
followBtnPrimary: {
backgroundColor: colors.text.primary,
},
followBtnOutline: {
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider,
},
followBtnText: {
fontSize: fontSizes.md,
fontWeight: '700',
color: colors.text.primary,
},
followBtnTextPrimary: {
color: colors.background.paper,
},
// 名字
nameSection: {
marginBottom: spacing.sm,
},
nickname: {
fontSize: fontSizes['2xl'],
fontWeight: '800',
color: colors.text.primary,
marginBottom: 2,
},
username: {
fontSize: fontSizes.md,
color: colors.text.hint,
},
// 简介
bio: {
fontSize: fontSizes.md,
color: colors.text.primary,
lineHeight: 20,
marginBottom: spacing.sm,
},
// 元信息
metaInfo: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.md,
marginBottom: spacing.sm,
},
metaItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
metaText: {
fontSize: fontSizes.sm,
color: colors.text.hint,
},
metaLink: {
color: colors.info.main,
},
// 统计
statsRow: {
flexDirection: 'row',
gap: spacing.lg,
marginTop: spacing.xs,
},
statItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
statNumber: {
fontSize: fontSizes.md,
fontWeight: '700',
color: colors.text.primary,
},
statLabel: {
fontSize: fontSizes.sm,
color: colors.text.hint,
},
// 下拉菜单
menuBackdrop: {
flex: 1,
backgroundColor: 'transparent',
},
menuContainer: {
position: 'absolute',
minWidth: 160,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
...shadows.lg,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
overflow: 'hidden',
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
},
menuItemText: {
fontSize: fontSizes.md,
fontWeight: '600',
},
});
}
// 使用 React.memo 避免不必要的重新渲染
const MemoizedUserProfileHeader = React.memo(UserProfileHeader);
import { shadows } from '../../theme';
const MemoizedUserProfileHeader = React.memo(UserProfileHeader);
export default MemoizedUserProfileHeader;

View File

@@ -2,56 +2,54 @@ import React from 'react';
import { TouchableOpacity, StyleProp, ViewStyle, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { borderRadius, spacing, useAppColors } from '../../theme';
import { useAppColors } from '../../theme';
type AppBackButtonIcon = 'arrow-left' | 'close';
type AppBackButtonIcon = 'chevron-left' | 'arrow-left' | 'close';
interface AppBackButtonProps {
onPress: () => void;
icon?: AppBackButtonIcon;
style?: StyleProp<ViewStyle>;
iconColor?: string;
backgroundColor?: string;
hitSlop?: number;
testID?: string;
}
const AppBackButton: React.FC<AppBackButtonProps> = ({
onPress,
icon = 'arrow-left',
icon = 'chevron-left',
style,
iconColor: iconColorProp,
backgroundColor: backgroundColorProp,
hitSlop = 8,
testID,
}) => {
const colors = useAppColors();
const iconColor = iconColorProp ?? colors.text.primary;
const backgroundColor = backgroundColorProp ?? colors.background.paper;
return (
<TouchableOpacity
onPress={onPress}
style={[styles.button, { backgroundColor }, style]}
activeOpacity={0.75}
style={[styles.button, style]}
activeOpacity={0.6}
hitSlop={hitSlop}
testID={testID}
accessibilityRole="button"
accessibilityLabel={icon === 'close' ? '关闭' : '返回'}
>
<MaterialCommunityIcons name={icon} size={22} color={iconColor} />
<MaterialCommunityIcons name={icon} size={36} color={iconColor} />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
width: 36,
height: 36,
borderRadius: borderRadius.full,
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.xs,
marginLeft: -12,
marginRight: 0,
padding: 0,
},
});