帖子详情页

69 阅读4分钟

1.实现过程

在主页点击某一条帖子可以跳转到帖子的详情页,使用路由导航跳转到详情页并传递参数,同时主页默认只展示帖子的第一种图片,如果有多张图片,则在图片右下角显示+N(比如有两张图片则显示+1)

<MasonryList
          innerRef={scrollRef}
          data={posts}
          keyExtractor={(item: Post) => item.id.toString()}
          numColumns={2}
          contentContainerStyle={styles.content}
          showsVerticalScrollIndicator={true}
          refreshing={refreshing}
          onRefresh={onRefresh}
          refreshControl={true}
          refreshControlProps={{
            colors: [COLORS.primary], // Android 上的颜色
            tintColor: COLORS.primary, // iOS 上的颜色
          }}
          renderItem={({ item }: any) => {
            const handlePress = () => {
              navigation.navigate('PostDetail', { post: item });
            };
            if (item.images && item.images.length > 0) {
              return (
                <TouchableOpacity onPress={handlePress}>
                  <View style={[styles.card, { width: CARD_WIDTH, alignSelf: 'flex-start' }]}>
                    <Text style={styles.cardTitle}>{item.title}</Text>
                    <Text style={styles.cardSummary}>{item.summary}</Text>
                    <View style={{ marginTop: 8 }}>
                      {/* 只展示第一张图片 */}
                      <Image
                        source={{ uri: item.images[0] }}
                        style={{ width: '100%', height: 120, borderRadius: 8 }}
                        resizeMode="cover"
                      />
                      {/* 如果有多张图片,显示+N的提示 */}
                      {item.images.length > 1 && (
                        <View style={{
                          position: 'absolute',
                          right: 8,
                          bottom: 8,
                          backgroundColor: 'rgba(0,0,0,0.6)',
                          paddingHorizontal: 8,
                          paddingVertical: 4,
                          borderRadius: 12,
                        }}>
                          <Text style={{ color: 'white', fontSize: 12 }}>+{item.images.length - 1}</Text>
                        </View>
                      )}
                    </View>
                  </View>
                </TouchableOpacity>
              );
            }
            return (
              <TouchableOpacity onPress={handlePress}>
                <View style={[styles.card, { width: CARD_WIDTH, alignSelf: 'flex-start' }]}>
                  <Text style={styles.cardTitle}>{item.title}</Text>
                  <Text style={styles.cardSummary}>{item.summary}</Text>
                </View>
              </TouchableOpacity>
            );
          }}
        />

帖子详情页采用FlatList实现图片的横向滚动

slideSize 和 roundIndex 是在 PostDetailScreen.tsx 文件中用于实现图片轮播功能的关键变量

slideSize 表示每个轮播项(在这个例子中是图片)的宽度。在代码中,它是从滚动事件中获取的:

const slideSize = event.nativeEvent.layoutMeasurement.width;

这里的 event.nativeEvent.layoutMeasurement.width 获取的是 FlatList 组件可见区域的宽度,也就是一个完整轮播项的宽度。在应用中,由于我们设置了 pagingEnabled={true},每次滚动都会停在完整的一项上,所以这个宽度通常等于设备的屏幕宽度。

roundIndex 是当前显示的轮播项的索引(从0开始计数)。它是通过计算滚动位置与轮播项宽度的比值,然后四舍五入得到的:

const roundIndex = Math.round(event.nativeEvent.contentOffset.x / slideSize);

这里的计算过程是:

event.nativeEvent.contentOffset.x 获取水平滚动的偏移量(即用户滑动了多远)

将这个偏移量除以 slideSize(每个轮播项的宽度)得到一个比值

使用 Math.round() 将这个比值四舍五入到最接近的整数,这就是当前显示的轮播项的索引

例如:

  • 如果用户没有滚动(在第一张图片),contentOffset.x 为 0,则 roundIndex 为 0
  • 如果用户滚动到第二张图片,contentOffset.x 大约为屏幕宽度,则 roundIndex 为 1
  • 如果用户正在从第一张图片滑向第二张图片,但还没完全滑过去(比如滑了屏幕宽度的40%),则 contentOffset.x 约为屏幕宽度的0.4倍,roundIndex 仍为0(因为四舍五入)
  • 如果用户已经滑过了屏幕宽度的50%以上,则 roundIndex 会变为1
