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
This commit is contained in:
@@ -32,7 +32,7 @@ function createQrCodeConfirmStyles(colors: AppColors) {
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
closeButton: {
|
||||
padding: spacing.sm,
|
||||
marginLeft: -12,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: fontSizes.lg,
|
||||
|
||||
@@ -309,10 +309,10 @@ function createRegisterStyles(colors: AppColors) {
|
||||
backIconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.background.default,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: -12,
|
||||
padding: 0,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
|
||||
@@ -327,6 +327,8 @@ function createStyles(colors: AppColors) {
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: -12,
|
||||
padding: 0,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
|
||||
@@ -1929,10 +1929,8 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
headerBackButton: {
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: spacing.xs,
|
||||
marginLeft: spacing.xs,
|
||||
marginRight: spacing.sm,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
// 帖子容器
|
||||
postContainer: {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { AppBackButton } from '../../components/common';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
@@ -275,9 +276,7 @@ export const MaterialDetailScreen: React.FC = () => {
|
||||
{/* Header */}
|
||||
<View style={[styles.header, isWideScreen && styles.headerWide]}>
|
||||
<View style={styles.headerLeft}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton onPress={() => router.back()} />
|
||||
</View>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||
@@ -341,12 +340,9 @@ function createMaterialDetailStyles(colors: AppColors) {
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
width: 40,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButton: {
|
||||
padding: spacing.xs,
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { AppBackButton } from '../../components/common';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
@@ -166,9 +167,7 @@ export const MaterialsScreen: React.FC = () => {
|
||||
{/* Header */}
|
||||
<View style={[styles.header, isWideScreen && styles.headerWide]}>
|
||||
<View style={styles.headerLeft}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton onPress={() => router.back()} />
|
||||
</View>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||
@@ -227,12 +226,9 @@ function createMaterialsStyles(colors: AppColors) {
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
width: 40,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButton: {
|
||||
padding: spacing.xs,
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { AppBackButton } from '../../components/common';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
@@ -255,9 +256,7 @@ export const SubjectMaterialsScreen: React.FC = () => {
|
||||
{/* Header */}
|
||||
<View style={[styles.header, isWideScreen && styles.headerWide]}>
|
||||
<View style={styles.headerLeft}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton onPress={() => router.back()} />
|
||||
</View>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||
@@ -302,12 +301,9 @@ function createSubjectMaterialsStyles(colors: AppColors) {
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
width: 40,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButton: {
|
||||
padding: spacing.xs,
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1108,10 +1108,10 @@ function createGroupInfoStyles(colors: AppColors) {
|
||||
fontSize: fontSizes.xl,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -690,6 +690,7 @@ function createGroupMembersStyles(colors: AppColors) {
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
section: {
|
||||
marginBottom: spacing.md,
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
useUnreadCount,
|
||||
useMarkAsRead,
|
||||
useMessageManagerConversations,
|
||||
useMessageStore,
|
||||
} from '../../stores';
|
||||
import { Avatar, Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
|
||||
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
|
||||
@@ -113,6 +114,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
|
||||
// 使用 MessageManager 获取未读数和系统通知数
|
||||
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
|
||||
const lastSystemMessageAt = useMessageStore(state => state.lastSystemMessageAt);
|
||||
const { markAllAsRead, isMarking } = useMarkAsRead(null);
|
||||
|
||||
// 【新架构】使用MessageManager的hook创建会话
|
||||
@@ -160,14 +162,14 @@ export const MessageListScreen: React.FC = () => {
|
||||
seq: 0,
|
||||
segments: [{ type: 'text', data: { text: '系统通知' } }],
|
||||
status: 'normal',
|
||||
created_at: new Date().toISOString(),
|
||||
created_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
},
|
||||
last_message_at: new Date().toISOString(),
|
||||
last_message_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
unread_count: systemUnreadCount,
|
||||
participants: [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}), [systemUnreadCount]);
|
||||
created_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
updated_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
}), [systemUnreadCount, lastSystemMessageAt]);
|
||||
|
||||
// 动画值
|
||||
const [scaleAnims] = useState(() =>
|
||||
@@ -451,6 +453,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
// 创建系统通知会话项
|
||||
const createSystemMessageItem = (): ConversationResponse => {
|
||||
const noticeContent = systemUnreadCount > 0 ? `您有 ${systemUnreadCount} 条未读通知` : '暂无新通知';
|
||||
const lastTime = lastSystemMessageAt || new Date().toISOString();
|
||||
return {
|
||||
id: SYSTEM_MESSAGE_CHANNEL_ID,
|
||||
type: 'private',
|
||||
@@ -462,13 +465,13 @@ export const MessageListScreen: React.FC = () => {
|
||||
seq: 0,
|
||||
segments: [{ type: 'text', data: { text: noticeContent } }],
|
||||
status: 'normal',
|
||||
created_at: new Date().toISOString(),
|
||||
created_at: lastTime,
|
||||
},
|
||||
last_message_at: new Date().toISOString(),
|
||||
last_message_at: lastTime,
|
||||
unread_count: systemUnreadCount,
|
||||
participants: [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_at: lastTime,
|
||||
updated_at: lastTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -936,7 +939,8 @@ function createMessageListStyles(colors: AppColors) {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.default,
|
||||
...shadows.sm,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider + '60',
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
@@ -949,7 +953,7 @@ function createMessageListStyles(colors: AppColors) {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
@@ -1028,6 +1032,7 @@ function createMessageListStyles(colors: AppColors) {
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
@@ -1086,11 +1091,9 @@ function createMessageListStyles(colors: AppColors) {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: spacing.sm,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: `${colors.divider}50`,
|
||||
backgroundColor: colors.background.default,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: `${colors.divider}50`,
|
||||
},
|
||||
searchResultContent: {
|
||||
flex: 1,
|
||||
|
||||
@@ -32,9 +32,10 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||
import { SystemMessageResponse } from '../../types/dto';
|
||||
import { messageService } from '@/services/message';
|
||||
import { systemNotificationService } from '@/services/notification';
|
||||
import { commentService } from '@/services/post';
|
||||
import { SystemMessageItem } from '../../components/business';
|
||||
import { Text, ResponsiveContainer } from '../../components/common';
|
||||
import { Text, ResponsiveContainer, AppBackButton } from '../../components/common';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useMessageManagerSystemUnreadCount } from '../../stores';
|
||||
@@ -174,6 +175,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
setSystemUnreadCount(0);
|
||||
// 同步更新全局 TabBar 红点
|
||||
messageManager.fetchUnreadCount();
|
||||
// 清除通知栏中已读的系统消息通知
|
||||
systemNotificationService.clearAllNotifications().catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('一键已读失败:', error);
|
||||
}
|
||||
@@ -381,11 +384,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
return (
|
||||
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
|
||||
{shouldShowBackButton ? (
|
||||
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.8}>
|
||||
<View style={styles.backIconButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={22} color={colors.text.primary} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.backButton}>
|
||||
<AppBackButton onPress={onBack} />
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.backButtonPlaceholder} />
|
||||
)}
|
||||
@@ -673,14 +674,6 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backIconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.background.default,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
@@ -735,7 +728,7 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 24,
|
||||
paddingBottom: 88,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: 32,
|
||||
|
||||
@@ -106,12 +106,12 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
height: 56,
|
||||
},
|
||||
backButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: -12,
|
||||
padding: 0,
|
||||
},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
|
||||
@@ -157,32 +157,27 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.divider + '40',
|
||||
backgroundColor: colors.background.default,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.chat.borderLight,
|
||||
},
|
||||
conversationItemSelected: {
|
||||
backgroundColor: colors.primary.light + '12',
|
||||
borderColor: colors.primary.main + '60',
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
systemAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: borderRadius.md,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
conversationContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
marginLeft: spacing.lg,
|
||||
},
|
||||
conversationHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -197,49 +192,49 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
officialBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
officialBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontSize: 9,
|
||||
fontWeight: '800',
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '700',
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.3,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
groupIcon: {
|
||||
marginRight: 4,
|
||||
marginRight: 3,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 2,
|
||||
fontWeight: '500',
|
||||
},
|
||||
pinnedIcon: {
|
||||
marginLeft: 6,
|
||||
marginLeft: 5,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: colors.text.secondary,
|
||||
color: colors.text.hint,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontWeight: '400',
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -250,25 +245,26 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
color: colors.text.secondary,
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
lineHeight: 20,
|
||||
},
|
||||
unreadMessageText: {
|
||||
color: colors.text.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
unreadBadge: {
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: colors.primary.main,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.error.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -321,12 +317,12 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
<View style={styles.avatarContainer}>
|
||||
{isSystemChannel ? (
|
||||
<View style={styles.systemAvatar}>
|
||||
<MaterialCommunityIcons name="bell-ring" size={24} color={colors.primary.contrast} />
|
||||
<MaterialCommunityIcons name="bell-ring" size={26} color={colors.primary.contrast} />
|
||||
</View>
|
||||
) : isGroupChat ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
{displayAvatar ? (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
) : (
|
||||
<View style={styles.groupAvatarPlaceholder}>
|
||||
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
||||
@@ -334,7 +330,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* 编辑资料页 EditProfileScreen(响应式适配)
|
||||
* 编辑资料页 EditProfileScreen - Twitter/X 风格
|
||||
* 威友 - 编辑用户资料
|
||||
* 与用户资料页样式完全一致
|
||||
* 表单在宽屏下居中显示
|
||||
* 与 Twitter/X 个人资料编辑页样式一致
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
@@ -21,274 +20,199 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
borderRadius,
|
||||
shadows,
|
||||
useAppColors,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { Avatar, Button, Text } from '../../components/common';
|
||||
import { Avatar, Text } from '../../components/common';
|
||||
import { authService, uploadService } from '../../services';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
|
||||
// 内容最大宽度
|
||||
const CONTENT_MAX_WIDTH = 720;
|
||||
|
||||
function createEditProfileStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
keyboardView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
keyboardView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
|
||||
// ===== 预览区域 - 与 UserProfileHeader 完全一致 =====
|
||||
previewContainer: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
coverContainer: {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 14,
|
||||
},
|
||||
coverTouchable: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
coverImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
gradient: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
coverUploadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
coverEditOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
coverEditText: {
|
||||
color: colors.text.inverse,
|
||||
fontSize: fontSizes.sm,
|
||||
marginTop: spacing.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
waveDecoration: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 40,
|
||||
},
|
||||
wave: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopLeftRadius: 30,
|
||||
borderTopRightRadius: 30,
|
||||
},
|
||||
// ===== 封面区域 - Twitter 风格全宽 =====
|
||||
coverContainer: {
|
||||
position: 'relative',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
coverTouchable: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
coverImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
coverFallback: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: colors.primary.main,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
coverOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
// 用户信息卡片
|
||||
profileCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: -50,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg,
|
||||
},
|
||||
avatarWrapper: {
|
||||
alignItems: 'center',
|
||||
marginTop: -60,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
padding: 4,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: 50,
|
||||
},
|
||||
avatarUploadingOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
borderRadius: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
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',
|
||||
},
|
||||
username: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
bio: {
|
||||
textAlign: 'center',
|
||||
marginTop: spacing.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
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,
|
||||
},
|
||||
editHint: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.md,
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
editHintText: {
|
||||
fontSize: fontSizes.xs,
|
||||
},
|
||||
// ===== 信息区 =====
|
||||
infoSection: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
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',
|
||||
position: 'relative',
|
||||
},
|
||||
avatarOverlay: {
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
left: 3,
|
||||
right: 3,
|
||||
bottom: 3,
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
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,
|
||||
},
|
||||
|
||||
// ===== 表单区域 =====
|
||||
formCard: {
|
||||
borderRadius: 14,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
maxWidth: CONTENT_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
formField: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
fieldIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.background.default,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
fieldContent: {
|
||||
flex: 1,
|
||||
},
|
||||
fieldLabel: {
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '500',
|
||||
},
|
||||
fieldInput: {
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
disabledInput: {
|
||||
color: colors.text.disabled,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: colors.divider,
|
||||
marginVertical: spacing.md,
|
||||
marginLeft: 56,
|
||||
},
|
||||
// 名字预览
|
||||
nameSection: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
nickname: {
|
||||
fontSize: fontSizes['2xl'],
|
||||
fontWeight: '800',
|
||||
color: colors.text.primary,
|
||||
marginBottom: 2,
|
||||
},
|
||||
username: {
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
|
||||
// 保存按钮
|
||||
buttonContainer: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
saveButton: {
|
||||
height: 50,
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
saveButtonContent: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonIcon: {
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
saveButtonText: {
|
||||
color: colors.text.inverse,
|
||||
},
|
||||
// 编辑提示
|
||||
editHint: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.xs,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
editHintText: {
|
||||
fontSize: fontSizes.xs,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
|
||||
// ===== 表单区域 - Twitter 风格全宽卡片 =====
|
||||
formSection: {
|
||||
backgroundColor: colors.background.paper,
|
||||
marginTop: spacing.md,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
formField: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
formFieldLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
fieldLabel: {
|
||||
fontSize: fontSizes.sm,
|
||||
fontWeight: '500',
|
||||
color: colors.text.secondary,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
fieldInput: {
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
paddingVertical: spacing.xs,
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
disabledInput: {
|
||||
color: colors.text.disabled,
|
||||
},
|
||||
charCount: {
|
||||
textAlign: 'right',
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
|
||||
// 保存按钮
|
||||
buttonContainer: {
|
||||
marginTop: spacing.lg,
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
saveButton: {
|
||||
height: 48,
|
||||
backgroundColor: colors.text.primary,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
saveButtonText: {
|
||||
color: colors.background.paper,
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type EditProfileSheet = ReturnType<typeof createEditProfileStyles>;
|
||||
|
||||
|
||||
// 表单输入项组件
|
||||
interface FormFieldProps {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
@@ -301,10 +225,10 @@ interface FormFieldProps {
|
||||
editable?: boolean;
|
||||
sheet: EditProfileSheet;
|
||||
colors: AppColors;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
const FormField: React.FC<FormFieldProps> = ({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
@@ -317,32 +241,33 @@ const FormField: React.FC<FormFieldProps> = ({
|
||||
editable = true,
|
||||
sheet,
|
||||
colors,
|
||||
isLast = false,
|
||||
}) => {
|
||||
return (
|
||||
<View style={sheet.formField}>
|
||||
<View style={sheet.fieldIconContainer}>
|
||||
<MaterialCommunityIcons name={icon as any} size={22} color={colors.primary.main} />
|
||||
</View>
|
||||
<View style={sheet.fieldContent}>
|
||||
<Text variant="caption" style={sheet.fieldLabel}>{label}</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
sheet.fieldInput,
|
||||
multiline && sheet.textArea,
|
||||
!editable && sheet.disabledInput
|
||||
]}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.text.hint}
|
||||
maxLength={maxLength}
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
autoCapitalize={autoCapitalize}
|
||||
keyboardType={keyboardType}
|
||||
editable={editable}
|
||||
/>
|
||||
</View>
|
||||
<View style={[sheet.formField, isLast && sheet.formFieldLast]}>
|
||||
<Text style={sheet.fieldLabel}>{label}</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
sheet.fieldInput,
|
||||
multiline && sheet.textArea,
|
||||
!editable && sheet.disabledInput
|
||||
]}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.text.hint}
|
||||
maxLength={maxLength}
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
autoCapitalize={autoCapitalize}
|
||||
keyboardType={keyboardType}
|
||||
editable={editable}
|
||||
/>
|
||||
{maxLength && (
|
||||
<Text variant="caption" color={colors.text.hint} style={sheet.charCount}>
|
||||
{value.length}/{maxLength}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -353,7 +278,7 @@ export const EditProfileScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const { currentUser, updateUser } = useAuthStore();
|
||||
const { isWideScreen, isMobile, width } = useResponsive();
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [nickname, setNickname] = useState(currentUser?.nickname || '');
|
||||
@@ -367,16 +292,13 @@ export const EditProfileScreen: React.FC = () => {
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
const [uploadingCover, setUploadingCover] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// 底部间距,避免被 TabBar 遮挡
|
||||
|
||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
|
||||
const coverHeight = isWideScreen ? Math.min((width * 10) / 16, 320) : (width * 10) / 16;
|
||||
const avatarSize = isWideScreen ? 120 : 88;
|
||||
|
||||
// 根据屏幕宽度计算封面高度
|
||||
const coverHeight = isWideScreen ? Math.min((width * 9) / 16, 300) : (width * 9) / 16;
|
||||
|
||||
// 选择头图
|
||||
const handlePickCover = async () => {
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (!permissionResult.granted) {
|
||||
Alert.alert('权限不足', '需要访问相册权限来选择头图');
|
||||
return;
|
||||
@@ -416,10 +338,8 @@ export const EditProfileScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 选择头像
|
||||
const handlePickAvatar = async () => {
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (!permissionResult.granted) {
|
||||
Alert.alert('权限不足', '需要访问相册权限来选择头像');
|
||||
return;
|
||||
@@ -459,7 +379,6 @@ export const EditProfileScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存资料
|
||||
const handleSave = async () => {
|
||||
if (!nickname.trim()) {
|
||||
Alert.alert('错误', '昵称不能为空');
|
||||
@@ -512,69 +431,56 @@ export const EditProfileScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单内容
|
||||
const renderFormContent = () => (
|
||||
<>
|
||||
{/* ===== 用户资料预览区域 - 与 UserProfileHeader 完全一致 ===== */}
|
||||
<View style={styles.previewContainer}>
|
||||
{/* 封面背景 */}
|
||||
<View style={[styles.coverContainer, { height: coverHeight }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.coverTouchable}
|
||||
onPress={handlePickCover}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{coverUrl ? (
|
||||
<Image
|
||||
source={{ uri: coverUrl }}
|
||||
style={styles.coverImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<LinearGradient
|
||||
colors={['#FF8F66', '#FF6B35', '#E5521D']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.gradient}
|
||||
/>
|
||||
)}
|
||||
{/* ===== 封面区域 - Twitter 风格全宽 ===== */}
|
||||
<View style={[styles.coverContainer, { height: coverHeight }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.coverTouchable}
|
||||
onPress={handlePickCover}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{coverUrl ? (
|
||||
<Image
|
||||
source={{ uri: coverUrl }}
|
||||
style={styles.coverImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.coverFallback}>
|
||||
<MaterialCommunityIcons name="image-outline" size={48} color="rgba(255,255,255,0.4)" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 头图编辑蒙版 */}
|
||||
{uploadingCover ? (
|
||||
<View style={styles.coverUploadingOverlay}>
|
||||
<MaterialCommunityIcons name="loading" size={32} color={colors.text.inverse} />
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.coverEditOverlay}>
|
||||
<MaterialCommunityIcons name="camera" size={28} color={colors.text.inverse} />
|
||||
<Text style={styles.coverEditText}>更换头图</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{uploadingCover ? (
|
||||
<View style={styles.coverOverlay}>
|
||||
<MaterialCommunityIcons name="loading" size={32} color={colors.text.inverse} />
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.coverOverlay}>
|
||||
<MaterialCommunityIcons name="camera" size={28} color={colors.text.inverse} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 装饰性波浪 */}
|
||||
<View style={styles.waveDecoration}>
|
||||
<View style={styles.wave} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
<View style={styles.profileCard}>
|
||||
{/* 悬浮头像 */}
|
||||
<View style={styles.avatarWrapper}>
|
||||
{/* ===== 用户信息区 - Twitter 风格左对齐 ===== */}
|
||||
<View style={styles.infoSection}>
|
||||
<View style={styles.avatarActionRow}>
|
||||
<View style={[styles.avatarWrapper, { marginTop: -avatarSize / 2 }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.avatarContainer}
|
||||
style={[styles.avatarContainer, { width: avatarSize, height: avatarSize }]}
|
||||
onPress={handlePickAvatar}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Avatar
|
||||
source={avatar || null}
|
||||
size={isWideScreen ? 110 : 90}
|
||||
size={avatarSize - 6}
|
||||
name={nickname}
|
||||
/>
|
||||
{uploadingAvatar && (
|
||||
<View style={styles.avatarUploadingOverlay}>
|
||||
<MaterialCommunityIcons name="loading" size={32} color={colors.text.inverse} />
|
||||
<View style={styles.avatarOverlay}>
|
||||
<MaterialCommunityIcons name="loading" size={24} color={colors.text.inverse} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.editAvatarButton}>
|
||||
@@ -582,85 +488,22 @@ export const EditProfileScreen: React.FC = () => {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 用户名和简介 */}
|
||||
<View style={styles.userInfo}>
|
||||
<Text variant="h2" style={styles.nickname}>
|
||||
{nickname || currentUser?.nickname || ''}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.username}>
|
||||
@{currentUser?.username || ''}
|
||||
</Text>
|
||||
<View style={styles.nameSection}>
|
||||
<Text style={styles.nickname}>{nickname || currentUser?.nickname || ''}</Text>
|
||||
<Text style={styles.username}>@{currentUser?.username || ''}</Text>
|
||||
</View>
|
||||
|
||||
{bio ? (
|
||||
<Text variant="body" color={colors.text.secondary} style={styles.bio}>
|
||||
{bio}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="body" color={colors.text.hint} style={styles.bioPlaceholder}>
|
||||
这个人很懒,还没有写简介~
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 个人信息标签 */}
|
||||
<View style={styles.metaInfo}>
|
||||
{location ? (
|
||||
<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}>
|
||||
{location}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.metaTag}>
|
||||
<MaterialCommunityIcons name="map-marker-outline" size={12} color={colors.text.hint} />
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaTagText}>
|
||||
未设置地区
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{website ? (
|
||||
<View style={styles.metaTag}>
|
||||
<MaterialCommunityIcons name="link-variant" size={12} color={colors.info.main} />
|
||||
<Text variant="caption" color={colors.info.main} style={styles.metaTagText}>
|
||||
{typeof website === 'string' ? website.replace(/^https?:\/\//, '') : ''}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.metaTag}>
|
||||
<MaterialCommunityIcons name="link-variant" size={12} color={colors.text.hint} />
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaTagText}>
|
||||
未设置网站
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.metaTag}>
|
||||
<MaterialCommunityIcons name="calendar-outline" size={12} color={colors.text.secondary} />
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.metaTagText}>
|
||||
{`加入于 ${new Date(currentUser?.created_at || Date.now()).getFullYear()}年`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 编辑提示 */}
|
||||
<View style={styles.editHint}>
|
||||
<MaterialCommunityIcons name="information-outline" size={14} color={colors.text.hint} />
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.editHintText}>
|
||||
点击头像或头图可更换照片
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.editHint}>
|
||||
<MaterialCommunityIcons name="information-outline" size={12} color={colors.text.hint} />
|
||||
<Text style={styles.editHintText}>点击头像或头图可更换照片</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ===== 表单区域 ===== */}
|
||||
<View style={styles.formCard}>
|
||||
<Text variant="body" style={[styles.sectionTitle, { fontWeight: '600' }]}>
|
||||
个人资料
|
||||
</Text>
|
||||
|
||||
{/* ===== 表单区域 - Twitter 风格全宽分组 ===== */}
|
||||
<View style={styles.formSection}>
|
||||
<FormField
|
||||
icon="account-outline"
|
||||
label="昵称"
|
||||
value={nickname}
|
||||
onChangeText={setNickname}
|
||||
@@ -670,14 +513,11 @@ export const EditProfileScreen: React.FC = () => {
|
||||
colors={colors}
|
||||
/>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<FormField
|
||||
icon="information-outline"
|
||||
label="个人简介"
|
||||
value={bio}
|
||||
onChangeText={setBio}
|
||||
placeholder="介绍一下自己,让大家更了解你"
|
||||
placeholder="介绍一下自己"
|
||||
maxLength={100}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
@@ -685,10 +525,7 @@ export const EditProfileScreen: React.FC = () => {
|
||||
colors={colors}
|
||||
/>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<FormField
|
||||
icon="map-marker-outline"
|
||||
label="地区"
|
||||
value={location}
|
||||
onChangeText={setLocation}
|
||||
@@ -698,10 +535,7 @@ export const EditProfileScreen: React.FC = () => {
|
||||
colors={colors}
|
||||
/>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<FormField
|
||||
icon="link-variant"
|
||||
label="个人网站"
|
||||
value={website}
|
||||
onChangeText={setWebsite}
|
||||
@@ -711,17 +545,13 @@ export const EditProfileScreen: React.FC = () => {
|
||||
keyboardType="url"
|
||||
sheet={styles}
|
||||
colors={colors}
|
||||
isLast
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 联系方式区域 */}
|
||||
<View style={styles.formCard}>
|
||||
<Text variant="body" style={[styles.sectionTitle, { fontWeight: '600' }]}>
|
||||
联系方式
|
||||
</Text>
|
||||
|
||||
<View style={[styles.formSection, { marginTop: spacing.md }]}>
|
||||
<FormField
|
||||
icon="phone-outline"
|
||||
label="手机号"
|
||||
value={phone}
|
||||
onChangeText={setPhone}
|
||||
@@ -732,10 +562,7 @@ export const EditProfileScreen: React.FC = () => {
|
||||
colors={colors}
|
||||
/>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<FormField
|
||||
icon="email-outline"
|
||||
label="邮箱"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
@@ -745,6 +572,7 @@ export const EditProfileScreen: React.FC = () => {
|
||||
keyboardType="email-address"
|
||||
sheet={styles}
|
||||
colors={colors}
|
||||
isLast
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -759,26 +587,9 @@ export const EditProfileScreen: React.FC = () => {
|
||||
disabled={saving || uploadingAvatar || uploadingCover}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.saveButtonContent}>
|
||||
{saving ? (
|
||||
<MaterialCommunityIcons
|
||||
name="loading"
|
||||
size={20}
|
||||
color={colors.text.inverse}
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
) : (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
size={20}
|
||||
color={colors.text.inverse}
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
)}
|
||||
<Text variant="body" style={[styles.saveButtonText, { fontWeight: '600' }]}>
|
||||
{saving ? '保存中...' : '保存修改'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{saving ? '保存中...' : '保存修改'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
@@ -801,5 +612,4 @@ export const EditProfileScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default EditProfileScreen;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 设置页 SettingsScreen(响应式适配)
|
||||
* 设置页 SettingsScreen - Twitter/X 风格
|
||||
* 威友 - 应用设置
|
||||
* 在宽屏下居中显示,最大宽度限制
|
||||
* 采用扁平化设计,全宽布局,简洁分割线
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -31,8 +31,7 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
// 设置卡片最大宽度
|
||||
const SETTINGS_CARD_MAX_WIDTH = 720;
|
||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
||||
|
||||
interface SettingsItem {
|
||||
key: string;
|
||||
@@ -44,12 +43,9 @@ interface SettingsItem {
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
||||
|
||||
const SETTINGS_GROUPS_BASE = [
|
||||
{
|
||||
title: '账号与安全',
|
||||
icon: 'shield-check-outline',
|
||||
items: [
|
||||
{ key: 'edit_profile', title: '编辑资料', icon: 'account-edit-outline', showArrow: true },
|
||||
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
|
||||
@@ -61,7 +57,6 @@ const SETTINGS_GROUPS_BASE = [
|
||||
},
|
||||
{
|
||||
title: '通知与通用',
|
||||
icon: 'bell-outline',
|
||||
items: [
|
||||
{ key: 'notification_settings', title: '通知设置', icon: 'bell-cog-outline', showArrow: true, subtitle: '推送、震动、提示音' },
|
||||
{ key: 'data_storage', title: '数据与存储', icon: 'database-outline', showArrow: true },
|
||||
@@ -69,7 +64,6 @@ const SETTINGS_GROUPS_BASE = [
|
||||
},
|
||||
{
|
||||
title: '关于与帮助',
|
||||
icon: 'information-outline',
|
||||
items: [
|
||||
{ key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` },
|
||||
{ key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true },
|
||||
@@ -81,39 +75,38 @@ function createSettingsStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.paper,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingVertical: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
groupContainer: {
|
||||
marginBottom: spacing['2xl'],
|
||||
maxWidth: SETTINGS_CARD_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
marginBottom: spacing.xl,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
groupHeader: {
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
groupTitle: {
|
||||
fontWeight: '600',
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.secondary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: spacing['2xl'],
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: spacing.lg,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
settingItemFirst: {},
|
||||
settingItemLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
@@ -123,13 +116,17 @@ function createSettingsStyles(colors: AppColors) {
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.background.default,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
dangerIconContainer: {
|
||||
backgroundColor: colors.error.light + '20',
|
||||
},
|
||||
dangerIconContainer: {},
|
||||
settingContent: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -141,22 +138,21 @@ function createSettingsStyles(colors: AppColors) {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: spacing.lg,
|
||||
marginBottom: spacing['2xl'],
|
||||
paddingVertical: 16,
|
||||
marginHorizontal: spacing['2xl'],
|
||||
borderRadius: 14,
|
||||
marginBottom: spacing.xl,
|
||||
paddingVertical: 14,
|
||||
marginHorizontal: spacing.lg,
|
||||
borderRadius: borderRadius.full,
|
||||
gap: spacing.sm,
|
||||
maxWidth: SETTINGS_CARD_MAX_WIDTH,
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
backgroundColor: colors.error.light + '20',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
logoutText: {
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.xl,
|
||||
marginTop: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
copyright: {
|
||||
@@ -170,7 +166,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
const { logout } = useAuthStore();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { isMobile } = useResponsive();
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
|
||||
const themePreference = useThemePreference();
|
||||
@@ -184,7 +180,6 @@ export const SettingsScreen: React.FC = () => {
|
||||
return [
|
||||
{
|
||||
title: '个性化',
|
||||
icon: 'palette-outline',
|
||||
items: [
|
||||
{
|
||||
key: 'chat_settings',
|
||||
@@ -307,16 +302,16 @@ export const SettingsScreen: React.FC = () => {
|
||||
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
|
||||
<MaterialCommunityIcons
|
||||
name={item.icon as any}
|
||||
size={22}
|
||||
size={20}
|
||||
color={item.danger ? colors.error.main : colors.text.secondary}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settingContent}>
|
||||
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary}>
|
||||
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary} style={{ fontWeight: '500' }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.subtitle && (
|
||||
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.subtitle}>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
@@ -329,13 +324,13 @@ export const SettingsScreen: React.FC = () => {
|
||||
);
|
||||
|
||||
const renderGroup = (group: (typeof settingsGroups)[0]) => (
|
||||
<View key={group.title} style={styles.groupContainer}>
|
||||
<View key={group.title}>
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
{group.title}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<View style={styles.groupContainer}>
|
||||
{group.items.map((item, index) => renderSettingItem(item, index, group.items.length))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -345,9 +340,9 @@ export const SettingsScreen: React.FC = () => {
|
||||
<TouchableOpacity
|
||||
style={styles.logoutButton}
|
||||
onPress={() => handleItemPress('logout')}
|
||||
activeOpacity={0.7}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialCommunityIcons name="logout-variant" size={20} color={colors.error.main} />
|
||||
<MaterialCommunityIcons name="logout-variant" size={18} color={colors.error.main} />
|
||||
<Text variant="body" color={colors.error.main} style={styles.logoutText}>
|
||||
退出登录
|
||||
</Text>
|
||||
@@ -356,11 +351,8 @@ export const SettingsScreen: React.FC = () => {
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
{/* 设置分组 */}
|
||||
{settingsGroups.map((group) => renderGroup(group))}
|
||||
|
||||
{renderLogoutButton()}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text variant="caption" color={colors.text.hint}>
|
||||
威友 v{APP_VERSION}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 统一的用户主页组件
|
||||
* 威友 - 支持两种模式:self(当前用户)和 other(其他用户)
|
||||
* 采用现代卡片式设计,优化视觉层次和交互体验
|
||||
* 统一的用户主页组件 - Twitter/X 风格
|
||||
* 支持两种模式:self(当前用户)和 other(其他用户)
|
||||
* 采用 Twitter/X 扁平化设计,全宽布局,无边框卡片
|
||||
* 支持桌面端双栏布局
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
const colors = useAppColors();
|
||||
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
|
||||
const { isDesktop, isTablet } = useResponsive();
|
||||
|
||||
|
||||
const {
|
||||
user,
|
||||
posts,
|
||||
@@ -45,14 +45,15 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
handleFollowingPress,
|
||||
handleFollowersPress,
|
||||
handleMessage,
|
||||
handleMore,
|
||||
handleBlock,
|
||||
isBlocked,
|
||||
handleSettings,
|
||||
handleEditProfile,
|
||||
isCurrentUser,
|
||||
currentUser,
|
||||
} = useUserProfile({ mode, userId, isDesktop, isTablet });
|
||||
|
||||
// 渲染帖子列表
|
||||
|
||||
// 渲染帖子列表 - Twitter 风格无边框
|
||||
const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string) => {
|
||||
if (postList.length === 0) {
|
||||
return (
|
||||
@@ -64,7 +65,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View style={sharedStyles.postsContainer}>
|
||||
{postList.map((post, index) => {
|
||||
@@ -85,11 +86,11 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
</View>
|
||||
);
|
||||
}, [currentUser?.id, handlePostAction, activeTab]);
|
||||
|
||||
|
||||
// 渲染内容
|
||||
const renderContent = useCallback(() => {
|
||||
if (loading) return <Loading />;
|
||||
|
||||
|
||||
if (activeTab === 0) {
|
||||
return renderPostList(
|
||||
posts,
|
||||
@@ -97,7 +98,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
mode === 'self' ? '分享你的想法,发布第一条帖子吧' : ''
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (activeTab === 1) {
|
||||
return renderPostList(
|
||||
favorites,
|
||||
@@ -105,29 +106,30 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : ''
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}, [loading, activeTab, posts, favorites, mode, renderPostList]);
|
||||
|
||||
|
||||
// 渲染用户信息头部
|
||||
const renderUserHeader = useMemo(() => {
|
||||
if (!user) return null;
|
||||
|
||||
|
||||
return (
|
||||
<UserProfileHeader
|
||||
user={user}
|
||||
isCurrentUser={isCurrentUser}
|
||||
isBlocked={isBlocked}
|
||||
onFollow={handleFollow}
|
||||
onSettings={handleSettings}
|
||||
onEditProfile={handleEditProfile}
|
||||
onMessage={handleMessage}
|
||||
onMore={handleMore}
|
||||
onBlock={handleBlock}
|
||||
onFollowingPress={handleFollowingPress}
|
||||
onFollowersPress={handleFollowersPress}
|
||||
/>
|
||||
);
|
||||
}, [user, isCurrentUser, handleFollow, handleSettings, handleEditProfile, handleMessage, handleMore, handleFollowingPress, handleFollowersPress]);
|
||||
|
||||
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
|
||||
|
||||
// 渲染 TabBar 和内容
|
||||
const renderTabBarAndContent = useMemo(() => (
|
||||
<>
|
||||
@@ -145,7 +147,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
</View>
|
||||
</>
|
||||
), [activeTab, renderContent]);
|
||||
|
||||
|
||||
// 未登录/用户不存在状态
|
||||
if (mode === 'self' && !currentUser) {
|
||||
return (
|
||||
@@ -158,7 +160,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'other' && !user && !loading) {
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={['bottom']}>
|
||||
@@ -170,14 +172,14 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const safeAreaEdges = hasHeader ? ['bottom'] : (mode === 'self' ? ['top', 'bottom'] : ['bottom']);
|
||||
|
||||
|
||||
// 桌面端使用双栏布局
|
||||
if (isDesktop || isTablet) {
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
||||
<ResponsiveContainer maxWidth={1400}>
|
||||
<ResponsiveContainer maxWidth={1200}>
|
||||
<View style={sharedStyles.desktopContainer}>
|
||||
{/* 左侧:用户信息 */}
|
||||
<View style={sharedStyles.desktopSidebar}>
|
||||
@@ -196,7 +198,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
{renderUserHeader}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 右侧:帖子列表 */}
|
||||
<View style={sharedStyles.desktopContent}>
|
||||
<ScrollView
|
||||
@@ -211,7 +213,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 移动端使用单栏布局
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
||||
@@ -234,4 +236,4 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileScreen;
|
||||
export default UserProfileScreen;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { spacing, type AppColors } from '../../theme';
|
||||
import { spacing, borderRadius, type AppColors } from '../../theme';
|
||||
import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/auth';
|
||||
@@ -54,7 +54,7 @@ export interface UseUserProfileReturn {
|
||||
|
||||
// 仅 other 模式
|
||||
handleMessage?: () => Promise<void>;
|
||||
handleMore?: () => void;
|
||||
handleBlock?: () => Promise<void>;
|
||||
|
||||
// 仅 self 模式
|
||||
handleSettings?: () => void;
|
||||
@@ -356,53 +356,28 @@ case 'like':
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
// 更多操作(仅 other 模式)
|
||||
const handleMore = useCallback(() => {
|
||||
// 更多操作(仅 other 模式)- 由 UserProfileHeader 内部下拉菜单触发
|
||||
const handleBlock = useCallback(async () => {
|
||||
if (!user) return;
|
||||
|
||||
const { Alert } = require('react-native');
|
||||
Alert.alert(
|
||||
'更多操作',
|
||||
undefined,
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: isBlocked ? '取消拉黑' : '拉黑用户',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
Alert.alert(
|
||||
isBlocked ? '确认取消拉黑' : '确认拉黑',
|
||||
isBlocked
|
||||
? '取消拉黑后,对方可以重新与你建立关系。'
|
||||
: '拉黑后,对方将无法给你发送私聊消息,且会互相移除关注关系。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '确定',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const ok = isBlocked
|
||||
? await authService.unblockUser(user.id)
|
||||
: await authService.blockUser(user.id);
|
||||
if (!ok) {
|
||||
Alert.alert('失败', isBlocked ? '取消拉黑失败,请稍后重试' : '拉黑失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
setUser(prev =>
|
||||
prev
|
||||
? { ...prev, is_following: false, is_following_me: false }
|
||||
: prev
|
||||
);
|
||||
setIsBlocked(!isBlocked);
|
||||
Alert.alert('成功', isBlocked ? '已取消拉黑' : '已拉黑该用户');
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const ok = isBlocked
|
||||
? await authService.unblockUser(user.id)
|
||||
: await authService.blockUser(user.id);
|
||||
if (!ok) {
|
||||
const { showDialog } = require('../../services/ui');
|
||||
showDialog({
|
||||
title: '失败',
|
||||
message: isBlocked ? '取消拉黑失败,请稍后重试' : '拉黑失败,请稍后重试',
|
||||
actions: [{ text: '确定' }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setUser(prev =>
|
||||
prev
|
||||
? { ...prev, is_following: false, is_following_me: false }
|
||||
: prev
|
||||
);
|
||||
setIsBlocked(!isBlocked);
|
||||
}, [user, isBlocked]);
|
||||
|
||||
// 设置页跳转(仅 self 模式)
|
||||
@@ -444,7 +419,7 @@ case 'like':
|
||||
handleFollowersPress,
|
||||
handlePostAction,
|
||||
handleMessage: mode === 'other' ? handleMessage : undefined,
|
||||
handleMore: mode === 'other' ? handleMore : undefined,
|
||||
handleBlock: mode === 'other' ? handleBlock : undefined,
|
||||
handleSettings: mode === 'self' ? handleSettings : undefined,
|
||||
handleEditProfile: mode === 'self' ? handleEditProfile : undefined,
|
||||
isCurrentUser: mode === 'self',
|
||||
@@ -474,18 +449,21 @@ export function createSharedProfileStyles(colors: AppColors) {
|
||||
desktopContent: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
desktopScrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
tabBarContainer: {
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: 2,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
minHeight: 350,
|
||||
paddingTop: spacing.xs,
|
||||
paddingTop: 0,
|
||||
},
|
||||
postsContainer: {
|
||||
paddingHorizontal: spacing.md,
|
||||
|
||||
@@ -1156,7 +1156,7 @@ function createScheduleStyles(colors: AppColors) {
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
marginLeft: -8,
|
||||
},
|
||||
settingsButton: {
|
||||
width: 40,
|
||||
|
||||
Reference in New Issue
Block a user