refactor(TabsLayout, ImageGallery, ChatScreen, GroupInfoScreen, PrivateChatInfoScreen): enhance UI components and improve layout structure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 3m4s
Frontend CI / ota-android (push) Successful in 12m24s
Frontend CI / build-android-apk (push) Successful in 1h17m36s

- Adjusted tab bar dimensions and margins in TabsLayout for better visual consistency.
- Streamlined ImageGallery by removing loading states and optimizing image transition handling.
- Updated ChatScreen to utilize SafeAreaView for improved layout on different devices.
- Enhanced GroupInfoScreen and PrivateChatInfoScreen with a consistent page header layout, including back navigation.
- Refactored ChatHeader to simplify structure and improve maintainability.
This commit is contained in:
lafay
2026-03-24 15:39:28 +08:00
parent 2ddb9cadd8
commit b49cc0f3bd
6 changed files with 126 additions and 110 deletions

View File

@@ -7,9 +7,9 @@ import { colors, shadows } from '../../../src/theme';
import { BREAKPOINTS } from '../../../src/hooks/useResponsive'; import { BREAKPOINTS } from '../../../src/hooks/useResponsive';
import { useTotalUnreadCount } from '../../../src/stores'; import { useTotalUnreadCount } from '../../../src/stores';
const TAB_BAR_HEIGHT = 64; const TAB_BAR_HEIGHT = 56;
const TAB_BAR_MARGIN = 14;
const TAB_BAR_FLOATING_MARGIN = 12; const TAB_BAR_FLOATING_MARGIN = 12;
const TAB_BAR_MARGIN = 24;
export default function TabsLayout() { export default function TabsLayout() {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
@@ -27,19 +27,25 @@ export default function TabsLayout() {
? { display: 'none', height: 0, overflow: 'hidden' } ? { display: 'none', height: 0, overflow: 'hidden' }
: { : {
position: 'absolute', position: 'absolute',
left: TAB_BAR_MARGIN, left: 0,
right: TAB_BAR_MARGIN, right: 0,
marginHorizontal: TAB_BAR_MARGIN,
bottom: TAB_BAR_FLOATING_MARGIN + insets.bottom, bottom: TAB_BAR_FLOATING_MARGIN + insets.bottom,
height: TAB_BAR_HEIGHT, height: TAB_BAR_HEIGHT,
borderRadius: 24, borderRadius: 22,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
...shadows.lg, ...shadows.lg,
borderTopWidth: 0, borderTopWidth: 0,
elevation: 100, elevation: 100,
zIndex: 100, zIndex: 100,
}, },
tabBarItemStyle: {
borderRadius: 16,
marginHorizontal: 2,
paddingVertical: 1,
},
tabBarShowLabel: true, tabBarShowLabel: true,
tabBarLabelStyle: { fontSize: 12, fontWeight: '600', marginTop: -2 }, tabBarLabelStyle: { fontSize: 11, fontWeight: '600', marginTop: -1 },
}} }}
> >
<Tabs.Screen <Tabs.Screen
@@ -49,7 +55,7 @@ export default function TabsLayout() {
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<MaterialCommunityIcons <MaterialCommunityIcons
name={focused ? 'home' : 'home-outline'} name={focused ? 'home' : 'home-outline'}
size={focused ? 26 : 24} size={focused ? 24 : 22}
color={color} color={color}
/> />
), ),
@@ -63,7 +69,7 @@ export default function TabsLayout() {
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<MaterialCommunityIcons <MaterialCommunityIcons
name={focused ? 'message-text' : 'message-text-outline'} name={focused ? 'message-text' : 'message-text-outline'}
size={focused ? 26 : 24} size={focused ? 24 : 22}
color={color} color={color}
/> />
), ),
@@ -74,7 +80,7 @@ export default function TabsLayout() {
options={{ options={{
title: '课表', title: '课表',
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<MaterialCommunityIcons name="calendar-today" size={focused ? 26 : 24} color={color} /> <MaterialCommunityIcons name="calendar-today" size={focused ? 24 : 22} color={color} />
), ),
}} }}
/> />
@@ -85,7 +91,7 @@ export default function TabsLayout() {
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<MaterialCommunityIcons <MaterialCommunityIcons
name={focused ? 'account' : 'account-outline'} name={focused ? 'account' : 'account-outline'}
size={focused ? 26 : 24} size={focused ? 24 : 22}
color={color} color={color}
/> />
), ),

View File

