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/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 0000000..2c35236
Binary files /dev/null and b/screenshots/after-fix.png differ
diff --git a/screenshots/test-navigation-fix.png b/screenshots/test-navigation-fix.png
new file mode 100644
index 0000000..060a680
Binary files /dev/null and b/screenshots/test-navigation-fix.png differ
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 && (
= ({
requestStatus === 'pending' &&
!!onRequestAction;
+ // 使用固定的响应式值,兼容移动端和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',
+ padding: containerPadding,
+ backgroundColor: colors.background.paper,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.divider,
+ },
+ iconContainer: {
+ marginRight: spacing.md,
+ },
+ iconWrapper: {
+ width: avatarSize,
+ height: avatarSize,
+ borderRadius: avatarSize / 2,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ content: {
+ flex: 1,
+ justifyContent: 'center',
+ minWidth: 0,
+ },
+ 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: 20,
+ fontSize: contentFontSize,
+ },
+ 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,
+ width: 24,
+ },
+ });
+
// 判断是否显示操作者头像
const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes(
message.system_type
@@ -207,19 +300,19 @@ const SystemMessageItem: React.FC = ({
{showGroupAvatar ? (
) : showOperatorAvatar ? (
) : (
-
+
)}
@@ -231,7 +324,7 @@ const SystemMessageItem: React.FC = ({
{title}
-
+
{formatTime(message.created_at)}
@@ -288,82 +381,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..6a05791 100644
--- a/src/screens/message/NotificationsScreen.tsx
+++ b/src/screens/message/NotificationsScreen.tsx
@@ -13,18 +13,19 @@ import {
TouchableOpacity,
RefreshControl,
ActivityIndicator,
+ Dimensions,
} 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';
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';
@@ -41,15 +42,33 @@ 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();
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');
@@ -148,16 +167,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,21 +332,62 @@ export const NotificationsScreen: React.FC = () => {
);
};
+ // 渲染头部(包含返回按钮)
+ const renderHeader = () => {
+ // 大屏模式(>= lg 断点)隐藏返回按钮
+ const shouldShowBackButton = onBack && !isWideScreen;
+
+ return (
+
+ {shouldShowBackButton ? (
+
+
+
+ ) : (
+
+ )}
+
+ 系统通知
+
+
+
+ );
+ };
+
// 渲染空状态
const renderEmpty = () => (
-
+
+
+
);
+ // 如果还未hydrated,显示加载占位符,避免竞态条件导致的渲染问题
+ if (!isHydrated) {
+ return (
+
+
+
+
+
+ );
+ }
+
return (
{isWideScreen ? (
-
+
+ {/* 头部 - 大屏模式下隐藏返回按钮 */}
+ {renderHeader()}
{/* 分类筛选 */}
-
+
{MESSAGE_TYPES.map(type => {
const count = type.key === 'all'
? displayMessages.length
@@ -403,6 +459,8 @@ export const NotificationsScreen: React.FC = () => {
) : (
<>
+ {/* 头部 - 小屏模式下显示返回按钮 */}
+ {renderHeader()}
{/* 分类筛选 */}
{MESSAGE_TYPES.map(type => {
@@ -491,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',
@@ -516,6 +575,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,
},
@@ -537,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/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 f2623fd..a7d92e0 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/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;
}
}
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, // 会在使用时覆盖
+};