From ad19bc2af78b8ef3d17a79566c60b01ac8bfe978 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sat, 25 Apr 2026 00:39:40 +0800 Subject: [PATCH] feat(ui): unify design system to Twitter/X style across screens and improve message handling 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 --- src/components/business/UserProfileHeader.tsx | 835 +++++++++--------- src/components/common/AppBackButton.tsx | 24 +- src/screens/auth/QRCodeConfirmScreen.tsx | 2 +- src/screens/auth/RegisterScreen.tsx | 4 +- src/screens/auth/VerificationFormScreen.tsx | 2 + src/screens/home/PostDetailScreen.tsx | 6 +- src/screens/material/MaterialDetailScreen.tsx | 10 +- src/screens/material/MaterialsScreen.tsx | 10 +- .../material/SubjectMaterialsScreen.tsx | 10 +- src/screens/message/GroupInfoScreen.tsx | 8 +- src/screens/message/GroupMembersScreen.tsx | 1 + src/screens/message/MessageListScreen.tsx | 35 +- src/screens/message/NotificationsScreen.tsx | 23 +- .../message/components/ChatScreen/styles.ts | 8 +- .../components/ConversationListRow.tsx | 70 +- src/screens/profile/EditProfileScreen.tsx | 702 ++++++--------- src/screens/profile/SettingsScreen.tsx | 90 +- src/screens/profile/UserProfileScreen.tsx | 52 +- src/screens/profile/useUserProfile.ts | 80 +- src/screens/schedule/ScheduleScreen.tsx | 2 +- src/services/background/backgroundService.ts | 19 +- src/services/core/wsService.ts | 7 + .../notification/systemNotificationService.ts | 28 + .../message/services/MessageSyncService.ts | 12 + .../message/services/WSMessageHandler.ts | 21 + src/stores/message/store.ts | 10 + 26 files changed, 950 insertions(+), 1121 deletions(-) diff --git a/src/components/business/UserProfileHeader.tsx b/src/components/business/UserProfileHeader.tsx index e685e70..0b4e81b 100644 --- a/src/components/business/UserProfileHeader.tsx +++ b/src/components/business/UserProfileHeader.tsx @@ -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 = ({ user, isCurrentUser = false, + isBlocked = false, onFollow, onSettings, onEditProfile, onMessage, - onMore, - onPostsPress, + onBlock, onFollowingPress, onFollowersPress, onAvatarPress, @@ -52,108 +52,118 @@ const UserProfileHeader: React.FC = ({ 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(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 = ( - - {value} - - {label} - - - ); + 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 ( - - {content} - + + {formatCount(count)} + {label} + ); } - - return {content}; + return ( + + {formatCount(count)} + {label} + + ); }; + const renderDropdownMenu = () => ( + setMenuVisible(false)} + > + setMenuVisible(false)}> + + + + + {isBlocked ? '取消拉黑' : '拉黑用户'} + + + + + + ); + return ( - {/* 渐变封面背景 */} + {/* 全宽封面背景 */} {user.cover_url ? ( @@ -163,382 +173,351 @@ const UserProfileHeader: React.FC = ({ resizeMode="cover" /> ) : ( - + + + )} - - {/* 设置按钮 */} - {isCurrentUser && onSettings && ( - - - - )} - {/* 装饰性波浪 */} - - + {/* 顶部导航栏按钮 */} + + {isCurrentUser && onSettings && ( + + + + )} - {/* 用户信息卡片 */} - - {/* 悬浮头像 */} - - - - {isCurrentUser && onAvatarPress && ( - - + {/* 用户信息区 */} + + {/* 头像与操作按钮行 */} + + + + + {isCurrentUser && onAvatarPress && ( + + + + )} + + + + + {isCurrentUser ? ( + + 编辑资料 + ) : ( + + + + + + + + + + + + {getButtonConfig().title} + + + )} - {/* 用户名和简介 */} - - - {user.nickname} - - - @{user.username} - - - {user.bio ? ( - - {user.bio} - - ) : ( - - 这个人很懒,还没有写简介~ - - )} + {/* 用户名 */} + + {user.nickname} + @{user.username} - {/* 个人信息标签 */} + {/* 简介 */} + {user.bio ? ( + {user.bio} + ) : null} + + {/* 元信息 */} {user.location?.trim() ? ( - - - - {user.location} - + + + {user.location} ) : null} {user.website?.trim() ? ( - - - - {user.website.replace(/^https?:\/\//, '')} - + + + {user.website.replace(/^https?:\/\//, '')} ) : null} - - - + + + 加入于 {new Date(user.created_at || Date.now()).getFullYear()}年 + {new Date(user.created_at || Date.now()).getMonth() + 1}月 - {/* 统计数据 - 卡片式 */} - - {renderStatItem({ - value: formatCount(getPostsCount()), - label: '帖子', - onPress: onPostsPress, - })} - - {renderStatItem({ - value: formatCount(getFollowingCount()), - label: '关注', - onPress: onFollowingPress, - })} - - {renderStatItem({ - value: formatCount(getFollowersCount()), - label: '粉丝', - onPress: onFollowersPress, - })} - - - {/* 操作按钮 */} - - {isCurrentUser ? ( - - ) : ( - -