@@ -13,7 +13,6 @@ import {
TouchableOpacity, TouchableOpacity,
Text, Text,
StatusBar, StatusBar,
ActivityIndicator,
Alert, Alert,
} from 'react-native'; } from 'react-native';
import { Image as ExpoImage } from 'expo-image'; import { Image as ExpoImage } from 'expo-image';
@@ -25,10 +24,8 @@ import {
import Animated, { import Animated, {
useSharedValue, useSharedValue,
useAnimatedStyle, useAnimatedStyle,
useDerivedValue,
runOnJS, runOnJS,
withTiming, withTiming,
withSpring,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
@@ -91,7 +88,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
const [currentIndex, setCurrentIndex] = useState(initialIndex); const [currentIndex, setCurrentIndex] = useState(initialIndex);
currentIndexRef.current = currentIndex; currentIndexRef.current = currentIndex;
const [showControls, setShowControls] = useState(true); const [showControls, setShowControls] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saveToast, setSaveToast] = useState<'success' | 'error' | null>(null); const [saveToast, setSaveToast] = useState<'success' | 'error' | null>(null);
@@ -132,7 +128,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
if (visible) { if (visible) {
setCurrentIndex(initialIndex); setCurrentIndex(initialIndex);
setShowControls(true); setShowControls(true);
setLoading(true);
setError(false); setError(false);
resetZoom(); resetZoom();
StatusBar.setHidden(true, 'fade'); StatusBar.setHidden(true, 'fade');
@@ -166,7 +161,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
if (clampedIndex === currentIndexRef.current) { if (clampedIndex === currentIndexRef.current) {
return; return;
} }
setLoading(true);
setError(false); setError(false);
setCurrentIndex(clampedIndex); setCurrentIndex(clampedIndex);
onIndexChange?.(clampedIndex); onIndexChange?.(clampedIndex);
@@ -266,24 +260,16 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 滑动切换图片相关状态 // 滑动切换图片相关状态
const swipeTranslateX = useSharedValue(0); const swipeTranslateX = useSharedValue(0);
// 滑动切图完成后,让新图从目标方向进入,避免旧图先弹回导致前后图闪烁
useEffect(() => { useEffect(() => {
const direction = pendingSwipeDirectionRef.current;
if (direction == null) {
return;
}
pendingSwipeDirectionRef.current = null; pendingSwipeDirectionRef.current = null;
swipeTranslateX.value = -direction * SCREEN_WIDTH; swipeTranslateX.value = 0;
swipeTranslateX.value = withTiming(0, { duration: 180 });
}, [currentIndex, swipeTranslateX]); }, [currentIndex, swipeTranslateX]);
const switchWithDirection = useCallback( const switchWithDirection = useCallback(
(targetIndex: number, direction: -1 | 1) => { (targetIndex: number, direction: -1 | 1) => {
swipeTranslateX.value = withTiming(direction * SCREEN_WIDTH, { duration: 200 }, () => { updateIndexFromSwipe(targetIndex, direction);
runOnJS(updateIndexFromSwipe)(targetIndex, direction);
});
}, },
[swipeTranslateX, updateIndexFromSwipe] [updateIndexFromSwipe]
); );
const goToPrev = useCallback(() => { const goToPrev = useCallback(() => {
@@ -310,16 +296,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 放大状态下:拖动图片 // 放大状态下:拖动图片
translateX.value = savedTranslateX.value + e.translationX; translateX.value = savedTranslateX.value + e.translationX;
translateY.value = savedTranslateY.value + e.translationY; translateY.value = savedTranslateY.value + e.translationY;
} else if (validImages.length > 1) {
// 未放大且有多张图片:切换图片的跟随效果
const isFirst = currentIndex === 0 && e.translationX > 0;
const isLast = currentIndex === validImages.length - 1 && e.translationX < 0;
if (isFirst || isLast) {
// 边界阻力效果
swipeTranslateX.value = e.translationX * 0.3;
} else {
swipeTranslateX.value = e.translationX;
}
} }
}) })
.onEnd((e) => { .onEnd((e) => {
@@ -337,17 +313,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
if (shouldGoNext && currentIndex < validImages.length - 1) { if (shouldGoNext && currentIndex < validImages.length - 1) {
// 向左滑动,显示下一张 // 向左滑动,显示下一张
swipeTranslateX.value = withTiming(-SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(updateIndexFromSwipe)(currentIndex + 1, -1); runOnJS(updateIndexFromSwipe)(currentIndex + 1, -1);
});
} else if (shouldGoPrev && currentIndex > 0) { } else if (shouldGoPrev && currentIndex > 0) {
// 向右滑动,显示上一张 // 向右滑动,显示上一张
swipeTranslateX.value = withTiming(SCREEN_WIDTH, { duration: 200 }, () => {
runOnJS(updateIndexFromSwipe)(currentIndex - 1, 1); runOnJS(updateIndexFromSwipe)(currentIndex - 1, 1);
});
} else {
// 回弹到原位
swipeTranslateX.value = withTiming(0, { duration: 200 });
} }
} }
}); });
@@ -411,7 +380,7 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
disabled={saving} disabled={saving}
> >
{saving ? ( {saving ? (
<ActivityIndicator size="small" color="#FFF" /> <MaterialCommunityIcons name="loading" size={24} color="#FFF" />
) : ( ) : (
<MaterialCommunityIcons name="download" size={24} color="#FFF" /> <MaterialCommunityIcons name="download" size={24} color="#FFF" />
)} )}
@@ -423,12 +392,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
{/* 图片显示区域 */} {/* 图片显示区域 */}
<GestureDetector gesture={composedGesture}> <GestureDetector gesture={composedGesture}>
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
{loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#FFF" />
</View>
)}
{error && ( {error && (
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<MaterialCommunityIcons name="image-off" size={48} color="#999" /> <MaterialCommunityIcons name="image-off" size={48} color="#999" />
@@ -447,13 +410,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
recyclingKey={currentImage.url} recyclingKey={currentImage.url}
transition={null} transition={null}
allowDownscaling allowDownscaling
onLoadStart={() => setLoading(true)}
onLoad={() => { onLoad={() => {
setLoading(false);
setError(false); setError(false);
}} }}
onError={() => { onError={() => {
setLoading(false);
setError(true); setError(true);
}} }}
/> />
@@ -584,13 +544,6 @@ const styles = StyleSheet.create({
width: SCREEN_WIDTH, width: SCREEN_WIDTH,
height: SCREEN_HEIGHT * 0.8, height: SCREEN_HEIGHT * 0.8,
}, },
loadingContainer: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
backgroundColor: '#000',
},
errorContainer: { errorContainer: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
justifyContent: 'center', justifyContent: 'center',

View File

@@ -29,7 +29,7 @@ import {
} from 'react-native'; } from 'react-native';
import { useNavigation, useRouter } from 'expo-router'; import { useNavigation, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, ImageGallery, ImageGridItem } from '../../components/common'; import { Text, ImageGallery, ImageGridItem } from '../../components/common';
import { colors } from '../../theme'; import { colors } from '../../theme';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
@@ -364,8 +364,9 @@ export const ChatScreen: React.FC = () => {
}, [handleMessageListContentSizeChange]); }, [handleMessageListContentSizeChange]);
return ( return (
<SafeAreaView style={containerStyle} edges={['top']}>
<KeyboardAvoidingView <KeyboardAvoidingView
style={containerStyle} style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
> >
@@ -613,6 +614,7 @@ export const ChatScreen: React.FC = () => {
}} }}
/> />
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView>
); );
}; };

