From cb2087f77988b5e7c0f2bcbfb86ccf4d277be051 Mon Sep 17 00:00:00 2001 From: LostSalt Date: Mon, 16 Mar 2026 17:47:10 +0800 Subject: [PATCH 1/2] =?UTF-8?q?PC=E7=AB=AF=E7=9A=84=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 30 +- screenshots/after-fix.png | Bin 0 -> 126583 bytes screenshots/test-navigation-fix.png | Bin 0 -> 2877 bytes src/components/business/SearchBar.tsx | 27 +- src/components/business/SystemMessageItem.tsx | 178 +-- src/components/common/Input.tsx | 5 + src/navigation/MainNavigator.tsx | 58 +- src/navigation/MainNavigator.tsx.bak | 1126 +++++++++++++++++ .../MobileTabNavigatorWithDelay.tsx | 545 ++++++++ src/navigation/SimpleMobileTabNavigator.tsx | 282 +++++ src/screens/auth/LoginScreen.tsx | 451 +++++-- src/screens/auth/RegisterScreen.tsx | 472 +++++-- src/screens/home/HomeScreen.tsx | 50 +- src/screens/home/SearchScreen.tsx | 22 +- src/screens/message/ChatScreen.tsx | 65 +- src/screens/message/CreateGroupScreen.tsx | 4 + src/screens/message/JoinGroupScreen.tsx | 2 + src/screens/message/MessageListScreen.tsx | 79 +- src/screens/message/NotificationsScreen.tsx | 79 +- .../components/ChatScreen/ChatHeader.tsx | 46 +- .../components/ChatScreen/ChatInput.tsx | 3 + .../components/ChatScreen/GroupInfoPanel.tsx | 593 +++++++++ .../components/ChatScreen/SegmentRenderer.tsx | 47 +- .../message/components/ChatScreen/index.ts | 1 + .../message/components/ChatScreen/styles.ts | 21 +- .../message/components/ChatScreen/types.ts | 4 + .../message/components/EmbeddedChat.tsx | 428 ++++++- src/screens/profile/ProfileScreen.tsx | 9 +- src/theme/index.ts | 20 +- src/utils/inputStyles.ts | 33 + 30 files changed, 4242 insertions(+), 438 deletions(-) create mode 100644 screenshots/after-fix.png create mode 100644 screenshots/test-navigation-fix.png create mode 100644 src/navigation/MainNavigator.tsx.bak create mode 100644 src/navigation/MobileTabNavigatorWithDelay.tsx create mode 100644 src/navigation/SimpleMobileTabNavigator.tsx create mode 100644 src/screens/message/components/ChatScreen/GroupInfoPanel.tsx create mode 100644 src/utils/inputStyles.ts diff --git a/App.tsx b/App.tsx index de54b07..771c3c4 100644 --- a/App.tsx +++ b/App.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect, useRef } from 'react'; -import { AppState, AppStateStatus } from 'react-native'; +import { AppState, AppStateStatus, Platform, StyleSheet } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -42,6 +42,34 @@ const queryClient = new QueryClient({ installAlertOverride(); +// 注入全局CSS来移除React Native Web在浏览器中的默认focus outline +if (Platform.OS === 'web') { + const style = document.createElement('style'); + style.textContent = ` + /* 移除TextInput focus时的浏览器默认outline */ + input:focus, textarea:focus, select:focus { + outline: none !important; + outline-width: 0 !important; + box-shadow: none !important; + } + + /* 移除focus-visible时的默认outline */ + input:focus-visible, textarea:focus-visible { + outline: none !important; + } + + /* 移除所有:focus相关的默认样式 */ + *:focus { + outline: none !important; + } + + *:focus-visible { + outline: none !important; + } + `; + document.head.appendChild(style); +} + export default function App() { const appState = useRef(AppState.currentState); const notificationResponseListener = useRef(null); diff --git a/screenshots/after-fix.png b/screenshots/after-fix.png new file mode 100644 index 0000000000000000000000000000000000000000..2c352368355bb76dc996383facff166a30a7cb03 GIT binary patch literal 126583 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 literal 0 HcmV?d00001 diff --git a/screenshots/test-navigation-fix.png b/screenshots/test-navigation-fix.png new file mode 100644 index 0000000000000000000000000000000000000000..060a68050f33d4168d51b7bfe6d7a8745acf750c GIT binary patch literal 2877 zcmeAS@N?(olHy`uVBq!ia0y~yU~XYxV2a>i0*Y*1zQ2cof$Ni}i(^Q|oHy4Tc^M2i z7z{U^*`GS0aDJHU|95;03?la!7#v*O7#U8S&|qMYlw@U4P+rWykdh+8(9qMv#4urE zAOnMu5f_7l+o+<^AQ(*rqZwheEEp{gM{9)9N^!JZFxoU4jIBdUVB7cq|NX~qJOMJ; dK&@Y9hNrvOH%M!>yab9cc)I$ztaD0e0sy-%b@l)N literal 0 HcmV?d00001 diff --git a/src/components/business/SearchBar.tsx b/src/components/business/SearchBar.tsx index 040bd8c..c1bf4b4 100644 --- a/src/components/business/SearchBar.tsx +++ b/src/components/business/SearchBar.tsx @@ -59,6 +59,9 @@ const SearchBar: React.FC = ({ onFocus={handleFocus} onBlur={handleBlur} autoFocus={autoFocus} + // 确保光标可见 + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> {value.length > 0 && ( = ({ onRequestAction, requestActionLoading = false, }) => { + // 响应式适配 + const { isWideScreen, isDesktop } = useResponsive(); + const containerPadding = useResponsiveValue({ xs: spacing.md, lg: spacing.lg, xl: spacing.xl }); + const avatarSize = useResponsiveValue({ xs: 40, lg: 48, xl: 52 }); + const iconSize = useResponsiveValue({ xs: 20, lg: 22, xl: 24 }); + const contentFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); + const titleFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); + const timeFontSize = useResponsiveValue({ xs: 12, lg: 13, xl: 14 }); // 格式化时间 const formatTime = (dateString: string): string => { try { @@ -189,6 +198,89 @@ const SystemMessageItem: React.FC = ({ requestStatus === 'pending' && !!onRequestAction; + const styles = React.useMemo(() => StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-start', + padding: containerPadding, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + iconContainer: { + marginRight: isWideScreen ? spacing.lg : spacing.md, + }, + iconWrapper: { + width: avatarSize, + height: avatarSize, + borderRadius: avatarSize / 2, + justifyContent: 'center', + alignItems: 'center', + }, + content: { + flex: 1, + justifyContent: 'center', + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.xs, + }, + title: { + fontWeight: '600', + flex: 1, + marginRight: spacing.sm, + fontSize: titleFontSize, + }, + time: { + fontSize: timeFontSize, + }, + messageContent: { + lineHeight: isWideScreen ? 22 : 18, + fontSize: contentFontSize, + }, + extraInfo: { + flexDirection: 'row', + alignItems: 'center', + marginTop: spacing.xs, + backgroundColor: colors.background.default, + paddingHorizontal: isWideScreen ? spacing.md : spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.sm, + alignSelf: 'flex-start', + }, + extraText: { + marginLeft: spacing.xs, + flex: 1, + }, + actionsRow: { + flexDirection: 'row', + marginTop: spacing.sm, + }, + actionBtn: { + paddingVertical: isWideScreen ? spacing.sm : spacing.xs, + paddingHorizontal: isWideScreen ? spacing.lg : spacing.md, + borderRadius: borderRadius.sm, + borderWidth: 1, + marginRight: spacing.sm, + }, + rejectBtn: { + borderColor: colors.error.light, + backgroundColor: colors.error.light + '18', + }, + approveBtn: { + borderColor: colors.success.light, + backgroundColor: colors.success.light + '18', + }, + arrowContainer: { + marginLeft: spacing.sm, + justifyContent: 'center', + alignItems: 'center', + height: 20, + }, + }), [containerPadding, isWideScreen, avatarSize, titleFontSize, timeFontSize, contentFontSize]); + // 判断是否显示操作者头像 const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes( message.system_type @@ -207,19 +299,19 @@ const SystemMessageItem: React.FC = ({ {showGroupAvatar ? ( ) : showOperatorAvatar ? ( ) : ( - + )} @@ -231,7 +323,7 @@ const SystemMessageItem: React.FC = ({ {title} - + {formatTime(message.created_at)} @@ -288,82 +380,4 @@ const SystemMessageItem: React.FC = ({ ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'flex-start', - padding: spacing.lg, - backgroundColor: colors.background.paper, - borderBottomWidth: 1, - borderBottomColor: colors.divider, - }, - iconContainer: { - marginRight: spacing.md, - }, - iconWrapper: { - width: 44, - height: 44, - borderRadius: 22, - justifyContent: 'center', - alignItems: 'center', - }, - content: { - flex: 1, - justifyContent: 'center', - }, - titleRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: spacing.xs, - }, - title: { - fontWeight: '600', - flex: 1, - marginRight: spacing.sm, - }, - messageContent: { - lineHeight: 18, - }, - extraInfo: { - flexDirection: 'row', - alignItems: 'center', - marginTop: spacing.xs, - backgroundColor: colors.background.default, - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - borderRadius: borderRadius.sm, - alignSelf: 'flex-start', - }, - extraText: { - marginLeft: spacing.xs, - flex: 1, - }, - actionsRow: { - flexDirection: 'row', - marginTop: spacing.sm, - }, - actionBtn: { - paddingVertical: spacing.xs, - paddingHorizontal: spacing.md, - borderRadius: borderRadius.sm, - borderWidth: 1, - marginRight: spacing.sm, - }, - rejectBtn: { - borderColor: colors.error.light, - backgroundColor: colors.error.light + '18', - }, - approveBtn: { - borderColor: colors.success.light, - backgroundColor: colors.success.light + '18', - }, - arrowContainer: { - marginLeft: spacing.sm, - justifyContent: 'center', - alignItems: 'center', - height: 20, - }, -}); - export default SystemMessageItem; diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx index f677003..a4f958c 100644 --- a/src/components/common/Input.tsx +++ b/src/components/common/Input.tsx @@ -105,6 +105,9 @@ const Input: React.FC = ({ autoCapitalize={autoCapitalize} keyboardType={keyboardType} autoCorrect={autoCorrect} + // 确保光标可见 + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> {rightIcon && ( { + console.log('[Debug Sidebar]', { + width, + isCollapsed, + activeTab, + unreadCount, + insetsTop: insets.top, + insetsBottom: insets.bottom, + }); + }, [width, isCollapsed, activeTab, unreadCount, insets]); + const navItems: NavItem[] = [ { name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' }, { name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline', badge: unreadCount }, @@ -461,12 +478,13 @@ interface ResponsiveTabNavigatorProps { function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) { const navigation = useNavigation>(); const route = useRoute>(); - const { isMobile, isTablet, isDesktop } = useResponsive(); + const { isMobile, isTablet, isDesktop, width, height } = useResponsive(); const insets = useSafeAreaInsets(); const [activeTab, setActiveTab] = useState('HomeTab'); const [isCollapsed, setIsCollapsed] = useState(false); const messageUnreadCount = useTotalUnreadCount(); + // 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新 useEffect(() => { messageManager.initialize(); @@ -483,54 +501,62 @@ function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) { // 计算侧边栏宽度 const sidebarWidth = useCallback(() => { + if (isMobile) return 0; // 移动端不显示侧边栏 if (isCollapsed) return SIDEBAR_COLLAPSED_WIDTH; if (isDesktop) return SIDEBAR_WIDTH_DESKTOP; if (isTablet) return SIDEBAR_WIDTH_TABLET; - return 0; - }, [isCollapsed, isDesktop, isTablet]); + // 默认返回桌面宽度,避免返回 0 导致导航栏消失 + return SIDEBAR_WIDTH_DESKTOP; + }, [isCollapsed, isDesktop, isTablet, isMobile]); // 切换 Tab const handleTabChange = useCallback((tab: TabName) => { setActiveTab(tab); - if (Platform.OS === 'web') { + // 在大屏幕模式下,通过导航切换 Tab + if (!isMobile) { let tabTarget: NavigatorScreenParams; if (tab === 'HomeTab') { tabTarget = { screen: 'HomeTab', params: { screen: 'Home' } }; } else if (tab === 'MessageTab') { tabTarget = { screen: 'MessageTab', params: { screen: 'MessageList' } }; + } else if (tab === 'ScheduleTab') { + tabTarget = { screen: 'ScheduleTab', params: { screen: 'Schedule' } }; } else { tabTarget = { screen: 'ProfileTab', params: { screen: 'Profile' } }; } navigation.navigate('Main', tabTarget); } - }, [navigation]); + }, [navigation, isMobile]); // 切换折叠状态 const handleToggleCollapse = useCallback(() => { setIsCollapsed(prev => !prev); }, []); - // 渲染当前 Tab 的内容 + // 移动端:使用底部 Tab 导航 + if (isMobile) { + // 使用简化的移动端导航,避免 React Navigation 状态恢复问题 + return ; + } + + // 渲染当前 Tab 的内容(仅在大屏模式下使用) + // 注意:直接渲染屏幕组件,不使用 Stack Navigators + // 这样可以避免 React Navigation 状态恢复问题 const renderTabContent = () => { switch (activeTab) { case 'HomeTab': - return ; + return ; case 'MessageTab': - return ; + return ; case 'ScheduleTab': - return ; + return ; case 'ProfileTab': - return ; + return ; default: - return ; + return ; } }; - // 移动端:使用底部 Tab 导航 - if (isMobile) { - return ; - } - // 平板/桌面端:使用侧边导航 const currentSidebarWidth = sidebarWidth(); diff --git a/src/navigation/MainNavigator.tsx.bak b/src/navigation/MainNavigator.tsx.bak new file mode 100644 index 0000000..a60fced --- /dev/null +++ b/src/navigation/MainNavigator.tsx.bak @@ -0,0 +1,1126 @@ +/** + * 主导航组件 + * 配置底部Tab导航和Stack导航 + * 根据登录状态自动切换AuthStack和MainStack + * 支持响应式布局:移动端底部导航,平板/桌面端侧边导航 + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + StyleSheet, + View, + ActivityIndicator, + TouchableOpacity, + Animated, + ScrollView, + Text as RNText, + Platform, +} from 'react-native'; +import { + NavigationContainer, + LinkingOptions, + NavigatorScreenParams, + RouteProp, + useNavigation, + useRoute, +} from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useTheme } from 'react-native-paper'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context'; + +import { colors, shadows } from '../theme'; +import { useAuthStore, useTotalUnreadCount } from '../stores'; +import { messageManager } from '../stores'; +import { useResponsive } from '../hooks'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import type { + RootStackParamList, + MainTabParamList, + HomeStackParamList, + MessageStackParamList, + ScheduleStackParamList, + ProfileStackParamList, + AuthStackParamList, +} from './types'; + +// ==================== 导入屏幕组件 ==================== +import { HomeScreen, PostDetailScreen, SearchScreen } from '../screens/home'; +import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule'; +import { + MessageListScreen, + ChatScreen, + NotificationsScreen, + CreateGroupScreen, + JoinGroupScreen, + GroupRequestDetailScreen, + GroupInviteDetailScreen, + GroupInfoScreen, + GroupMembersScreen, + PrivateChatInfoScreen +} from '../screens/message'; +import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile'; +import { CreatePostScreen } from '../screens/create'; +import { UserScreen } from '../screens/profile'; +import FollowListScreen from '../screens/profile/FollowListScreen'; +import { LoginScreen, RegisterScreen, ForgotPasswordScreen } from '../screens/auth'; + +// ==================== Stack Navigators ==================== +const RootStack = createNativeStackNavigator(); +const AuthStack = createNativeStackNavigator(); +const HomeStack = createNativeStackNavigator(); +const MessageStack = createNativeStackNavigator(); +const ScheduleStack = createNativeStackNavigator(); +const ProfileStack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +// ==================== 导航栏宽度常量 ==================== +const SIDEBAR_WIDTH_DESKTOP = 240; +const SIDEBAR_WIDTH_TABLET = 200; +const SIDEBAR_COLLAPSED_WIDTH = 72; +const MOBILE_TAB_FLOATING_MARGIN = 12; + +// ==================== 持久化存储键 ==================== +const LAST_ACTIVE_TAB_KEY = 'last_active_tab'; + +// ==================== 导航项类型 ==================== +type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab'; + +type IconName = React.ComponentProps['name']; + +interface NavItem { + name: TabName; + label: string; + icon: IconName; + iconOutline: IconName; + badge?: number; +} + +// ==================== AuthStack导航 ==================== +function AuthStackNavigator() { + return ( + + + + + + ); +} + +// ==================== 首页Stack导航 ==================== +function HomeStackNavigator() { + const paperTheme = useTheme(); + + return ( + + + + + ); +} + +// ==================== 消息Stack导航 ==================== +function MessageStackNavigator() { + return ( + + + + + + ); +} + +// ==================== 个人中心Stack导航 ==================== +function ProfileStackNavigator() { + return ( + + + + + + + + + + + ); +} + +// ==================== 侧边导航栏组件 ==================== +interface SidebarProps { + activeTab: TabName; + onTabChange: (tab: TabName) => void; + unreadCount: number; + isCollapsed: boolean; + onToggleCollapse: () => void; + width: number; +} + +function Sidebar({ + activeTab, + onTabChange, + unreadCount, + isCollapsed, + onToggleCollapse, + width, +}: SidebarProps) { + const insets = useSafeAreaInsets(); + + // 调试日志 + useEffect(() => { + console.log('[Debug Sidebar]', { + width, + isCollapsed, + activeTab, + unreadCount, + insetsTop: insets.top, + insetsBottom: insets.bottom, + }); + }, [width, isCollapsed, activeTab, unreadCount, insets]); + + const navItems: NavItem[] = [ + { name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' }, + { name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline', badge: unreadCount }, + { name: 'ScheduleTab', label: '课表', icon: 'calendar-today', iconOutline: 'calendar-today' }, + { name: 'ProfileTab', label: '我的', icon: 'account', iconOutline: 'account-outline' }, + ]; + + return ( + + {/* Logo/品牌区域 */} + + {!isCollapsed && ( + + + + 胡萝卜BBS + + + )} + {isCollapsed && ( + + )} + + + {/* 导航项 */} + + {navItems.map((item) => { + const isActive = activeTab === item.name; + const showBadge = !!(item.badge && item.badge > 0); + + return ( + onTabChange(item.name)} + activeOpacity={0.7} + > + + + {showBadge && ( + + + {item.badge! > 99 ? '99+' : item.badge} + + + )} + + {!isCollapsed && ( + + {item.label} + + )} + + ); + })} + + + {/* 底部折叠按钮 */} + + + + + ); +} + +// ==================== Text 组件 ==================== +function Text({ style, children, numberOfLines }: { style?: any; children: React.ReactNode; numberOfLines?: number }) { + return ( + + {children} + + ); +} + +// ==================== 课程表Stack导航 ==================== +function ScheduleStackNavigator() { + return ( + + + + + ); +} + +// ==================== 响应式 Tab Navigator ==================== +interface ResponsiveTabNavigatorProps { + children?: React.ReactNode; +} + +function ResponsiveTabNavigator({ children }: ResponsiveTabNavigatorProps) { + const navigation = useNavigation>(); + const route = useRoute>(); + const { isMobile, isTablet, isDesktop, width, height } = useResponsive(); + const insets = useSafeAreaInsets(); + const [activeTab, setActiveTab] = useState('HomeTab'); + const [isCollapsed, setIsCollapsed] = useState(false); + const messageUnreadCount = useTotalUnreadCount(); + + // 调试日志:跟踪关键变量 + useEffect(() => { + console.log('[Debug ResponsiveTabNavigator]', { + width, + height, + isMobile, + isTablet, + isDesktop, + isCollapsed, + sidebarWidth: isCollapsed ? 72 : (isDesktop ? 240 : (isTablet ? 200 : 0)) + }); + }, [width, height, isMobile, isTablet, isDesktop, isCollapsed]); + + // 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新 + useEffect(() => { + messageManager.initialize(); + }, []); + + // 同步 URL 中的 Main 子路由到桌面侧边栏激活态 + useEffect(() => { + const targetTab = route.params?.screen; + if (!targetTab) return; + if ((targetTab === 'HomeTab' || targetTab === 'MessageTab' || targetTab === 'ScheduleTab' || targetTab === 'ProfileTab') && targetTab !== activeTab) { + setActiveTab(targetTab); + } + }, [route.params?.screen, activeTab]); + + // 计算侧边栏宽度 + const sidebarWidth = useCallback(() => { + if (isMobile) return 0; // 移动端不显示侧边栏 + if (isCollapsed) return SIDEBAR_COLLAPSED_WIDTH; + if (isDesktop) return SIDEBAR_WIDTH_DESKTOP; + if (isTablet) return SIDEBAR_WIDTH_TABLET; + // 默认返回桌面宽度,避免返回 0 导致导航栏消失 + return SIDEBAR_WIDTH_DESKTOP; + }, [isCollapsed, isDesktop, isTablet, isMobile]); + + // 切换 Tab + const handleTabChange = useCallback((tab: TabName) => { + setActiveTab(tab); + // 在大屏幕模式下,通过导航切换 Tab + if (!isMobile) { + let tabTarget: NavigatorScreenParams; + if (tab === 'HomeTab') { + tabTarget = { screen: 'HomeTab', params: { screen: 'Home' } }; + } else if (tab === 'MessageTab') { + tabTarget = { screen: 'MessageTab', params: { screen: 'MessageList' } }; + } else if (tab === 'ScheduleTab') { + tabTarget = { screen: 'ScheduleTab', params: { screen: 'Schedule' } }; + } else { + tabTarget = { screen: 'ProfileTab', params: { screen: 'Profile' } }; + } + navigation.navigate('Main', tabTarget); + } + }, [navigation, isMobile]); + + // 切换折叠状态 + const handleToggleCollapse = useCallback(() => { + setIsCollapsed(prev => !prev); + }, []); + + // 渲染当前 Tab 的内容 + const renderTabContent = () => { + switch (activeTab) { + case 'HomeTab': + return ; + case 'MessageTab': + return ; + case 'ScheduleTab': + return ; + case 'ProfileTab': + return ; + default: + return ; + } + }; + + // 移动端:使用底部 Tab 导航 + if (isMobile) { + return ; + } + + // 平板/桌面端:使用侧边导航 + const currentSidebarWidth = sidebarWidth(); + + return ( + + {/* 侧边导航栏 */} + + + {/* 主内容区域 */} + + {renderTabContent()} + + + ); +} + +// ==================== 底部Tab导航(移动端)==================== +function MainTabNavigator() { + const insets = useSafeAreaInsets(); + const messageUnreadCount = useTotalUnreadCount(); + + // 【新架构】MessageManager 自动处理 WebSocket 消息和未读数更新 + useEffect(() => { + messageManager.initialize(); + }, []); + + return ( + + ( + + + + ), + }} + /> + 0 ? (messageUnreadCount > 99 ? '99+' : messageUnreadCount) : undefined, + tabBarIcon: ({ color, size, focused }) => ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + + ); +} + +// ==================== 根导航 ==================== +export default function MainNavigator() { + const { isAuthenticated, fetchCurrentUser } = useAuthStore(); + const [initializing, setInitializing] = useState(true); + + const linking: LinkingOptions = { + prefixes: [], + config: { + screens: { + Auth: { + screens: { + Login: 'login', + Register: 'register', + ForgotPassword: 'forgot-password', + }, + }, + Main: { + screens: { + HomeTab: { + screens: { + Home: 'home', + Search: 'search', + }, + }, + MessageTab: { + screens: { + MessageList: 'messages', + Notifications: 'notifications', + }, + }, + ProfileTab: { + screens: { + Profile: 'me', + Settings: 'settings', + EditProfile: 'me/edit', + AccountSecurity: 'me/security', + NotificationSettings: 'me/notifications', + BlockedUsers: 'me/blocked', + }, + }, + }, + }, + PostDetail: 'posts/:postId', + UserProfile: 'users/:userId', + CreatePost: 'posts/create', + Chat: 'chat/:conversationId', + FollowList: 'users/:userId/:type', + }, + }, + }; + + // 初始化时检查登录状态 + useEffect(() => { + const initAuth = async () => { + await fetchCurrentUser(); + setInitializing(false); + }; + initAuth(); + }, [fetchCurrentUser]); + + // 加载中显示 + if (initializing) { + return ( + + + + ); + } + + return ( + + + {/* 根据登录状态显示不同Stack */} + {isAuthenticated ? ( + <> + + + + + + + + + + + + + + + ) : ( + <> + + {/* 未登录时也可以查看帖子详情 */} + + + + )} + + + ); +} + +// ==================== 样式 ==================== +const styles = StyleSheet.create({ + // 响应式容器 + responsiveContainer: { + flex: 1, + flexDirection: 'row', + backgroundColor: colors.background.default, + }, + + // 侧边栏样式 + sidebar: { + backgroundColor: colors.background.paper, + borderRightWidth: 1, + borderRightColor: colors.divider, + flexDirection: 'column', + ...shadows.md, + }, + sidebarHeader: { + paddingHorizontal: 16, + paddingVertical: 20, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + alignItems: 'center', + justifyContent: 'center', + }, + logoContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + logoText: { + fontSize: 20, + fontWeight: '700', + color: colors.primary.main, + marginLeft: 12, + }, + sidebarContent: { + flex: 1, + paddingTop: 8, + }, + sidebarItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + marginHorizontal: 8, + marginVertical: 4, + borderRadius: 12, + }, + sidebarItemActive: { + backgroundColor: `${colors.primary.main}15`, + }, + sidebarItemCollapsed: { + justifyContent: 'center', + paddingHorizontal: 0, + }, + sidebarIconContainer: { + position: 'relative', + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 12, + }, + sidebarLabel: { + fontSize: 15, + fontWeight: '500', + color: colors.text.secondary, + marginLeft: 12, + flex: 1, + }, + sidebarLabelActive: { + color: colors.primary.main, + fontWeight: '600', + }, + collapseButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + paddingHorizontal: 16, + paddingVertical: 12, + borderTopWidth: 1, + borderTopColor: colors.divider, + }, + collapseButtonCollapsed: { + justifyContent: 'center', + paddingHorizontal: 0, + }, + + // 徽章样式 + badge: { + position: 'absolute', + top: 2, + right: 2, + backgroundColor: colors.error.main, + borderRadius: 10, + minWidth: 18, + height: 18, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 4, + }, + badgeText: { + color: colors.primary.contrast, + fontSize: 10, + fontWeight: '600', + }, + + // 主内容区域 + mainContent: { + flex: 1, + backgroundColor: colors.background.default, + }, + + // 底部 Tab 样式 + tabIconContainer: { + alignItems: 'center', + justifyContent: 'center', + width: 38, + height: 30, + borderRadius: 15, + }, + tabIconActive: { + backgroundColor: `${colors.primary.main}20`, + transform: [{ translateY: -1 }], + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background.default, + }, +}); diff --git a/src/navigation/MobileTabNavigatorWithDelay.tsx b/src/navigation/MobileTabNavigatorWithDelay.tsx new file mode 100644 index 0000000..5eb1ac3 --- /dev/null +++ b/src/navigation/MobileTabNavigatorWithDelay.tsx @@ -0,0 +1,545 @@ +/** + * 移动端 Tab Navigator(完全独立版本) + * + * 这个组件完全独立于 MainNavigator.tsx 中的其他导航组件, + * 用于解决从大屏切换到小屏时的白屏问题。 + * + * 问题根源: + * - React Navigation 的 TabNavigator 在初始化时需要完整的 navigation state + * - 从大屏(Sidebar)切换到小屏(BottomTab)时,state 可能还没有准备好 + * - 导致 TabRouter 报错:"Cannot read properties of undefined (reading 'filter')" + * + * 解决方案: + * - 完全独立的组件,不依赖外部 navigation state + * - 使用延迟初始化,确保 React Navigation 内部状态就绪 + * - 错误边界和重试机制 + */ + +import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { + View, + ActivityIndicator, + StyleSheet, + AppState, + Platform, +} from 'react-native'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; + +import { colors, shadows } from '../theme'; +import { useTotalUnreadCount } from '../stores'; +import { messageManager } from '../stores'; + +// ==================== 导入屏幕组件 ==================== +import { HomeScreen, PostDetailScreen, SearchScreen } from '../screens/home'; +import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule'; +import { + MessageListScreen, + ChatScreen, + NotificationsScreen, + CreateGroupScreen, + JoinGroupScreen, + GroupRequestDetailScreen, + GroupInviteDetailScreen, + GroupInfoScreen, + GroupMembersScreen, + PrivateChatInfoScreen +} from '../screens/message'; +import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile'; +import { CreatePostScreen } from '../screens/create'; +import { UserScreen } from '../screens/profile'; +import FollowListScreen from '../screens/profile/FollowListScreen'; + +// ==================== 类型定义 ==================== +export type MainTabParamList = { + HomeTab: undefined; + MessageTab: undefined; + ScheduleTab: undefined; + ProfileTab: undefined; +}; + +export type HomeStackParamList = { + Home: undefined; + Search: undefined; +}; + +export type MessageStackParamList = { + MessageList: undefined; + Notifications: undefined; + PrivateChatInfo: { conversationId: string; userId: string }; +}; + +export type ScheduleStackParamList = { + Schedule: undefined; + CourseDetail: { courseId: string }; +}; + +export type ProfileStackParamList = { + Profile: undefined; + Settings: undefined; + EditProfile: undefined; + AccountSecurity: undefined; + MyPosts: undefined; + Bookmarks: undefined; + NotificationSettings: undefined; + BlockedUsers: undefined; +}; + +// ==================== Stack Navigators ==================== +const HomeStack = createNativeStackNavigator(); +const MessageStack = createNativeStackNavigator(); +const ScheduleStack = createNativeStackNavigator(); +const ProfileStack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +// ==================== 常量 ==================== +const MOBILE_TAB_FLOATING_MARGIN = 12; +const INITIALIZATION_DELAY = 500; // ms,增加延迟时间确保 React Navigation 完全初始化 +const MAX_RETRY_ATTEMPTS = 5; +const RETRY_DELAY = 500; // ms + +// ==================== Stack Navigator 组件 ==================== + +function HomeStackNavigatorComponent() { + return ( + + + + + ); +} + +function MessageStackNavigatorComponent() { + return ( + + + + + + ); +} + +function ScheduleStackNavigatorComponent() { + return ( + + + + + ); +} + +function ProfileStackNavigatorComponent() { + return ( + + + + + + + + + + + ); +} + +// ==================== 主组件:MobileTabNavigatorWithDelay ==================== + +export function MobileTabNavigatorWithDelay() { + const insets = useSafeAreaInsets(); + const messageUnreadCount = useTotalUnreadCount(); + const [isReady, setIsReady] = useState(false); + const [hasError, setHasError] = useState(false); + const initAttemptRef = useRef(0); + + // 初始化 MessageManager + useEffect(() => { + messageManager.initialize(); + }, []); + + // 延迟初始化 + useEffect(() => { + let timeoutId: NodeJS.Timeout; + let isMounted = true; + + const initializeNavigator = async () => { + try { + initAttemptRef.current += 1; + console.log( + `[MobileTabNavigator] Initialization attempt ${initAttemptRef.current}/${MAX_RETRY_ATTEMPTS}` + ); + + // 延迟初始化 + await new Promise((resolve) => { + timeoutId = setTimeout(resolve, INITIALIZATION_DELAY); + }); + + if (!isMounted) return; + + // 后台检测 + if (Platform.OS !== 'web' && AppState.currentState === 'background') { + console.log('[MobileTabNavigator] App in background, delaying...'); + await new Promise((resolve) => { + timeoutId = setTimeout(resolve, 300); + }); + } + + if (!isMounted) return; + + setIsReady(true); + setHasError(false); + console.log('[MobileTabNavigator] Initialization successful'); + } catch (error) { + console.error('[MobileTabNavigator] Initialization error:', error); + + if (!isMounted) return; + + if (initAttemptRef.current < MAX_RETRY_ATTEMPTS) { + console.log('[MobileTabNavigator] Retrying...'); + timeoutId = setTimeout(initializeNavigator, RETRY_DELAY); + } else { + setHasError(true); + } + } + }; + + initializeNavigator(); + + return () => { + isMounted = false; + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, []); + + // 渲染加载状态 + if (!isReady && !hasError) { + return ( + + + + ); + } + + // 渲染错误状态(带重试按钮) + if (hasError) { + return ( + + + + + ); + } + + // 渲染 Tab Navigator + return ( + + ( + + + + ), + }} + /> + 0 + ? messageUnreadCount > 99 + ? '99+' + : messageUnreadCount + : undefined, + tabBarIcon: ({ color, size, focused }) => ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + + ); +} + +// ==================== 样式 ==================== +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background.default, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.background.default, + padding: 20, + }, + retryIndicator: { + marginTop: 16, + }, + tabIconContainer: { + alignItems: 'center', + justifyContent: 'center', + width: 38, + height: 30, + borderRadius: 15, + }, + tabIconActive: { + backgroundColor: `${colors.primary.main}20`, + transform: [{ translateY: -1 }], + }, +}); diff --git a/src/navigation/SimpleMobileTabNavigator.tsx b/src/navigation/SimpleMobileTabNavigator.tsx new file mode 100644 index 0000000..4045aa6 --- /dev/null +++ b/src/navigation/SimpleMobileTabNavigator.tsx @@ -0,0 +1,282 @@ +/** + * 简单的移动端 Tab Navigator + * + * 不使用 React Navigation 的 Tab Navigator,而是使用一个简单的自定义实现 + * 这样可以完全避免 React Navigation 状态恢复的问题 + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + View, + StyleSheet, + TouchableOpacity, + Text, + Dimensions, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; + +import { colors, shadows } from '../theme'; +import { useTotalUnreadCount } from '../stores'; +import { messageManager } from '../stores'; + +// ==================== 导入屏幕组件 ==================== +import { HomeScreen, SearchScreen } from '../screens/home'; +import { ScheduleScreen, CourseDetailScreen } from '../screens/schedule'; +import { + MessageListScreen, + NotificationsScreen, + PrivateChatInfoScreen, +} from '../screens/message'; +import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsScreen, BlockedUsersScreen, AccountSecurityScreen } from '../screens/profile'; + +// ==================== 类型定义 ==================== +type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab'; + +// ==================== 常量 ==================== +const TAB_BAR_HEIGHT = 64; +const TAB_BAR_MARGIN = 14; +const TAB_BAR_FLOATING_MARGIN = 12; + +// ==================== 组件 ==================== + +/** + * 简化的 Stack Navigator 包装 + */ +function SimpleStackNavigator({ + children, + screenName +}: { + children: React.ReactNode; + screenName: string; +}) { + return ( + + {children} + + ); +} + +/** + * 主组件:SimpleMobileTabNavigator + */ +export function SimpleMobileTabNavigator() { + const insets = useSafeAreaInsets(); + const messageUnreadCount = useTotalUnreadCount(); + const [activeTab, setActiveTab] = useState('HomeTab'); + + // 初始化 MessageManager + useEffect(() => { + messageManager.initialize(); + }, []); + + // 处理 Tab 切换 + const handleTabPress = useCallback((tabName: TabName) => { + setActiveTab(tabName); + }, []); + + // 渲染当前 Tab 的内容 + const renderTabContent = () => { + switch (activeTab) { + case 'HomeTab': + return ; + case 'MessageTab': + return ; + case 'ScheduleTab': + return ; + case 'ProfileTab': + return ; + default: + return ; + } + }; + + // 渲染 Tab Bar 图标 + const renderTabIcon = (tabName: TabName, isActive: boolean) => { + const iconColor = isActive ? colors.primary.main : colors.text.secondary; + const iconSize = isActive ? 26 : 24; + + switch (tabName) { + case 'HomeTab': + return ( + + ); + case 'MessageTab': + return ( + + ); + case 'ScheduleTab': + return ( + + ); + case 'ProfileTab': + return ( + + ); + default: + return null; + } + }; + + // 渲染 Tab Bar 标签 + const renderTabLabel = (tabName: TabName, isActive: boolean) => { + const labels: Record = { + HomeTab: '首页', + MessageTab: '消息', + ScheduleTab: '课表', + ProfileTab: '我的', + }; + + return ( + + {labels[tabName]} + + ); + }; + + return ( + + {/* 内容区域 */} + + {renderTabContent()} + + + {/* Tab Bar */} + + {(['HomeTab', 'MessageTab', 'ScheduleTab', 'ProfileTab'] as TabName[]).map( + (tabName) => { + const isActive = activeTab === tabName; + const showBadge = tabName === 'MessageTab' && messageUnreadCount > 0; + + return ( + handleTabPress(tabName)} + activeOpacity={0.7} + > + + {renderTabIcon(tabName, isActive)} + {showBadge && ( + + + {messageUnreadCount > 99 ? '99+' : messageUnreadCount} + + + )} + + {renderTabLabel(tabName, isActive)} + + ); + } + )} + + + ); +} + +// ==================== 样式 ==================== +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + stackContainer: { + flex: 1, + }, + content: { + flex: 1, + marginBottom: TAB_BAR_HEIGHT + TAB_BAR_FLOATING_MARGIN * 2, + }, + tabBar: { + position: 'absolute', + left: TAB_BAR_MARGIN, + right: TAB_BAR_MARGIN, + height: TAB_BAR_HEIGHT, + backgroundColor: colors.background.paper, + borderRadius: 24, + borderTopWidth: 1, + borderTopColor: `${colors.divider}88`, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + paddingHorizontal: 8, + ...shadows.lg, + }, + tabItem: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 6, + borderRadius: 18, + }, + tabItemActive: { + backgroundColor: `${colors.primary.main}15`, + }, + tabIconContainer: { + position: 'relative', + width: 38, + height: 30, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 15, + }, + tabLabel: { + fontSize: 12, + fontWeight: '600', + marginTop: -2, + letterSpacing: 0.2, + color: colors.text.secondary, + }, + tabLabelActive: { + color: colors.primary.main, + }, + badge: { + position: 'absolute', + top: -2, + right: -2, + backgroundColor: colors.error.main, + borderRadius: 10, + minWidth: 18, + height: 18, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 4, + }, + badgeText: { + color: colors.primary.contrast, + fontSize: 10, + fontWeight: '600', + }, +}); diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index a5e0984..711bcfb 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -1,9 +1,10 @@ /** - * 登录页 LoginScreen(响应式适配) + * 登录页 LoginScreen(响应式适配 - 分栏布局) * 胡萝卜BBS - 用户登录 - * 现代化设计 - 渐变背景 + 动画效果 - * 登录表单在桌面端居中显示,限制最大宽度 - * 支持横屏模式下的布局调整 + * + * 布局规则: + * - 屏幕宽度 < 768px:单栏布局,橙色渐变背景 + * - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰 */ import React, { useState, useEffect, useRef } from 'react'; @@ -18,6 +19,7 @@ import { ScrollView, ActivityIndicator, Animated, + StatusBar, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; @@ -28,11 +30,13 @@ import { colors, spacing, borderRadius, shadows, fontSizes } from '../../theme'; import { useAuthStore } from '../../stores'; import { RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveValue } from '../../hooks'; -import { ResponsiveContainer } from '../../components/common'; import { showPrompt } from '../../services/promptService'; type LoginNavigationProp = NativeStackNavigationProp; +// 分栏布局断点 +const SPLIT_BREAKPOINT = 768; + export const LoginScreen: React.FC = () => { const navigation = useNavigation(); const login = useAuthStore((state) => state.login); @@ -40,7 +44,10 @@ export const LoginScreen: React.FC = () => { const setStoreError = useAuthStore((state) => state.setError); // 响应式布局 - const { isWideScreen, isLandscape, isDesktop, width } = useResponsive(); + const { isLandscape, width } = useResponsive(); + + // 是否使用分栏布局 + const isSplitLayout = width >= SPLIT_BREAKPOINT; const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -54,11 +61,23 @@ export const LoginScreen: React.FC = () => { const scaleAnim = useRef(new Animated.Value(0.9)).current; const inputFadeAnim = useRef(new Animated.Value(0)).current; - // 响应式值 - const logoSize = useResponsiveValue({ xs: 64, sm: 72, md: 80, lg: 96, xl: 110 }); - const logoContainerSize = useResponsiveValue({ xs: 110, sm: 120, md: 130, lg: 150, xl: 170 }); - const appNameSize = useResponsiveValue({ xs: 28, sm: 30, md: 32, lg: 36, xl: 40 }); - const formMaxWidth = isDesktop ? 480 : isWideScreen ? 420 : undefined; + // 响应式值 - 大屏模式单独放大Logo尺寸,更显大气 + const logoSize = useResponsiveValue({ + xs: 64, sm: 72, md: 80, + lg: isSplitLayout ? 140 : 96, // 大屏模式Logo图标进一步放大 + xl: isSplitLayout ? 160 : 110 + }); + const logoContainerSize = useResponsiveValue({ + xs: 110, sm: 120, md: 130, + lg: isSplitLayout ? 200 : 150, // 大屏模式Logo容器进一步放大 + xl: isSplitLayout ? 220 : 170 + }); + const appNameSize = useResponsiveValue({ + xs: 28, sm: 30, md: 32, + lg: isSplitLayout ? 60 : 36, // 大屏模式标题文字大幅放大 + xl: isSplitLayout ? 72 : 40 + }); + const formMaxWidth = isSplitLayout ? 420 : undefined; // 启动入场动画 useEffect(() => { @@ -105,7 +124,7 @@ export const LoginScreen: React.FC = () => { return true; }; - // store error 同步到本地,方便在输入时清除 + // store error 同步到本地 useEffect(() => { if (storeError) { setErrorMsg(storeError); @@ -126,8 +145,6 @@ export const LoginScreen: React.FC = () => { try { const success = await login({ username, password }); if (!success) { - // authStore 已将错误写入 storeError,useEffect 会同步到 errorMsg - // 若 storeError 为空则显示通用提示 if (!useAuthStore.getState().error) { setErrorMsg('登录失败,请稍后重试'); } @@ -144,39 +161,66 @@ export const LoginScreen: React.FC = () => { navigation.navigate('Register' as any); }; + // 渲染左侧面板(Logo和品牌信息)- 优化大屏布局 + const renderLeftPanel = () => ( + + {/* 胡萝卜图标 - 靠上显示 */} + + + + + {/* 品牌名称 - 优化大屏排版 */} + 胡萝卜 + 发现有趣的内容,结识志同道合的朋友 + + {/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */} + + {isSplitLayout ? ( + // 大屏模式:水平排列 + 大幅放大图标 + + + + + + ) : ( + // 小屏模式:保持原有横向排列 + + + + + + )} + + + ); + // 渲染表单内容 const renderFormContent = () => ( - <> - {/* Logo和标题区域 */} - - - - - 胡萝卜 - 发现有趣的内容,结识志同道合的朋友 - - - {/* 表单卡片 */} + { {/* 用户名输入框 */} - - + { {/* 密码输入框 */} - - + { {errorMsg} @@ -290,13 +322,10 @@ export const LoginScreen: React.FC = () => { activeOpacity={0.8} > {loading ? ( @@ -319,13 +348,48 @@ export const LoginScreen: React.FC = () => { 立即注册 - + ); - return ( - + // 渲染分栏布局 + const renderSplitLayout = () => ( + + {/* 左侧橙色面板 */} + {/* 装饰性背景元素 */} + + + + + + {renderLeftPanel()} + + + + {/* 右侧中性灰面板 */} + + + + {/* 核心修复:用View包裹,确保垂直+水平居中 */} + + {renderFormContent()} + + + + + ); + + // 渲染单栏布局 + const renderSingleLayout = () => ( + + + { behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardView} > - {isWideScreen ? ( - - - {/* 装饰性背景元素 */} - - - - {renderFormContent()} - - - ) : ( - + {/* 装饰性背景元素 */} + + + + {/* 头部 */} + - {/* 装饰性背景元素 */} - - - - {renderFormContent()} - - )} + + + + 胡萝卜 + 发现有趣的内容,结识志同道合的朋友 + + + {/* 表单 */} + {renderFormContent()} + ); + + return isSplitLayout ? renderSplitLayout() : renderSingleLayout(); }; const styles = StyleSheet.create({ @@ -383,11 +456,17 @@ const styles = StyleSheet.create({ keyboardView: { flex: 1, }, + // 单栏布局内容 scrollContent: { flexGrow: 1, paddingHorizontal: spacing.xl, paddingVertical: spacing['2xl'], }, + scrollContentCentered: { + alignItems: 'center', + justifyContent: 'center', + flex: 1, + }, scrollContentLandscape: { paddingVertical: spacing.lg, }, @@ -429,6 +508,15 @@ const styles = StyleSheet.create({ borderColor: 'rgba(255,255,255,0.4)', ...shadows.md, }, + // 大屏模式Logo容器样式 - 增加阴影和内边距,更立体 + splitLogoContainer: { + borderWidth: 3, + borderColor: 'rgba(255,255,255,0.5)', + ...shadows.lg, + }, + logoContainerTop: { + marginTop: spacing['3xl'], + }, appName: { fontWeight: '800', color: '#FFFFFF', @@ -437,20 +525,54 @@ const styles = StyleSheet.create({ textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, }, + // 大屏模式标题样式 - 大幅放大,增加间距和文字效果 + splitAppName: { + fontSize: 72, // 大幅放大主标题 + marginBottom: spacing['2xl'], + textShadowOffset: { width: 0, height: 4 }, + textShadowRadius: 8, + letterSpacing: 4, // 增加字间距,更显大气 + fontWeight: '900', + }, subtitle: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', textAlign: 'center', paddingHorizontal: spacing.xl, }, - // 表单卡片 + // 大屏模式副标题样式 - 大幅放大 + splitSubtitle: { + fontSize: 24, // 放大副标题 + marginBottom: spacing['6xl'], // 增加底部间距 + lineHeight: 36, + paddingHorizontal: spacing['4xl'], + letterSpacing: 1.5, + fontWeight: '500', + }, + // 表单卡片(基础样式,无阴影) formCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius['2xl'], padding: spacing['2xl'], marginBottom: spacing.xl, + }, + // 小屏模式:原有默认阴影 + normalFormCardShadow: { ...shadows.md, }, + // 大屏模式:专用全向阴影 + splitFormCardShadow: { + // iOS 全向柔和阴影 + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.12, + shadowRadius: 12, + // Android 高程 + 轻微边框(模拟顶部阴影) + elevation: 8, + // 新增:极细微的顶部边框,强化区分度 + borderTopWidth: 1, + borderTopColor: 'rgba(0,0,0,0.05)', + }, formTitle: { fontSize: fontSizes['2xl'], fontWeight: '700', @@ -471,9 +593,6 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, height: 56, }, - inputWrapperWide: { - height: 60, - }, inputIcon: { marginRight: spacing.sm, }, @@ -482,9 +601,6 @@ const styles = StyleSheet.create({ fontSize: fontSizes.md, color: colors.text.primary, }, - inputWide: { - fontSize: fontSizes.lg, - }, clearButton: { padding: spacing.xs, }, @@ -495,23 +611,23 @@ const styles = StyleSheet.create({ forgotPasswordText: { fontSize: fontSizes.sm, color: colors.primary.main, - fontWeight: '500', + fontWeight: '600', }, errorBox: { flexDirection: 'row', alignItems: 'flex-start', - backgroundColor: '#FFEBEE', + backgroundColor: colors.accent.light, borderRadius: borderRadius.md, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, marginBottom: spacing.md, borderLeftWidth: 3, - borderLeftColor: '#D32F2F', + borderLeftColor: colors.accent.main, }, errorText: { flex: 1, fontSize: fontSizes.sm, - color: '#D32F2F', + color: colors.accent.dark, lineHeight: 20, }, loginButton: { @@ -524,9 +640,6 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - loginButtonGradientWide: { - height: 56, - }, loginButtonDisabled: { opacity: 0.7, }, @@ -540,20 +653,108 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', - marginTop: 'auto', paddingTop: spacing.xl, }, footerText: { fontSize: fontSizes.md, - color: 'rgba(255,255,255,0.9)', + color: colors.text.primary, }, registerLink: { fontSize: fontSizes.md, - color: '#FFFFFF', + color: colors.primary.main, fontWeight: '700', marginLeft: spacing.xs, textDecorationLine: 'underline', }, + // ========== 分栏布局样式 ========== + splitContainer: { + flex: 1, + flexDirection: 'row', + }, + leftPanelContainer: { + flex: 1, // 50% + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + }, + leftPanelSafeArea: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: spacing['4xl'], // 增加内边距,不贴边 + }, + leftPanel: { + alignItems: 'center', + paddingHorizontal: spacing['2xl'], + }, + // 大屏模式左侧面板布局 + splitLeftPanel: { + width: '100%', + justifyContent: 'center', + paddingVertical: spacing['6xl'], + }, + leftPanelDecor: { + position: 'absolute', + bottom: spacing['4xl'], + left: 0, + right: 0, + alignItems: 'center', + }, + leftPanelDecorBottom: { + marginTop: spacing['2xl'], + }, + // 大屏模式装饰元素布局 + splitLeftPanelDecorBottom: { + marginTop: spacing['6xl'], + width: '100%', + alignItems: 'center', + }, + // 大屏模式装饰元素水平排列 + splitDecorRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + width: '100%', + gap: spacing['2xl'], // 增加图标间距 + }, + // 大屏模式装饰图标样式 + splitDecorIcon: { + transform: [{ scale: 1.1 }], + opacity: 0.9, + }, + decorRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + width: '80%', + }, + rightPanelContainer: { + flex: 1, // 50% + backgroundColor: colors.background.paper, + }, + rightPanelSafeArea: { + flex: 1, + // 移除左右内边距,交给子容器控制 + }, + // 分栏布局右侧内容包裹器(核心修复) + splitRightContentWrapper: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + // 新增:左右内边距,保证表单和边缘有间距 + paddingHorizontal: spacing['2xl'], + }, + // 表单内容包裹器(区分大屏/小屏) + splitFormWrapper: { + // 关键修改:设置最大宽度 + 自适应宽度,实现水平居中 + maxWidth: 420, + width: '100%', + }, + singleFormWrapper: { + flexGrow: 1, + width: '100%', + // 移除这里的阴影,避免重复 + }, }); -export default LoginScreen; +export default LoginScreen; \ No newline at end of file diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index 43820ef..12f9794 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -1,9 +1,10 @@ /** - * 注册页 RegisterScreen(响应式适配) + * 注册页 RegisterScreen(响应式适配 - 分栏布局) * 胡萝卜BBS - 用户注册 - * 现代化设计 - 渐变背景 + 动画效果 - * 注册表单在桌面端居中显示,限制最大宽度 - * 支持横屏模式下的布局调整 + * + * 布局规则: + * - 屏幕宽度 < 768px:单栏布局,橙色渐变背景 + * - 屏幕宽度 >= 768px:分栏布局,左侧橙色右侧中性灰 */ import React, { useState, useEffect, useRef } from 'react'; @@ -18,6 +19,7 @@ import { ScrollView, ActivityIndicator, Animated, + StatusBar, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; @@ -29,17 +31,22 @@ import { authService, resolveAuthApiError } from '../../services/authService'; import { useAuthStore } from '../../stores'; import { RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveValue } from '../../hooks'; -import { ResponsiveContainer } from '../../components/common'; import { showPrompt } from '../../services/promptService'; type RegisterNavigationProp = NativeStackNavigationProp; +// 分栏布局断点 +const SPLIT_BREAKPOINT = 768; + export const RegisterScreen: React.FC = () => { const navigation = useNavigation(); const register = useAuthStore((state) => state.register); // 响应式布局 - const { isWideScreen, isLandscape, isDesktop } = useResponsive(); + const { isLandscape, width } = useResponsive(); + + // 是否使用分栏布局 + const isSplitLayout = width >= SPLIT_BREAKPOINT; const [username, setUsername] = useState(''); const [nickname, setNickname] = useState(''); @@ -61,11 +68,23 @@ export const RegisterScreen: React.FC = () => { const scaleAnim = useRef(new Animated.Value(0.95)).current; const inputFadeAnim = useRef(new Animated.Value(0)).current; - // 响应式值 - const iconSize = useResponsiveValue({ xs: 48, sm: 52, md: 56, lg: 64, xl: 72 }); - const iconContainerSize = useResponsiveValue({ xs: 80, sm: 88, md: 96, lg: 108, xl: 120 }); - const titleSize = useResponsiveValue({ xs: 24, sm: 26, md: 28, lg: 30, xl: 32 }); - const formMaxWidth = isDesktop ? 520 : isWideScreen ? 480 : undefined; + // 响应式值 - 大屏模式单独放大图标尺寸,更显大气 + const iconSize = useResponsiveValue({ + xs: 48, sm: 52, md: 56, + lg: isSplitLayout ? 120 : 64, // 大屏模式图标进一步放大 + xl: isSplitLayout ? 140 : 72 + }); + const iconContainerSize = useResponsiveValue({ + xs: 80, sm: 88, md: 96, + lg: isSplitLayout ? 180 : 108, // 大屏模式图标容器进一步放大 + xl: isSplitLayout ? 200 : 120 + }); + const titleSize = useResponsiveValue({ + xs: 24, sm: 26, md: 28, + lg: isSplitLayout ? 60 : 30, // 大屏模式标题文字大幅放大 + xl: isSplitLayout ? 72 : 32 + }); + const formMaxWidth = isSplitLayout ? 480 : undefined; // 启动入场动画 useEffect(() => { @@ -219,64 +238,93 @@ export const RegisterScreen: React.FC = () => { // 跳转到登录页 const handleGoToLogin = () => { - navigation.goBack(); + navigation.navigate('Login' as any); }; + // 渲染左侧面板(Logo和品牌信息)- 优化大屏布局 + const renderLeftPanel = () => ( + + {/* 用户图标 - 靠上显示 */} + + + + + {/* 品牌名称 - 优化大屏排版 */} + 创建账号 + 加入胡萝卜,开始你的旅程 + + {/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */} + + {isSplitLayout ? ( + // 大屏模式:水平排列 + 大幅放大图标 + + + + + + ) : ( + // 小屏模式:保持原有横向排列 + + + + + + )} + + + ); + // 渲染表单内容 const renderFormContent = () => ( - <> - {/* 标题区域 */} - - - - - 创建账号 - 加入胡萝卜,开始你的旅程 - - - {/* 表单卡片 */} - + + 注册账号 + {/* 用户名输入框 */} - { - { - { {/* 邮箱验证码 */} - + { style={styles.inputIcon} /> { - { - { - { activeOpacity={0.8} > {loading ? ( @@ -577,24 +625,60 @@ export const RegisterScreen: React.FC = () => { {/* 底部登录提示 */} - - 已有账号? + 已有账号? - 立即登录 + 立即登录 - + ); - return ( - + // 渲染分栏布局 + const renderSplitLayout = () => ( + + {/* 左侧橙色面板 */} + {/* 装饰性背景元素 */} + + + + + + {renderLeftPanel()} + + + + {/* 右侧中性灰面板 */} + + + + {/* 核心修复:用View包裹,确保垂直+水平居中 */} + + {renderFormContent()} + + + + + ); + + // 渲染单栏布局 + const renderSingleLayout = () => ( + + + { behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardView} > - {isWideScreen ? ( - - - {/* 装饰性背景元素 */} - - - - {renderFormContent()} - - - ) : ( - + {/* 装饰性背景元素 */} + + + + {/* 头部 */} + - {/* 装饰性背景元素 */} - - - - {renderFormContent()} - - )} + + + + 创建账号 + 加入胡萝卜,开始你的旅程 + + + {/* 表单 */} + {renderFormContent()} + ); + + return isSplitLayout ? renderSplitLayout() : renderSingleLayout(); }; const styles = StyleSheet.create({ @@ -827,10 +920,16 @@ const styles = StyleSheet.create({ paddingTop: spacing.md, paddingBottom: spacing.md, }, + splitFooterSection: { + paddingTop: spacing.xl, + }, footerText: { fontSize: fontSizes.md, color: 'rgba(255,255,255,0.9)', }, + splitFooterText: { + color: colors.text.primary, + }, loginLink: { fontSize: fontSizes.md, color: '#FFFFFF', @@ -838,6 +937,141 @@ const styles = StyleSheet.create({ marginLeft: spacing.xs, textDecorationLine: 'underline', }, + splitLoginLink: { + color: colors.primary.main, + }, + // ========== 分栏布局样式 ========== + splitContainer: { + flex: 1, + flexDirection: 'row', + }, + leftPanelContainer: { + flex: 1, // 50% + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + }, + leftPanelSafeArea: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: spacing['4xl'], // 增加内边距,不贴边 + }, + leftPanel: { + alignItems: 'center', + paddingHorizontal: spacing['2xl'], + }, + // 大屏模式左侧面板布局 + splitLeftPanel: { + width: '100%', + justifyContent: 'center', + paddingVertical: 120, + }, + // 大屏模式图标容器样式 - 增加阴影和内边距,更立体 + splitIconContainer: { + borderWidth: 3, + borderColor: 'rgba(255,255,255,0.5)', + ...shadows.lg, + }, + iconContainerTop: { + marginTop: spacing['3xl'], + }, + // 大屏模式标题样式 - 大幅放大,增加间距和文字效果 + splitTitle: { + fontSize: 72, // 大幅放大主标题 + marginBottom: 48, + textShadowOffset: { width: 0, height: 4 }, + textShadowRadius: 8, + letterSpacing: 4, // 增加字间距,更显大气 + fontWeight: '900', + }, + // 大屏模式副标题样式 - 大幅放大 + splitSubtitle: { + fontSize: 24, // 放大副标题 + marginBottom: 120, // 增加底部间距 + lineHeight: 36, + paddingHorizontal: spacing['4xl'], + letterSpacing: 1.5, + fontWeight: '500', + }, + leftPanelDecorBottom: { + marginTop: spacing['2xl'], + }, + // 大屏模式装饰元素布局 + splitLeftPanelDecorBottom: { + marginTop: 120, + width: '100%', + alignItems: 'center', + }, + // 大屏模式装饰元素水平排列 + splitDecorRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + width: '100%', + gap: spacing['2xl'], // 增加图标间距 + }, + // 大屏模式装饰图标样式 + splitDecorIcon: { + transform: [{ scale: 1.1 }], + opacity: 0.9, + }, + decorRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + width: '80%', + }, + rightPanelContainer: { + flex: 1, // 50% + backgroundColor: colors.background.paper, + }, + rightPanelSafeArea: { + flex: 1, + // 移除左右内边距,交给子容器控制 + }, + // 分栏布局右侧内容包裹器(核心修复) + splitRightContentWrapper: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + // 新增:左右内边距,保证表单和边缘有间距 + paddingHorizontal: spacing['2xl'], + }, + // 表单内容包裹器(区分大屏/小屏) + splitFormWrapper: { + // 关键修改:设置最大宽度 + 自适应宽度,实现水平居中 + maxWidth: 480, + width: '100%', + }, + singleFormWrapper: { + flexGrow: 1, + width: '100%', + }, + // 小屏模式:原有默认阴影 + normalFormCardShadow: { + ...shadows.md, + }, + // 大屏模式:专用全向阴影 + splitFormCardShadow: { + // iOS 全向柔和阴影 + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.12, + shadowRadius: 12, + // Android 高程 + 轻微边框(模拟顶部阴影) + elevation: 8, + // 新增:极细微的顶部边框,强化区分度 + borderTopWidth: 1, + borderTopColor: 'rgba(0,0,0,0.05)', + }, + formTitle: { + fontSize: fontSizes['2xl'], + fontWeight: '700', + color: colors.text.primary, + marginBottom: spacing.xl, + textAlign: 'center', + }, }); export default RegisterScreen; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 4ea3d1a..f576dc1 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -32,6 +32,7 @@ import { PostCard, TabBar, SearchBar } from '../../components/business'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common'; import { HomeStackParamList, RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive'; +import { SearchScreen } from './SearchScreen'; type NavigationProp = NativeStackNavigationProp & NativeStackNavigationProp; @@ -84,6 +85,9 @@ export const HomeScreen: React.FC = () => { const [postImages, setPostImages] = useState([]); const [selectedImageIndex, setSelectedImageIndex] = useState(0); + // 搜索显示状态(用于内嵌搜索页面) + const [showSearch, setShowSearch] = useState(false); + // 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态 const postIdsRef = React.useRef>(new Set()); const inFlightRequestKeysRef = React.useRef>(new Set()); @@ -325,9 +329,9 @@ export const HomeScreen: React.FC = () => { }) ), [handleSwipeTabChange]); - // 跳转到搜索页 + // 跳转到搜索页(使用内嵌模式,不再依赖导航) const handleSearchPress = () => { - navigation.navigate('Search'); + setShowSearch(true); }; // 跳转到帖子详情 @@ -630,6 +634,19 @@ export const HomeScreen: React.FC = () => { ); }; + // 搜索页面内嵌模式 + if (showSearch) { + return ( + + + setShowSearch(false)} + /> + + ); + } + + // 正常首页内容 return ( @@ -667,7 +684,20 @@ export const HomeScreen: React.FC = () => { {/* 帖子列表 */} - + {isMobile ? ( + // 移动端:使用 GestureDetector 支持手势切换 Tab + + + {viewMode === 'list' ? ( + renderListContent() + ) : ( + // 网格模式:使用响应式网格 + renderResponsiveGrid() + )} + + + ) : ( + // 桌面端/平板端:不使用 GestureDetector,避免拦截侧边栏点击 {viewMode === 'list' ? ( renderListContent() @@ -676,7 +706,7 @@ export const HomeScreen: React.FC = () => { renderResponsiveGrid() )} - + )} {/* 漂浮发帖按钮 */} { {/* 图片查看器 */} ({ - id: img.id || img.url || String(Math.random()), - url: img.url || img.uri || '' + images={postImages.map(img => ({ + id: img.id || img.url || String(Math.random()), + url: img.url || img.uri || '' }))} initialIndex={selectedImageIndex} onClose={() => setShowImageViewer(false)} @@ -717,6 +747,12 @@ const styles = StyleSheet.create({ }, searchWrapper: { paddingBottom: spacing.sm, + // 移除阴影效果 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, }, viewToggleBtn: { width: 44, diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index aa10fba..3c815cc 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -31,8 +31,14 @@ const TABS = ['帖子', '用户']; type SearchType = 'posts' | 'users'; -export const SearchScreen: React.FC = () => { - const navigation = useNavigation(); +interface SearchScreenProps { + onBack?: () => void; + navigation?: NavigationProp; +} + +export const SearchScreen: React.FC = ({ onBack, navigation: propNavigation }) => { + // 如果传入了 navigation 则使用传入的,否则使用 hook 获取的 + const navigation = propNavigation || useNavigation(); const insets = useSafeAreaInsets(); const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore(); @@ -422,9 +428,9 @@ export const SearchScreen: React.FC = () => { autoFocus /> - navigation.goBack()} + onBack ? onBack() : navigation.goBack()} activeOpacity={0.85} > { - const navigation = useNavigation(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); // 响应式布局 const isWideScreen = useBreakpointGTE('lg'); const styles = baseStyles; + + // 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页 + useEffect(() => { + if (isWideScreen) { + // 导航到大屏幕模式的主界面首页 + navigation.reset({ + index: 0, + routes: [ + { + name: 'Main', + params: { + screen: 'HomeTab', + params: { screen: 'Home' } + } + } + ] + }); + } + }, [isWideScreen, navigation]); // 输入框区域高度(用于定位浮动 mention 面板) const [inputWrapperHeight, setInputWrapperHeight] = useState(60); @@ -93,6 +113,19 @@ export const ChatScreen: React.FC = () => { setShowImageViewer(false); }; + // 群信息面板状态 + const [showGroupInfoPanel, setShowGroupInfoPanel] = useState(false); + + // 打开群信息面板 + const handleGroupInfoPress = useCallback(() => { + setShowGroupInfoPanel(true); + }, []); + + // 关闭群信息面板 + const handleCloseGroupInfoPanel = useCallback(() => { + setShowGroupInfoPanel(false); + }, []); + const { // 状态 messages, @@ -125,6 +158,7 @@ export const ChatScreen: React.FC = () => { messageMap, loadingMore, hasMoreHistory, + conversationId, // Refs flatListRef, @@ -222,6 +256,8 @@ export const ChatScreen: React.FC = () => { onBack={() => navigation.goBack()} onTitlePress={navigateToInfo} onMorePress={navigateToChatSettings} + onGroupInfoPress={handleGroupInfoPress} + isWideScreen={isWideScreen} /> {/* 消息列表 */} @@ -369,6 +405,33 @@ export const ChatScreen: React.FC = () => { onClose={handleCloseImageViewer} enableSave /> + + {/* 群信息侧边栏面板 - 仅大屏幕显示 */} + { + // TODO: 实现置顶功能 + console.log('Toggle pin:', pinned); + }} + onLeaveGroup={() => { + // TODO: 实现退出群聊功能 + console.log('Leave group'); + }} + onInviteMembers={() => { + // TODO: 实现邀请新成员功能 + console.log('Invite members'); + }} + onViewAllMembers={() => { + // TODO: 实现查看全部成员功能 + console.log('View all members'); + }} + /> ); }; diff --git a/src/screens/message/CreateGroupScreen.tsx b/src/screens/message/CreateGroupScreen.tsx index c3c0ba4..2f35d03 100644 --- a/src/screens/message/CreateGroupScreen.tsx +++ b/src/screens/message/CreateGroupScreen.tsx @@ -215,6 +215,8 @@ const CreateGroupScreen: React.FC = () => { placeholder="请输入群名称" placeholderTextColor={colors.text.hint} maxLength={50} + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> {groupName.length}/50 @@ -238,6 +240,8 @@ const CreateGroupScreen: React.FC = () => { multiline numberOfLines={4} textAlignVertical="top" + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> {groupDescription.length}/500 diff --git a/src/screens/message/JoinGroupScreen.tsx b/src/screens/message/JoinGroupScreen.tsx index 209a85f..378b1f1 100644 --- a/src/screens/message/JoinGroupScreen.tsx +++ b/src/screens/message/JoinGroupScreen.tsx @@ -110,6 +110,8 @@ const JoinGroupScreen: React.FC = () => { style={styles.input} editable={!searching && !joining} autoCapitalize="none" + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> ; type MessageNavProp = NativeStackNavigationProp; @@ -140,7 +142,15 @@ export const MessageListScreen: React.FC = () => { const navigation = useNavigation(); const isFocused = useIsFocused(); const insets = useSafeAreaInsets(); - const tabBarHeight = useBottomTabBarHeight(); + // 在大屏幕模式下不使用 Bottom Tab Navigator,所以不需要 tabBarHeight + const { isMobile } = useResponsive(); + let tabBarHeight = 0; + try { + tabBarHeight = useBottomTabBarHeight(); + } catch (e) { + // 大屏幕模式下不在 Bottom Tab Navigator 中,会抛出错误,忽略即可 + tabBarHeight = 0; + } const currentUserId = useAuthStore(state => state.currentUser?.id); const setMessageUnreadCount = useUserStore(state => state.setMessageUnreadCount); @@ -173,6 +183,30 @@ export const MessageListScreen: React.FC = () => { const [activeSearchTab, setActiveSearchTab] = useState<'chat' | 'user'>('chat'); const [actionMenuVisible, setActionMenuVisible] = useState(false); + // 系统通知显示状态 - 用于在移动端显示通知页面 + const [showNotifications, setShowNotifications] = useState(false); + + // 系统消息会话对象(用于选中状态) + const systemMessageConversation: ConversationResponse = useMemo(() => ({ + id: SYSTEM_MESSAGE_CHANNEL_ID, + type: 'private', + last_seq: 0, + last_message: { + id: '0', + conversation_id: SYSTEM_MESSAGE_CHANNEL_ID, + sender_id: 'system', + seq: 0, + segments: [{ type: 'text', data: { text: '系统通知' } }], + status: 'normal', + created_at: new Date().toISOString(), + }, + last_message_at: new Date().toISOString(), + unread_count: systemUnreadCount, + participants: [], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }), [systemUnreadCount]); + // 动画值 const [scaleAnims] = useState(() => Array.from({ length: 20 }, () => new Animated.Value(1)) @@ -272,19 +306,19 @@ export const MessageListScreen: React.FC = () => { }), ]).start(() => { if (conversation.id === SYSTEM_MESSAGE_CHANNEL_ID) { - // 跳转到系统通知页面 - 使用正确的导航路径 + // 系统通知 - 根据屏幕宽度决定如何显示 if (isWideScreen) { - // 宽屏下直接导航到通知页面 - navigation.navigate('Notifications' as never); + // 宽屏下使用内部状态显示通知页面,同时设置系统消息为选中状态 + setSelectedConversation(systemMessageConversation); + setShowNotifications(true); } else { - // 窄屏下使用 Tab 导航 - (navigation as any).navigate('Main', { - screen: 'MessageTab', - params: { screen: 'Notifications' } - }); + // 窄屏下也使用内部状态显示通知页面 + setShowNotifications(true); } } else if (isWideScreen) { // 【桌面端双栏布局】宽屏下设置选中会话,在右侧显示聊天 + // 同时关闭系统通知页面 + setShowNotifications(false); setSelectedConversation(conversation); } else if (conversation.type === 'group' && conversation.group) { // 群聊 - 窄屏下导航 @@ -765,6 +799,9 @@ export const MessageListScreen: React.FC = () => { onChangeText={setSearchText} autoFocus returnKeyType="search" + // 确保光标可见 + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> {searchText.length > 0 && ( setSearchText('')}> @@ -903,7 +940,10 @@ export const MessageListScreen: React.FC = () => { {/* 右侧内容区域 - 使用 flex:1 填充剩余空间 */} - {selectedConversation ? ( + {showNotifications ? ( + // 显示系统通知页面 + setShowNotifications(false)} /> + ) : selectedConversation ? ( // 显示选中会话的聊天内容 { return ( - {isSearchMode ? ( + {showNotifications ? ( + // 显示系统通知页面,传入 onBack 回调 + setShowNotifications(false)} /> + ) : isSearchMode ? ( renderSearchMode() + ) : isWideScreen ? ( + + {renderConversationList()} + ) : ( - isWideScreen ? ( - - {renderConversationList()} - - ) : ( - renderConversationList() - ) + renderConversationList() )} {renderActionMenu()} @@ -1101,7 +1142,7 @@ const styles = StyleSheet.create({ ...shadows.sm, }, conversationItemSelected: { - backgroundColor: '#E3F2FD', + backgroundColor: colors.primary.light + '20', borderColor: colors.primary.main, borderWidth: 1, }, diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index 56cb6e9..c3ca10a 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -14,10 +14,11 @@ import { RefreshControl, ActivityIndicator, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing } from '../../theme'; import { SystemMessageResponse } from '../../types/dto'; import { messageService } from '../../services/messageService'; @@ -41,7 +42,7 @@ const MESSAGE_TYPES = [ const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']); const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']); -export const NotificationsScreen: React.FC = () => { +export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => { const isFocused = useIsFocused(); const fetchMessageUnreadCount = useUserStore(state => state.fetchMessageUnreadCount); const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); @@ -148,16 +149,12 @@ export const NotificationsScreen: React.FC = () => { } }, [isFocused, fetchMessages, fetchUnreadCount, handleMarkAllRead]); - // 屏幕失去焦点时返回消息列表 + // 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式) useEffect(() => { - if (!isFocused) { - // 使用 setTimeout 确保在导航状态稳定后再执行 - const timer = setTimeout(() => { - navigation.goBack(); - }, 0); - return () => clearTimeout(timer); + if (!isFocused && onBack) { + onBack(); } - }, [isFocused, navigation]); + }, [isFocused, onBack]); // 筛选消息 const filteredMessages = activeType === 'all' @@ -317,6 +314,32 @@ export const NotificationsScreen: React.FC = () => { ); }; + // 渲染头部(包含返回按钮) + const renderHeader = () => { + // 大屏模式(>= lg 断点)隐藏返回按钮 + const shouldShowBackButton = onBack && !isWideScreen; + + return ( + + {shouldShowBackButton ? ( + + + + ) : ( + + )} + + 系统通知 + + + + ); + }; + // 渲染空状态 const renderEmpty = () => ( { {isWideScreen ? ( + {/* 头部 - 大屏模式下隐藏返回按钮 */} + {renderHeader()} {/* 分类筛选 */} {MESSAGE_TYPES.map(type => { @@ -403,6 +428,8 @@ export const NotificationsScreen: React.FC = () => { ) : ( <> + {/* 头部 - 小屏模式下显示返回按钮 */} + {renderHeader()} {/* 分类筛选 */} {MESSAGE_TYPES.map(type => { @@ -516,6 +543,38 @@ const styles = StyleSheet.create({ filterCount: { marginLeft: spacing.xs, }, + // Header 样式 + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + backgroundColor: colors.background.paper, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + headerWide: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.lg, + }, + backButton: { + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'flex-start', + }, + backButtonPlaceholder: { + width: 40, + }, + headerTitle: { + fontSize: 18, + fontWeight: '600', + color: colors.text.primary, + }, + headerTitleWide: { + fontSize: 20, + }, listContent: { flexGrow: 1, }, diff --git a/src/screens/message/components/ChatScreen/ChatHeader.tsx b/src/screens/message/components/ChatScreen/ChatHeader.tsx index 73f0aeb..437a6d1 100644 --- a/src/screens/message/components/ChatScreen/ChatHeader.tsx +++ b/src/screens/message/components/ChatScreen/ChatHeader.tsx @@ -22,10 +22,15 @@ export const ChatHeader: React.FC = ({ onBack, onTitlePress, onMorePress, + onGroupInfoPress, + isWideScreen: propIsWideScreen, }) => { // 响应式布局 const { width } = useResponsive(); - const isWideScreen = useBreakpointGTE('lg'); + // 使用 768px 作为大屏幕和小屏幕的分界线 + const isWideScreenHook = useBreakpointGTE('lg'); + // 优先使用 props 传入的 isWideScreen,否则使用 hook + const isWideScreen = propIsWideScreen !== undefined ? propIsWideScreen : isWideScreenHook; // 合并样式 const styles = useMemo(() => { @@ -67,12 +72,17 @@ export const ChatHeader: React.FC = ({ return ( - - - + {/* 大屏幕(>= 768px)时隐藏返回按钮 */} + {!isWideScreen ? ( + + + + ) : ( + + )} = ({ - - - + {/* 大屏幕 + 群聊时显示群信息按钮,否则显示三点菜单 */} + {isWideScreen && isGroupChat && onGroupInfoPress ? ( + + + + ) : ( + + + + )} ); diff --git a/src/screens/message/components/ChatScreen/ChatInput.tsx b/src/screens/message/components/ChatScreen/ChatInput.tsx index 8676b1c..79858c8 100644 --- a/src/screens/message/components/ChatScreen/ChatInput.tsx +++ b/src/screens/message/components/ChatScreen/ChatInput.tsx @@ -134,6 +134,9 @@ export const ChatInput: React.FC diff --git a/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx new file mode 100644 index 0000000..c89ff3b --- /dev/null +++ b/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx @@ -0,0 +1,593 @@ +/** + * GroupInfoPanel.tsx + * 群信息侧边栏组件 + * 大屏幕模式下从右侧滑入显示 + * 实现手机端群信息界面的核心功能 + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + View, + StyleSheet, + TouchableOpacity, + ScrollView, + Dimensions, + Animated, + Alert, +} from 'react-native'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { Avatar, Text } from '../../../../components/common'; +import { colors, spacing, fontSizes, shadows } from '../../../../theme'; +import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; +import { useBreakpointGTE } from '../../../../hooks/useResponsive'; +import { groupManager } from '../../../../stores/groupManager'; +import { groupService } from '../../../../services/groupService'; + +const { width: screenWidth } = Dimensions.get('window'); +const PANEL_WIDTH = 360; + +interface GroupInfoPanelProps { + visible: boolean; + groupId?: string; + groupInfo: GroupResponse | null; + conversationId?: string; + isPinned?: boolean; + currentUserId?: string; + onClose: () => void; + onTogglePin?: (pinned: boolean) => void; + onLeaveGroup?: () => void; + onInviteMembers?: () => void; + onViewAllMembers?: () => void; +} + +export const GroupInfoPanel: React.FC = ({ + visible, + groupId, + groupInfo, + conversationId, + isPinned, + currentUserId, + onClose, + onTogglePin, + onLeaveGroup, + onInviteMembers, + onViewAllMembers, +}) => { + const isWideScreen = useBreakpointGTE('lg'); + const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current; + const fadeAnim = React.useRef(new Animated.Value(0)).current; + const [members, setMembers] = useState([]); + const [announcements, setAnnouncements] = useState([]); + const [loading, setLoading] = useState(false); + const [showAllMembers, setShowAllMembers] = useState(false); + const [allMembers, setAllMembers] = useState([]); + const [loadingMoreMembers, setLoadingMoreMembers] = useState(false); + + // 加载群成员(初始只加载10个) + const loadMembers = useCallback(async () => { + if (!groupId) return; + try { + setLoading(true); + const response = await groupManager.getMembers(groupId, 1, 10); + setMembers(response.list || []); + } catch (error) { + console.error('[GroupInfoPanel] 加载成员失败:', error); + } finally { + setLoading(false); + } + }, [groupId]); + + // 加载全部群成员 + const loadAllMembers = useCallback(async () => { + if (!groupId) return; + try { + setLoadingMoreMembers(true); + const response = await groupManager.getMembers(groupId, 1, 100); + setAllMembers(response.list || []); + setShowAllMembers(true); + } catch (error) { + console.error('[GroupInfoPanel] 加载全部成员失败:', error); + } finally { + setLoadingMoreMembers(false); + } + }, [groupId]); + + // 加载群公告 + const loadAnnouncements = useCallback(async () => { + if (!groupId) return; + try { + const response = await groupService.getAnnouncements(groupId, 1, 10); + setAnnouncements(response.list || []); + } catch (error) { + console.error('[GroupInfoPanel] 加载公告失败:', error); + } + }, [groupId]); + + // 每次打开面板时重置状态 + useEffect(() => { + if (visible) { + setShowAllMembers(false); + setAllMembers([]); + } + }, [visible]); + + useEffect(() => { + if (visible && groupId) { + loadMembers(); + loadAnnouncements(); + } + }, [visible, groupId, loadMembers, loadAnnouncements]); + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: PANEL_WIDTH, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, slideAnim, fadeAnim]); + + if (!isWideScreen) { + return null; + } + + // 获取加群方式文本 + const getJoinTypeText = (joinType?: number): string => { + switch (joinType) { + case 0: + return '允许任何人加入'; + case 1: + return '需要管理员审批'; + case 2: + return '不允许任何人加入'; + default: + return '未知'; + } + }; + + return ( + <> + {/* 遮罩层 */} + {visible && ( + + + + )} + + {/* 侧边栏面板 */} + + {/* 头部 */} + + 群信息 + + + + + + + {/* 群头像和名称 */} + + + {groupInfo?.name || '群聊'} + + {groupInfo?.member_count || members.length || 0} 位成员 + + {groupInfo?.join_type !== undefined && ( + + {getJoinTypeText(groupInfo.join_type)} + + )} + {/* 邀请新成员按钮 */} + {onInviteMembers && ( + + + 邀请新成员 + + )} + + + {/* 分割线 */} + + + {/* 群公告 */} + {announcements.length > 0 && ( + + + + 群公告 + + + {announcements[0].content} + + {new Date(announcements[0].created_at).toLocaleDateString()} + + + + )} + + {/* 群简介 */} + {groupInfo?.description && ( + + 群简介 + + {groupInfo.description} + + + )} + + {/* 群成员列表 */} + + + 成员列表 ({groupInfo?.member_count || members.length}) + {onViewAllMembers && !showAllMembers && members.length >= 10 && ( + + + {loadingMoreMembers ? '加载中...' : '查看全部'} + + + + )} + {onViewAllMembers && showAllMembers && ( + setShowAllMembers(false)} style={styles.viewAllButton}> + 收起 + + + )} + + + {(showAllMembers ? allMembers : members).map((member) => ( + + + + + {member.nickname || member.user?.nickname || '用户'} + + {member.role === 'owner' && ( + 群主 + )} + {member.role === 'admin' && ( + 管理员 + )} + + + ))} + + + + {/* 群信息 */} + + 群信息 + + 群号 + {groupInfo?.id || '-'} + + + 创建时间 + + {groupInfo?.created_at + ? new Date(groupInfo.created_at).toLocaleDateString('zh-CN') + : '-'} + + + + 全员禁言 + + {groupInfo?.mute_all ? '已开启' : '已关闭'} + + + + + {/* 操作按钮 */} + + {conversationId && onTogglePin && ( + onTogglePin(!isPinned)} + activeOpacity={0.7} + > + + + {isPinned ? '取消置顶' : '置顶群聊'} + + + )} + + {onLeaveGroup && ( + + + + 退出群聊 + + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.3)', + zIndex: 100, + }, + overlayTouchable: { + flex: 1, + }, + panel: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + width: PANEL_WIDTH, + backgroundColor: '#FFF', + zIndex: 101, + ...shadows.lg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: '#E8E8E8', + }, + headerTitle: { + fontSize: fontSizes.xl, + fontWeight: '600', + color: '#333', + }, + closeButton: { + padding: spacing.xs, + }, + content: { + flex: 1, + }, + groupInfoSection: { + alignItems: 'center', + paddingVertical: spacing.xl, + }, + groupName: { + fontSize: fontSizes.xl, + fontWeight: '600', + color: '#333', + marginTop: spacing.md, + }, + memberCount: { + fontSize: fontSizes.md, + color: '#999', + marginTop: spacing.xs, + }, + joinType: { + fontSize: fontSizes.sm, + color: colors.primary.main, + marginTop: spacing.xs, + }, + inviteButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + backgroundColor: colors.primary.light + '15', + borderRadius: 20, + marginTop: spacing.md, + borderWidth: 1, + borderColor: colors.primary.light, + }, + inviteButtonText: { + fontSize: fontSizes.sm, + color: colors.primary.main, + marginLeft: spacing.xs, + fontWeight: '500', + }, + divider: { + height: 1, + backgroundColor: '#E8E8E8', + marginHorizontal: spacing.md, + }, + section: { + padding: spacing.md, + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: spacing.sm, + }, + sectionTitle: { + fontSize: fontSizes.md, + fontWeight: '600', + color: '#333', + marginLeft: spacing.xs, + }, + noticeBox: { + backgroundColor: '#FFF9E6', + padding: spacing.md, + borderRadius: 8, + borderLeftWidth: 3, + borderLeftColor: colors.warning.main, + }, + noticeText: { + fontSize: fontSizes.sm, + color: '#666', + lineHeight: 20, + }, + noticeTime: { + fontSize: fontSizes.xs, + color: '#999', + marginTop: spacing.xs, + textAlign: 'right', + }, + descriptionBox: { + backgroundColor: '#F5F7FA', + padding: spacing.md, + borderRadius: 8, + }, + description: { + fontSize: fontSizes.sm, + color: '#666', + lineHeight: 20, + }, + memberList: { + gap: spacing.sm, + }, + memberListHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.sm, + }, + viewAllButton: { + flexDirection: 'row', + alignItems: 'center', + }, + viewAllText: { + fontSize: fontSizes.sm, + color: colors.primary.main, + }, + memberItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.xs, + }, + memberInfo: { + flex: 1, + marginLeft: spacing.sm, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + memberName: { + fontSize: fontSizes.md, + color: '#333', + flex: 1, + }, + ownerBadge: { + fontSize: fontSizes.xs, + color: colors.warning.main, + marginLeft: spacing.xs, + backgroundColor: colors.warning.light + '30', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + adminBadge: { + fontSize: fontSizes.xs, + color: colors.primary.main, + marginLeft: spacing.xs, + backgroundColor: colors.primary.light + '30', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + moreMembers: { + fontSize: fontSizes.sm, + color: '#999', + textAlign: 'center', + paddingVertical: spacing.sm, + }, + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: '#F0F0F0', + }, + infoLabel: { + fontSize: fontSizes.sm, + color: '#999', + }, + infoValue: { + fontSize: fontSizes.sm, + color: '#333', + }, + actionSection: { + padding: spacing.md, + paddingBottom: spacing.xl, + }, + actionButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + backgroundColor: colors.primary.light + '15', + borderRadius: 12, + marginBottom: spacing.sm, + borderWidth: 1, + borderColor: colors.primary.light, + }, + actionButtonText: { + fontSize: fontSizes.md, + fontWeight: '600', + color: colors.primary.main, + marginLeft: spacing.sm, + }, + leaveButton: { + backgroundColor: colors.error.light + '15', + borderColor: colors.error.light, + }, + leaveButtonText: { + color: colors.error.main, + }, +}); + +export default GroupInfoPanel; diff --git a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx index 6657952..2055831 100644 --- a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx +++ b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx @@ -130,9 +130,22 @@ const ImageSegment: React.FC<{ }> = ({ data, isMe, onImagePress, onImageLongPress }) => { const pressPositionRef = useRef({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null); + const [loadError, setLoadError] = useState(false); const imageUrl = data.thumbnail_url || data.url; + // 如果没有有效的图片URL,显示图片占位符 + const hasValidUrl = !!imageUrl && imageUrl.trim() !== ''; + useEffect(() => { + // 重置状态 + setLoadError(false); + + // 如果没有有效的图片URL,设置默认尺寸用于显示占位符 + if (!hasValidUrl) { + setDimensions(IMAGE_FALLBACK_SIZE); + return; + } + // 如果已经有宽高信息,直接使用 if (data.width && data.height) { const aspectRatio = data.width / data.height; @@ -191,7 +204,7 @@ const ImageSegment: React.FC<{ ); return () => clearTimeout(timeoutId); - }, [imageUrl, data.width, data.height]); + }, [imageUrl, data.width, data.height, hasValidUrl]); // 记录按压位置 const handlePressIn = (event: GestureResponderEvent) => { @@ -204,6 +217,35 @@ const ImageSegment: React.FC<{ onImageLongPress?.(pressPositionRef.current); }; + // 没有有效URL或加载失败时显示占位符 + if (!hasValidUrl || loadError) { + return ( + hasValidUrl && onImagePress?.(data.url)} + onLongPress={handleLongPress} + delayLongPress={500} + activeOpacity={0.9} + > + + + 图片 + + + ); + } + // 初始加载时显示占位 if (!dimensions) { return ( @@ -248,6 +290,9 @@ const ImageSegment: React.FC<{ contentFit="cover" cachePolicy="disk" priority="normal" + onError={() => { + setLoadError(true); + }} /> ); diff --git a/src/screens/message/components/ChatScreen/index.ts b/src/screens/message/components/ChatScreen/index.ts index 44cd908..c905b0f 100644 --- a/src/screens/message/components/ChatScreen/index.ts +++ b/src/screens/message/components/ChatScreen/index.ts @@ -19,6 +19,7 @@ export { LongPressMenu } from './LongPressMenu'; export { ChatHeader } from './ChatHeader'; export { MessageBubble } from './MessageBubble'; export { ChatInput } from './ChatInput'; +export { GroupInfoPanel } from './GroupInfoPanel'; // Segment 渲染组件 export { diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index 3eaf6cb..1a2bd32 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -322,13 +322,18 @@ export const chatScreenStyles = StyleSheet.create({ inputContainer: { backgroundColor: colors.background.paper, borderWidth: 1, - borderColor: `${colors.divider}88`, + borderColor: colors.divider, borderRadius: 24, marginHorizontal: 14, marginBottom: 12, paddingHorizontal: spacing.sm, paddingVertical: spacing.sm, - ...shadows.lg, + // 移除明显的阴影效果,改用更微妙的边框 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, }, inputInner: { flexDirection: 'row', @@ -338,6 +343,12 @@ export const chatScreenStyles = StyleSheet.create({ paddingHorizontal: spacing.xs, paddingVertical: 4, minHeight: 44, + // 移除阴影效果 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, }, inputInnerMuted: { backgroundColor: '#F0F0F0', @@ -395,6 +406,12 @@ export const chatScreenStyles = StyleSheet.create({ paddingHorizontal: spacing.xs, maxHeight: 100, justifyContent: 'center', + // 移除阴影 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, }, input: { fontSize: 16, diff --git a/src/screens/message/components/ChatScreen/types.ts b/src/screens/message/components/ChatScreen/types.ts index 794651d..ea704fa 100644 --- a/src/screens/message/components/ChatScreen/types.ts +++ b/src/screens/message/components/ChatScreen/types.ts @@ -163,6 +163,10 @@ export interface ChatHeaderProps { onBack: () => void; onTitlePress: () => void; onMorePress: () => void; + /** 大屏幕下显示群信息面板的回调 */ + onGroupInfoPress?: () => void; + /** 是否为大屏幕模式 */ + isWideScreen?: boolean; } // 回复预览 Props diff --git a/src/screens/message/components/EmbeddedChat.tsx b/src/screens/message/components/EmbeddedChat.tsx index 9e610b4..8f7eecf 100644 --- a/src/screens/message/components/EmbeddedChat.tsx +++ b/src/screens/message/components/EmbeddedChat.tsx @@ -14,26 +14,40 @@ import { Platform, TextInput, Dimensions, + Animated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { Image as ExpoImage } from 'expo-image'; import { colors, spacing, fontSizes, shadows } from '../../../theme'; -import { ConversationResponse, MessageResponse, MessageSegment } from '../../../types/dto'; -import { Avatar, Text } from '../../../components/common'; +import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto'; +import { Avatar, Text, ImageGallery } from '../../../components/common'; import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores'; import { extractTextFromSegments } from '../../../types/dto'; +import { useBreakpointGTE } from '../../../hooks/useResponsive'; +import { useMarkAsRead } from '../../../stores/messageManagerHooks'; +import { messageService } from '../../../services'; +import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel'; +import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen'; + +const { width: screenWidth } = Dimensions.get('window'); +const PANEL_WIDTH = 360; interface EmbeddedChatProps { conversation: ConversationResponse; onBack: () => void; } -const { width: screenWidth } = Dimensions.get('window'); - export const EmbeddedChat: React.FC = ({ conversation, onBack }) => { const navigation = useNavigation(); const currentUser = useAuthStore(state => state.currentUser); + // 响应式布局 - 使用 768px 作为大屏幕和小屏幕的分界线 + const isWideScreen = useBreakpointGTE('lg'); + + // 使用 markAsRead hook + const { markAsRead } = useMarkAsRead(String(conversation.id)); + // 状态 const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); @@ -42,6 +56,61 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack const flatListRef = useRef(null); const inputRef = useRef(null); + // 图片预览状态 + const [imageGalleryVisible, setImageGalleryVisible] = useState(false); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const [galleryImages, setGalleryImages] = useState<{ url: string }[]>([]); + + // 右键菜单状态 + const [contextMenuVisible, setContextMenuVisible] = useState(false); + const [contextMenuPosition, setContextMenuPosition] = useState(); + const [selectedMessageForMenu, setSelectedMessageForMenu] = useState(null); + + // 回复状态 + const [replyingToMessage, setReplyingToMessage] = useState(null); + + // 群信息面板动画 + const slideAnim = useRef(new Animated.Value(PANEL_WIDTH)).current; + const fadeAnim = useRef(new Animated.Value(0)).current; + const [showGroupInfoPanel, setShowGroupInfoPanel] = useState(false); + + // 群成员列表 + const [groupMembers, setGroupMembers] = useState([]); + + // 打开/关闭群信息面板 + const toggleGroupInfoPanel = useCallback(() => { + if (showGroupInfoPanel) { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: PANEL_WIDTH, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + ]).start(() => { + setShowGroupInfoPanel(false); + }); + } else { + setShowGroupInfoPanel(true); + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } + }, [showGroupInfoPanel, slideAnim, fadeAnim]); + // 获取会话信息 const isGroupChat = conversation.type === 'group'; const chatTitle = isGroupChat @@ -88,6 +157,18 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack }; }, [conversation.id, loadMessages]); + // 自动标记已读 - 当有新消息时 + useEffect(() => { + if (messages.length === 0) return; + + const maxSeq = Math.max(...messages.map(m => m.seq), 0); + if (maxSeq > 0) { + markAsRead(maxSeq).catch(error => { + console.error('[EmbeddedChat] 自动标记已读失败:', error); + }); + } + }, [messages, markAsRead]); + // 发送消息 const handleSend = useCallback(async () => { if (!inputText.trim() || sending) return; @@ -127,32 +208,193 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack } }; + // 处理图片点击预览 + const handleImagePress = (images: { url: string }[], index: number) => { + setGalleryImages(images); + setCurrentImageIndex(index); + setImageGalleryVisible(true); + }; + + // 处理右键点击消息 + const handleMessageRightPress = (message: MessageResponse, event: any) => { + // 获取鼠标右键位置 + const pageX = event?.nativeEvent?.pageX || 0; + const pageY = event?.nativeEvent?.pageY || 0; + + setSelectedMessageForMenu(message as GroupMessage); + setContextMenuPosition({ + x: pageX, + y: pageY, + width: 0, + height: 0, + pressX: pageX, + pressY: pageY, + }); + setContextMenuVisible(true); + }; + + // 关闭右键菜单 + const handleContextMenuClose = () => { + setContextMenuVisible(false); + setSelectedMessageForMenu(null); + setContextMenuPosition(undefined); + }; + + // 处理回复消息 + const handleReply = (message: GroupMessage) => { + setReplyingToMessage(message); + handleContextMenuClose(); + }; + + // 处理撤回消息 + const handleRecall = async (messageId: string) => { + try { + await messageService.recallMessage(messageId); + handleContextMenuClose(); + } catch (error) { + console.error('[EmbeddedChat] 撤回消息失败:', error); + } + }; + + // 处理删除消息 + const handleDeleteMessage = async (messageId: string) => { + try { + await messageService.deleteMessage(messageId); + handleContextMenuClose(); + } catch (error) { + console.error('[EmbeddedChat] 删除消息失败:', error); + } + }; + + // 渲染消息内容 + const renderMessageContent = (item: MessageResponse) => { + // 撤回消息 + if (item.status === 'recalled') { + return ( + + 消息已撤回 + + ); + } + + // 获取消息 segments + const segments = item.segments || []; + + // 检查是否有图片消息 + const imageSegments = segments + .filter(s => s.type === 'image') + .map(s => s.data as ImageSegmentData); + + // 提取文本内容 + const textContent = extractTextFromSegments(segments); + + // 渲染多个内容(文本 + 图片) + return ( + + {/* 文本内容 */} + {textContent && textContent !== '[图片]' && ( + + {textContent} + + )} + + {/* 图片内容 */} + {imageSegments.length > 0 && ( + + {imageSegments.map((imgData, idx) => { + const imageUrl = imgData.thumbnail_url || imgData.url; + const hasValidUrl = !!imageUrl && imageUrl.trim() !== ''; + + if (!hasValidUrl) { + // 无效URL显示占位符 + return ( + + + 图片 + + ); + } + + // 计算图片尺寸 + let width = 180; + let height = 180; + if (imgData.width && imgData.height) { + const aspectRatio = imgData.width / imgData.height; + if (aspectRatio > 1) { + height = width / aspectRatio; + } else { + width = height * aspectRatio; + } + } + + return ( + handleImagePress(imageSegments.map(s => ({ url: s.thumbnail_url || s.url || '' })), idx)} + activeOpacity={0.8} + > + + + ); + })} + + )} + + {/* 如果没有文本也没有图片,显示空消息提示 */} + {!textContent && imageSegments.length === 0 && ( + + [消息格式错误] + + )} + + ); + }; + // 渲染消息 const renderMessage = ({ item }: { item: MessageResponse }) => { const isMe = String(item.sender_id) === String(currentUser?.id); + // 处理指针事件(鼠标右键)- 使用 onPointerDown 检测鼠标右键 + const handlePointerDown = (e: any) => { + // e.nativeEvent.button: 0=左键, 1=中键, 2=右键 + const isRightClick = e.nativeEvent?.button === 2; + if (isRightClick) { + // 阻止默认右键菜单 + e.preventDefault?.(); + e.stopPropagation?.(); + handleMessageRightPress(item, e); + } + }; + return ( - + {!isMe && ( - )} {isGroupChat && !isMe && ( {item.sender?.nickname || item.sender?.username} )} - - {item.status === 'recalled' ? '消息已撤回' : extractTextFromSegments(item.segments)} - + {renderMessageContent(item)} {isMe && ( - )} @@ -163,9 +405,14 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack {/* 头部 */} - - - + {/* 大屏幕(>= 768px)时隐藏返回按钮 */} + {!isWideScreen ? ( + + + + ) : ( + + )} @@ -174,9 +421,16 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack - - - + {/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */} + {isWideScreen && isGroupChat ? ( + + + + ) : ( + + + + )} {/* 消息列表 */} @@ -231,6 +485,9 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack returnKeyType="send" onSubmitEditing={handleSend} blurOnSubmit={false} + // 确保光标可见 + cursorColor={colors.primary.main} + selectionColor={`${colors.primary.main}40`} /> @@ -252,6 +509,96 @@ export const EmbeddedChat: React.FC = ({ conversation, onBack + + {/* 群信息侧边栏面板 - 仅大屏幕显示 */} + {isWideScreen && isGroupChat && ( + <> + {/* 遮罩层 */} + {showGroupInfoPanel && ( + + + + )} + + {/* 侧边栏面板 */} + + { + // TODO: 实现置顶功能 + console.log('Toggle pin:', pinned); + }} + onLeaveGroup={() => { + // TODO: 实现退出群聊功能 + console.log('Leave group'); + }} + onInviteMembers={() => { + // TODO: 实现邀请新成员功能 + console.log('Invite members'); + }} + onViewAllMembers={() => { + // TODO: 实现查看全部成员功能 + console.log('View all members'); + }} + /> + + + )} + + {/* 右键菜单 */} + + + {/* 图片查看器 */} + ({ + id: String(idx), + url: img.url || '' + }))} + initialIndex={currentImageIndex} + onClose={() => setImageGalleryVisible(false)} + enableSave + /> ); }; @@ -328,7 +675,7 @@ const styles = StyleSheet.create({ }, messageRowRight: { alignSelf: 'flex-end', - flexDirection: 'row-reverse', + justifyContent: 'flex-end', }, messageBubble: { maxWidth: screenWidth * 0.5, @@ -387,6 +734,12 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, minHeight: 40, justifyContent: 'center', + // 移除阴影 + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, }, textInput: { fontSize: 15, @@ -406,4 +759,33 @@ const styles = StyleSheet.create({ sendButtonDisabled: { backgroundColor: '#E0E0E0', }, + messageTextRecalled: { + fontStyle: 'italic', + color: '#999', + }, + imageGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 4, + }, + messageImage: { + borderRadius: 8, + marginRight: 4, + marginBottom: 4, + }, + imagePlaceholder: { + width: 120, + height: 120, + borderRadius: 8, + backgroundColor: 'rgba(0,0,0,0.1)', + justifyContent: 'center', + alignItems: 'center', + marginRight: 4, + marginBottom: 4, + }, + imagePlaceholderText: { + color: '#999', + fontSize: 12, + marginTop: 4, + }, }); diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx index 4a9f00c..1053576 100644 --- a/src/screens/profile/ProfileScreen.tsx +++ b/src/screens/profile/ProfileScreen.tsx @@ -37,7 +37,14 @@ const TAB_ICONS = ['file-document-outline', 'bookmark-outline']; export const ProfileScreen: React.FC = () => { const navigation = useNavigation(); const insets = useSafeAreaInsets(); - const tabBarHeight = useBottomTabBarHeight(); + // 在大屏幕模式下不使用 Bottom Tab Navigator,所以 try-catch 处理 + let tabBarHeight = 0; + try { + tabBarHeight = useBottomTabBarHeight(); + } catch (e) { + // 大屏幕模式下不在 Bottom Tab Navigator 中,会抛出错误,忽略即可 + tabBarHeight = 0; + } const homeNavigation = useNavigation(); // 使用 any 类型来访问根导航 const rootNavigation = useNavigation(); diff --git a/src/theme/index.ts b/src/theme/index.ts index 77291c8..4a511c4 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -6,9 +6,9 @@ // ==================== 颜色系统 ==================== export const colors = { primary: { - main: '#FF6B35', // 胡萝卜橙 - light: '#FF8F66', - dark: '#E5521D', + main: '#FF6B35', // 橙色(主色) + light: '#FFB733', + dark: '#CC8400', contrast: '#FFFFFF', }, secondary: { @@ -16,6 +16,18 @@ export const colors = { light: '#80E27E', dark: '#087F23', }, + neutral: { + main: '#6B7280', // 中性灰(辅助色) + light: '#9CA3AF', + dark: '#374151', + bg: '#F3F4F6', // 浅灰背景 + bgDark: '#1F2937', // 深灰背景 + }, + accent: { + main: '#E57373', // 低饱和红(点缀色) + light: '#FFCDD2', + dark: '#C62828', + }, background: { default: '#F5F5F5', paper: '#FFFFFF', @@ -73,6 +85,8 @@ export const spacing = { '2xl': 24, '3xl': 32, '4xl': 40, + '5xl': 48, + '6xl': 56, }; // ==================== 圆角系统 ==================== diff --git a/src/utils/inputStyles.ts b/src/utils/inputStyles.ts new file mode 100644 index 0000000..202da48 --- /dev/null +++ b/src/utils/inputStyles.ts @@ -0,0 +1,33 @@ +/** + * 输入框辅助样式 + * 用于处理React Native Web在浏览器中的默认focus outline样式 + */ + +/** + * 移除浏览器默认focus outline的样式 + * 这个样式可以覆盖浏览器原生的黑框/蓝框 + */ +export const noFocusOutline = { + // 移除浏览器默认的outline(针对Web) + outlineWidth: 0, + outline: 'none' as const, + outlineColor: 'transparent', + // 兼容React Native Web + ':focus': { + outlineWidth: 0, + outline: 'none', + }, + // Webkit浏览器兼容 + '::placeholder': { + outlineWidth: 0, + }, +}; + +/** + * 为TextInput添加无outline样式 + */ +export const inputNoFocusOutline = { + ...noFocusOutline, + // 确保placeholder也没有outline + placeholderTextColor: undefined, // 会在使用时覆盖 +}; -- 2.49.1 From b50bd9abb3c6881335ca8943c9b8e77509f65b8e Mon Sep 17 00:00:00 2001 From: LostSalt Date: Tue, 17 Mar 2026 12:29:04 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E5=91=A8=E6=95=B0=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/frontend.iml | 9 + .idea/gradle.xml | 448 ++++++++++++++++++ .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + src/components/business/SystemMessageItem.tsx | 33 +- src/screens/message/NotificationsScreen.tsx | 68 ++- src/screens/schedule/ScheduleScreen.tsx | 17 +- src/services/scheduleService.ts | 44 +- 10 files changed, 605 insertions(+), 43 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/frontend.iml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/frontend.iml b/.idea/frontend.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/frontend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..6752ffb --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,448 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ade1ecd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f3d93d7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/business/SystemMessageItem.tsx b/src/components/business/SystemMessageItem.tsx index d8f0b9b..64894ec 100644 --- a/src/components/business/SystemMessageItem.tsx +++ b/src/components/business/SystemMessageItem.tsx @@ -13,7 +13,6 @@ import { colors, spacing, borderRadius } from '../../theme'; import { SystemMessageResponse, SystemMessageType } from '../../types/dto'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; -import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive'; interface SystemMessageItemProps { message: SystemMessageResponse; @@ -165,14 +164,6 @@ const SystemMessageItem: React.FC = ({ onRequestAction, requestActionLoading = false, }) => { - // 响应式适配 - const { isWideScreen, isDesktop } = useResponsive(); - const containerPadding = useResponsiveValue({ xs: spacing.md, lg: spacing.lg, xl: spacing.xl }); - const avatarSize = useResponsiveValue({ xs: 40, lg: 48, xl: 52 }); - const iconSize = useResponsiveValue({ xs: 20, lg: 22, xl: 24 }); - const contentFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); - const titleFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); - const timeFontSize = useResponsiveValue({ xs: 12, lg: 13, xl: 14 }); // 格式化时间 const formatTime = (dateString: string): string => { try { @@ -198,7 +189,15 @@ const SystemMessageItem: React.FC = ({ requestStatus === 'pending' && !!onRequestAction; - const styles = React.useMemo(() => StyleSheet.create({ + // 使用固定的响应式值,兼容移动端和Web端 + const containerPadding = spacing.md; + const avatarSize = 44; + const iconSize = 22; + const contentFontSize = 14; + const titleFontSize = 14; + const timeFontSize = 12; + + const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'flex-start', @@ -208,7 +207,7 @@ const SystemMessageItem: React.FC = ({ borderBottomColor: colors.divider, }, iconContainer: { - marginRight: isWideScreen ? spacing.lg : spacing.md, + marginRight: spacing.md, }, iconWrapper: { width: avatarSize, @@ -220,6 +219,7 @@ const SystemMessageItem: React.FC = ({ content: { flex: 1, justifyContent: 'center', + minWidth: 0, }, titleRow: { flexDirection: 'row', @@ -237,7 +237,7 @@ const SystemMessageItem: React.FC = ({ fontSize: timeFontSize, }, messageContent: { - lineHeight: isWideScreen ? 22 : 18, + lineHeight: 20, fontSize: contentFontSize, }, extraInfo: { @@ -245,7 +245,7 @@ const SystemMessageItem: React.FC = ({ alignItems: 'center', marginTop: spacing.xs, backgroundColor: colors.background.default, - paddingHorizontal: isWideScreen ? spacing.md : spacing.sm, + paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, alignSelf: 'flex-start', @@ -259,8 +259,8 @@ const SystemMessageItem: React.FC = ({ marginTop: spacing.sm, }, actionBtn: { - paddingVertical: isWideScreen ? spacing.sm : spacing.xs, - paddingHorizontal: isWideScreen ? spacing.lg : spacing.md, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, borderRadius: borderRadius.sm, borderWidth: 1, marginRight: spacing.sm, @@ -278,8 +278,9 @@ const SystemMessageItem: React.FC = ({ justifyContent: 'center', alignItems: 'center', height: 20, + width: 24, }, - }), [containerPadding, isWideScreen, avatarSize, titleFontSize, timeFontSize, contentFontSize]); + }); // 判断是否显示操作者头像 const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes( diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index c3ca10a..6a05791 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -13,6 +13,7 @@ import { TouchableOpacity, RefreshControl, ActivityIndicator, + Dimensions, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; @@ -25,7 +26,6 @@ import { messageService } from '../../services/messageService'; import { commentService } from '../../services/commentService'; import { SystemMessageItem } from '../../components/business'; import { Text, EmptyState, ResponsiveContainer } from '../../components/common'; -import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { RootStackParamList } from '../../navigation/types'; import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores'; @@ -48,9 +48,27 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); const navigation = useNavigation>(); - // 响应式布局 - const { isDesktop, isTablet } = useResponsive(); - const isWideScreen = useBreakpointGTE('lg'); + // 修复竞态条件:使用本地状态管理窗口尺寸,避免 hydration 问题 + const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); + const [isHydrated, setIsHydrated] = useState(false); + + useEffect(() => { + // 延迟设置窗口尺寸,确保在客户端完全加载后再获取 + const timer = setTimeout(() => { + const { width, height } = Dimensions.get('window'); + setWindowSize({ width, height }); + setIsHydrated(true); + }, 100); + return () => clearTimeout(timer); + }, []); + + // 使用本地尺寸状态计算断点 (lg = 768) + const isWideScreen = windowSize.width >= 768; + const isDesktop = windowSize.width >= 1024; + const isTablet = windowSize.width >= 768 && windowSize.width < 1024; + + // Web端使用更大的容器宽度 + const containerMaxWidth = isDesktop ? 1200 : isTablet ? 1000 : 900; const [messages, setMessages] = useState([]); const [activeType, setActiveType] = useState('all'); @@ -320,7 +338,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const shouldShowBackButton = onBack && !isWideScreen; return ( - + {shouldShowBackButton ? ( void }> = ({ onBack // 渲染空状态 const renderEmpty = () => ( - + + + ); + // 如果还未hydrated,显示加载占位符,避免竞态条件导致的渲染问题 + if (!isHydrated) { + return ( + + + + + + ); + } + return ( {isWideScreen ? ( - + {/* 头部 - 大屏模式下隐藏返回按钮 */} {renderHeader()} {/* 分类筛选 */} - + {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length @@ -518,10 +549,11 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, borderBottomColor: colors.divider, }, - filterContainerWide: { + filterContainerWideWeb: { paddingHorizontal: spacing.xl, paddingVertical: spacing.md, justifyContent: 'center', + backgroundColor: colors.background.paper, }, filterTag: { flexDirection: 'row', @@ -596,4 +628,14 @@ const styles = StyleSheet.create({ loadingMoreText: { marginLeft: spacing.sm, }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + emptyContainerWeb: { + minHeight: 500, + justifyContent: 'center', + paddingTop: 100, + }, }); diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index f73a915..b9e44b0 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -30,6 +30,7 @@ import { TimeSlot, getCourseColor, hasCourseInWeek, + getCurrentWeek, } from '../../types/schedule'; import type { ScheduleStackParamList } from '../../navigation/types'; import { scheduleService } from '../../services/scheduleService'; @@ -130,9 +131,17 @@ const getWeekDates = (weekOffset: number = 0) => { return getWeekInfo(weekOffset).dates; }; +// 学期起始日期字符串(用于 getCurrentWeek 函数) +const SEMESTER_START_DATE = '2026-03-09'; + +// 计算当前实际周数 +const getInitialWeek = (): number => { + return getCurrentWeek(SEMESTER_START_DATE, TOTAL_WEEKS); +}; + export const ScheduleScreen: React.FC = () => { const navigation = useNavigation>(); - const [currentWeek, setCurrentWeek] = useState(1); + const [currentWeek, setCurrentWeek] = useState(getInitialWeek); const [courses, setCourses] = useState([]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [pendingDay, setPendingDay] = useState(0); @@ -163,8 +172,10 @@ export const ScheduleScreen: React.FC = () => { }; }, []); const todayColumnIndex = getTodayColumnIndex(); - // currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列 - const isViewingCurrentWeek = currentWeek === 1; + // 获取当前实际周数,用于判断是否在查看当前周 + const actualCurrentWeek = getInitialWeek(); + // 只有在查看当前实际周时才高亮今日列 + const isViewingCurrentWeek = currentWeek === actualCurrentWeek; const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1; const dayColumnWidth = getDayColumnWidth(); diff --git a/src/services/scheduleService.ts b/src/services/scheduleService.ts index 875d20c..1b6c551 100644 --- a/src/services/scheduleService.ts +++ b/src/services/scheduleService.ts @@ -45,22 +45,42 @@ interface SyncScheduleResponse { error_message?: string; } -const toCourse = (dto: ScheduleCourseDTO): Course => ({ - id: dto.id, - name: dto.name, - teacher: dto.teacher, - location: dto.location, - dayOfWeek: dto.day_of_week, - startSection: dto.start_section, - endSection: dto.end_section, - weeks: dto.weeks ?? [], - color: dto.color, -}); +const toCourse = (dto: ScheduleCourseDTO): Course => { + // 调试日志:检查后端返回的 weeks 字段 + console.log('[ScheduleService] 转换课程 DTO:', { + id: dto.id, + name: dto.name, + day_of_week: dto.day_of_week, + weeks: dto.weeks, + weeksType: typeof dto.weeks, + weeksLength: dto.weeks?.length, + }); + return { + id: dto.id, + name: dto.name, + teacher: dto.teacher, + location: dto.location, + dayOfWeek: dto.day_of_week, + startSection: dto.start_section, + endSection: dto.end_section, + weeks: dto.weeks ?? [], + color: dto.color, + }; +}; class ScheduleService { async getCourses(week?: number): Promise { const params = week ? { week } : undefined; + console.log('[ScheduleService] 获取课程列表, params:', params); const response = await api.get('/schedule/courses', params); + console.log('[ScheduleService] 课程列表响应, 数量:', response.data.list.length); + // 打印原始响应数据中的 weeks 字段 + response.data.list.forEach((item, index) => { + console.log(`[ScheduleService] 课程[${index}] ${item.name}:`, { + day_of_week: item.day_of_week, + weeks: item.weeks, + }); + }); return response.data.list.map(toCourse); } @@ -79,7 +99,9 @@ class ScheduleService { } async syncSchedule(req: SyncScheduleRequest): Promise { + console.log('[ScheduleService] 开始同步教务系统, username:', req.username); const response = await api.post('/schedule/sync', req); + console.log('[ScheduleService] 同步响应:', response.data); return response.data; } } -- 2.49.1