Merge remote-tracking branch 'origin/master' into feature/adap-pc

This commit is contained in:
2026-03-16 19:27:05 +08:00
10 changed files with 679 additions and 79 deletions

View File

@@ -20,6 +20,7 @@ import Avatar from '../common/Avatar';
import { ImageGrid, ImageGridItem, SmartImage } from '../common';
import VotePreview from './VotePreview';
import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive';
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
interface PostCardProps {
post: Post;
@@ -206,6 +207,8 @@ const PostCard: React.FC<PostCardProps> = ({
id: img.id,
url: img.url,
thumbnail_url: img.thumbnail_url,
preview_url: img.preview_url,
preview_url_large: img.preview_url_large,
width: img.width,
height: img.height,
}));
@@ -224,6 +227,8 @@ const PostCard: React.FC<PostCardProps> = ({
borderRadius={imageBorderRadius}
showMoreOverlay={true}
onImagePress={onImagePress}
displayMode={variant === 'grid' ? 'grid' : 'list'}
usePreview={true}
/>
</View>
);
@@ -335,6 +340,11 @@ const PostCard: React.FC<PostCardProps> = ({
const aspectRatios = [0.7, 0.75, 0.8, 0.85, 0.9, 1, 1.1, 1.2];
const aspectRatio = aspectRatios[hash % aspectRatios.length];
// 获取封面图预览 URL
const coverUrl = hasImage
? getPreviewImageUrl(coverImage, 'grid')
: '';
// 获取正文预览(无图时显示)
const getContentPreview = (content: string | undefined | null): string => {
if (!content) return '';
@@ -369,9 +379,10 @@ const PostCard: React.FC<PostCardProps> = ({
onPress={() => onImagePress?.(post.images || [], 0)}
>
<SmartImage
source={{ uri: coverImage.thumbnail_url || coverImage.url || '' }}
source={{ uri: coverUrl }}
style={[styles.gridCoverImage, { aspectRatio }]}
resizeMode="cover"
usePreview={true}
/>
</TouchableOpacity>
)}

View File

@@ -2,6 +2,7 @@
* ImageGrid 图片网格组件
* 支持 1-9 张图片的智能布局
* 根据图片数量自动选择最佳展示方式
* 支持预览图优化
*/
import React, { useMemo, useCallback, useState, useEffect } from 'react';
@@ -16,6 +17,7 @@ import {
} from 'react-native';
import { SmartImage, ImageSource } from './SmartImage';
import { colors, spacing, borderRadius } from '../../theme';
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
@@ -69,6 +71,10 @@ export interface ImageGridProps {
onImagePress?: (images: ImageGridItem[], index: number) => void;
/** 测试ID */
testID?: string;
/** 显示模式 */
displayMode?: ImageDisplayMode;
/** 是否使用预览图 */
usePreview?: boolean;
}
/**
@@ -201,6 +207,7 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
/**
* 图片网格组件
* 智能布局:根据图片数量自动选择最佳展示方式
* 支持预览图优化
*/
export const ImageGrid: React.FC<ImageGridProps> = ({
images,
@@ -214,6 +221,8 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
gridColumns = 3,
onImagePress,
testID,
displayMode = 'list',
usePreview = true,
}) => {
// 通过 onLayout 拿到容器实际宽度
const [containerWidth, setContainerWidth] = useState(0);
@@ -256,6 +265,12 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
const renderSingleImage = () => {
const image = displayImages[0];
if (!image) return null;
// 获取预览图 URL
const previewUrl = usePreview && displayMode
? getPreviewImageUrl(image as any, displayMode)
: (image.uri || image.url || '');
return (
<SingleImageItem
image={image}
@@ -271,27 +286,33 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
const renderHorizontal = () => {
return (
<View style={[styles.horizontalContainer, { gap }]}>
{displayImages.map((image, index) => (
<Pressable
key={image.id || index}
onPress={() => handleImagePress(index)}
style={[
styles.horizontalItem,
{
flex: 1,
aspectRatio: 1,
borderRadius: borderRadiusValue,
},
]}
>
<SmartImage
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
style={styles.fullSize}
resizeMode="cover"
borderRadius={borderRadiusValue}
/>
</Pressable>
))}
{displayImages.map((image, index) => {
const previewUrl = usePreview && displayMode
? getPreviewImageUrl(image as any, displayMode)
: (image.uri || image.url || '');
return (
<Pressable
key={image.id || index}
onPress={() => handleImagePress(index)}
style={[
styles.horizontalItem,
{
flex: 1,
aspectRatio: 1,
borderRadius: borderRadiusValue,
},
]}
>
<SmartImage
source={{ uri: previewUrl }}
style={styles.fullSize}
resizeMode="cover"
borderRadius={borderRadiusValue}
/>
</Pressable>
);
})}
</View>
);
};
@@ -303,6 +324,10 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
{displayImages.map((image, index) => {
const isLastVisible = index === displayImages.length - 1;
const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay;
const previewUrl = usePreview && displayMode
? getPreviewImageUrl(image as any, displayMode)
: (image.uri || image.url || '');
return (
<Pressable
@@ -317,7 +342,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
]}
>
<SmartImage
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
source={{ uri: previewUrl }}
style={styles.fullSize}
resizeMode="cover"
borderRadius={borderRadiusValue}
@@ -361,6 +386,10 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
? image.width / image.height
: 1;
const height = itemWidth / aspectRatio;
const previewUrl = usePreview && displayMode
? getPreviewImageUrl(image as any, displayMode)
: (image.uri || image.url || '');
return (
<Pressable
@@ -376,7 +405,7 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
]}
>
<SmartImage
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
source={{ uri: previewUrl }}
style={styles.fullSize}
resizeMode="cover"
borderRadius={borderRadiusValue}

View File

@@ -4,7 +4,7 @@
* 基于 expo-image 封装,原生支持 GIF/WebP 动图
*/
import React, { useState, useCallback, useRef } from 'react';
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
View,
StyleSheet,
@@ -55,6 +55,14 @@ export interface SmartImageProps {
onError?: (error: any) => void;
/** 测试ID */
testID?: string;
/** 预览图 URL */
previewUrl?: string;
/** 是否使用预览图(优先加载预览图) */
usePreview?: boolean;
/** 是否懒加载 */
lazyLoad?: boolean;
/** 缓存策略 */
cachePolicy?: 'memory' | 'disk' | 'memory-disk' | 'none';
}
/**
@@ -74,14 +82,33 @@ export const SmartImage: React.FC<SmartImageProps> = ({
onLoad,
onError,
testID,
previewUrl,
usePreview = false,
lazyLoad = false,
cachePolicy = 'memory-disk',
}) => {
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
const [isVisible, setIsVisible] = useState(!lazyLoad);
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
// 解析图片源 - 支持 uri 或 url 字段
const imageUri = typeof source === 'string'
? source
const imageUri = typeof source === 'string'
? source
: (source.uri || source.url || '');
// 解析预览图 URL
const previewUri = typeof previewUrl === 'string' ? previewUrl : '';
// 智能选择图片 URL
const getImageUrl = useCallback((): string => {
// 如果应该加载原图(预览图已加载完成或没有预览图)
if (shouldLoadOriginal || !usePreview || !previewUri) {
return imageUri;
}
// 否则使用预览图
return previewUri;
}, [imageUri, previewUri, usePreview, shouldLoadOriginal]);
// 处理加载开始
const handleLoadStart = useCallback(() => {
setLoadState('loading');
@@ -90,16 +117,25 @@ export const SmartImage: React.FC<SmartImageProps> = ({
// 处理加载完成
const handleLoad = useCallback(() => {
setLoadState('success');
// 如果正在加载预览图,切换到原图
if (usePreview && !shouldLoadOriginal && previewUri && getImageUrl() === previewUri) {
setShouldLoadOriginal(true);
}
onLoad?.();
}, [onLoad]);
}, [onLoad, usePreview, shouldLoadOriginal, previewUri, getImageUrl]);
// 处理加载错误
const handleError = useCallback(
(error: any) => {
// 如果预览图加载失败,尝试加载原图
if (usePreview && !shouldLoadOriginal && getImageUrl() === previewUri && imageUri) {
setShouldLoadOriginal(true);
return;
}
setLoadState('error');
onError?.(error);
},
[onError]
[onError, usePreview, shouldLoadOriginal, getImageUrl, previewUri, imageUri]
);
// 重试加载
@@ -144,7 +180,7 @@ export const SmartImage: React.FC<SmartImageProps> = ({
};
// 图片源配置
const imageSource = imageUri && imageUri.trim() !== '' ? { uri: imageUri } : undefined;
const imageSource = imageUri && imageUri.trim() !== '' ? { uri: getImageUrl() } : undefined;
// 如果没有有效的图片源,显示错误占位
if (!imageSource) {
@@ -164,6 +200,16 @@ export const SmartImage: React.FC<SmartImageProps> = ({
);
}
// 如果是懒加载且不可见,显示占位
if (lazyLoad && !isVisible) {
return (
<View
style={[containerStyle, style as ViewStyle, { backgroundColor: '#F5F5F5' }]}
testID={testID}
/>
);
}
return (
<Pressable
style={[containerStyle, style as ViewStyle]}
@@ -181,7 +227,7 @@ export const SmartImage: React.FC<SmartImageProps> = ({
resizeMode === 'center' ? 'scale-down' :
resizeMode
}
cachePolicy="memory-disk"
cachePolicy={cachePolicy}
onLoadStart={handleLoadStart}
onLoad={handleLoad}
onError={handleError}