const handleScroll = (event: any) => {
    const slideSize = event.nativeEvent.layoutMeasurement.width;
    const roundIndex = Math.round(event.nativeEvent.contentOffset.x / slideSize);
    
    setActiveIndex(roundIndex);
  };

  const renderImageCarousel = () => {
    if (!post.images || post.images.length === 0) {
      return null;
    }

    return (
      <View>
        <FlatList
          ref={flatListRef}
          data={post.images}
          horizontal
          pagingEnabled
          showsHorizontalScrollIndicator={false}
          onScroll={handleScroll}
          renderItem={({ item }) => (
            <View style={styles.imageContainer}>
              <Image 
                source={{ uri: item }} 
                style={styles.postImage} 
                resizeMode="cover"
              />
            </View>
          )}
          keyExtractor={(_, index) => index.toString()}
        />
        {post.images.length > 1 && (
          <View style={styles.paginationContainer}>
            {post.images.map((_: any, index: number) => (
              <View
                key={index}
                style={[
                  styles.paginationDot,
                  activeIndex === index ? styles.activeDot : undefined,
                ]}
              />
            ))}
          </View>
        )}
      </View>
    );
  };

2.踩坑记录

在上传完成图片后,下次在刷新登录进入首页发现帖子的图片显示不出来了

分析代码后,我发现了问题所在。当上传图片并发布帖子时,图片的 URI 是以本地临时文件路径的形式存储的,这些路径在应用重启或刷新后可能会失效。

解决方案:将图片从临时位置复制到应用的永久存储区域

安装依赖:npm install expo-file-system

import * as FileSystem from 'expo-file-system';
import { Platform } from 'react-native';

// 创建永久存储图片的目录
const setupImageDirectory = async (): Promise<string> => {
  // 检查可用的目录常量
  let baseDir = './';
  
  const imageDir = `${baseDir}images/`;
  
  try {
    const dirInfo = await FileSystem.getInfoAsync(imageDir);
    if (!dirInfo.exists) {
      await FileSystem.makeDirectoryAsync(imageDir, { intermediates: true });
    }
  } catch (error) {
    console.error('创建图片目录失败:', error);
  }
  
  return imageDir;
};

// 将临时图片保存到永久存储
export const saveImageToPermanentStorage = async (uri: string): Promise<string> => {
  try {
    // 对于 web 平台,直接返回 URI,因为 web 平台的资源 URI 通常是网络 URL
    if (Platform.OS === 'web') {
      return uri;
    }
    
    const imageDir = await setupImageDirectory();
    const filename = `image_${Date.now()}_${Math.floor(Math.random() * 10000)}.jpg`;
    const destinationUri = `${imageDir}${filename}`;
    
    await FileSystem.copyAsync({
      from: uri,
      to: destinationUri
    });
    
    return destinationUri;
  } catch (error) {
    console.error('保存图片失败:', error);
    return uri; // 失败时返回原始 URI
  }
};

// 保存多张图片
export const saveMultipleImages = async (uris: string[]): Promise<string[]> => {
  const savedUris = await Promise.all(
    uris.map(uri => saveImageToPermanentStorage(uri))
  );
  return savedUris;
};

然后,修改 HomeScreen.tsx 中的发布帖子逻辑,在保存帖子前先保存图片:

// 在文件顶部导入新的图片存储工具
import { saveMultipleImages } from '../utils/imageStorage';

// 修改 handleGoPostEditor 函数
const handleGoPostEditor = () => {
  navigation.navigate('PostEditor', {
    onPublish: async (post: { title: string; text: string; images: string[] }) => {
      let permanentImages = post.images || [];
      
      // 如果有图片,先保存到永久存储
      if (post.images && post.images.length > 0) {
        permanentImages = await saveMultipleImages(post.images);
      }
      
      const newPost = {
        id: Date.now(),
        title: post.title || '新发布',
        summary: post.text || '',
        images: permanentImages  // 使用永久存储的图片路径
      };
      
      await addPost(newPost);
      setPosts(prev => [newPost, ...prev]);
    }
  });
};

这个解决方案的工作原理是:

  1. 当用户选择或拍摄图片时,获取到临时 URI
  2. 在保存帖子前,将图片从临时位置复制到应用的永久存储区域
  3. 使用永久存储的 URI 来保存帖子数据
  4. 这样,即使应用重启或刷新,图片也能正常显示