View File

@@ -44,6 +44,7 @@ import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores/messageManager'; import { messageManager } from '../../stores/messageManager';
import { groupManager } from '../../stores/groupManager'; import { groupManager } from '../../stores/groupManager';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal'; import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
import { AppBackButton } from '../../components/common';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
@@ -537,7 +538,12 @@ const GroupInfoScreen: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<Loading /> <Loading />
</SafeAreaView> </SafeAreaView>
); );
@@ -545,7 +551,12 @@ const GroupInfoScreen: React.FC = () => {
if (!group) { if (!group) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<Text variant="body" color={colors.text.secondary}> <Text variant="body" color={colors.text.secondary}>
@@ -556,7 +567,12 @@ const GroupInfoScreen: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
@@ -1060,6 +1076,23 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
}, },
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
pageTitle: {
fontWeight: '600',
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: { scrollView: {
flex: 1, flex: 1,
}, },

View File

@@ -30,6 +30,7 @@ import { userManager } from '../../stores/userManager';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common'; import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { User } from '../../types'; import { User } from '../../types';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { AppBackButton } from '../../components/common';
const PrivateChatInfoScreen: React.FC = () => { const PrivateChatInfoScreen: React.FC = () => {
const router = useRouter(); const router = useRouter();
@@ -309,7 +310,12 @@ const PrivateChatInfoScreen: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<Loading /> <Loading />
</SafeAreaView> </SafeAreaView>
); );
@@ -322,7 +328,12 @@ const PrivateChatInfoScreen: React.FC = () => {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
@@ -414,6 +425,23 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
}, },
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
pageTitle: {
fontWeight: '600',
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: { scrollView: {
flex: 1, flex: 1,
}, },

View File

@@ -5,7 +5,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native'; import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Avatar, Text, AppBackButton } from '../../../../components/common'; import { Avatar, Text, AppBackButton } from '../../../../components/common';
import { colors, spacing } from '../../../../theme'; import { colors, spacing } from '../../../../theme';
@@ -69,14 +68,9 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
}; };
return ( return (
<SafeAreaView edges={['top']} style={styles.headerContainer}> <View style={styles.headerContainer}>
<View style={styles.header}> <View style={styles.header}>
{/* 大屏幕(>= 768px时隐藏返回按钮 */}
{!isWideScreen ? (
<AppBackButton style={styles.backButton} onPress={onBack} /> <AppBackButton style={styles.backButton} onPress={onBack} />
) : (
<View style={styles.backButton} />
)}
<View style={styles.headerCenter}> <View style={styles.headerCenter}>
<TouchableOpacity <TouchableOpacity
@@ -136,7 +130,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
</SafeAreaView> </View>
); );
}; };