599 lines
16 KiB
TypeScript
599 lines
16 KiB
TypeScript
|
|
/**
|
|||
|
|
* ImageGrid 图片网格组件
|
|||
|
|
* 支持 1-9 张图片的智能布局
|
|||
|
|
* 根据图片数量自动选择最佳展示方式
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
|||
|
|
import {
|
|||
|
|
View,
|
|||
|
|
StyleSheet,
|
|||
|
|
Dimensions,
|
|||
|
|
ViewStyle,
|
|||
|
|
Pressable,
|
|||
|
|
Text,
|
|||
|
|
Image,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import { SmartImage, ImageSource } from './SmartImage';
|
|||
|
|
import { colors, spacing, borderRadius } from '../../theme';
|
|||
|
|
|
|||
|
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|||
|
|
|
|||
|
|
// 默认容器内边距(用于计算可用宽度)
|
|||
|
|
const DEFAULT_CONTAINER_PADDING = spacing.lg * 2; // 左右各 spacing.lg
|
|||
|
|
|
|||
|
|
// 单张图片的默认宽高比
|
|||
|
|
const SINGLE_IMAGE_DEFAULT_ASPECT_RATIO = 4 / 3; // 默认4:3比例
|
|||
|
|
|
|||
|
|
// 单张图片的最大高度
|
|||
|
|
const SINGLE_IMAGE_MAX_HEIGHT = 400;
|
|||
|
|
|
|||
|
|
// 单张图片的最小高度
|
|||
|
|
const SINGLE_IMAGE_MIN_HEIGHT = 150;
|
|||
|
|
|
|||
|
|
// 图片项类型 - 兼容 PostImageDTO 和 CommentImage
|
|||
|
|
export interface ImageGridItem {
|
|||
|
|
id?: string;
|
|||
|
|
uri?: string;
|
|||
|
|
url?: string;
|
|||
|
|
thumbnailUrl?: string;
|
|||
|
|
thumbnail_url?: string;
|
|||
|
|
width?: number;
|
|||
|
|
height?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 布局模式
|
|||
|
|
export type GridLayoutMode = 'auto' | 'single' | 'horizontal' | 'grid' | 'masonry';
|
|||
|
|
|
|||
|
|
// 组件 Props
|
|||
|
|
export interface ImageGridProps {
|
|||
|
|
/** 图片列表 */
|
|||
|
|
images: ImageGridItem[];
|
|||
|
|
/** 容器样式 */
|
|||
|
|
style?: ViewStyle;
|
|||
|
|
/** 最大显示数量 */
|
|||
|
|
maxDisplayCount?: number;
|
|||
|
|
/** 布局模式 */
|
|||
|
|
mode?: GridLayoutMode;
|
|||
|
|
/** 图片间距 */
|
|||
|
|
gap?: number;
|
|||
|
|
/** 圆角大小 */
|
|||
|
|
borderRadius?: number;
|
|||
|
|
/** 是否显示更多遮罩 */
|
|||
|
|
showMoreOverlay?: boolean;
|
|||
|
|
/** 单张图片最大高度 */
|
|||
|
|
singleImageMaxHeight?: number;
|
|||
|
|
/** 网格列数(2或3) */
|
|||
|
|
gridColumns?: 2 | 3;
|
|||
|
|
/** 图片点击回调 */
|
|||
|
|
onImagePress?: (images: ImageGridItem[], index: number) => void;
|
|||
|
|
/** 测试ID */
|
|||
|
|
testID?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算图片网格尺寸
|
|||
|
|
*/
|
|||
|
|
const calculateGridDimensions = (
|
|||
|
|
count: number,
|
|||
|
|
containerWidth: number,
|
|||
|
|
gap: number,
|
|||
|
|
columns: number
|
|||
|
|
) => {
|
|||
|
|
const totalGap = (columns - 1) * gap;
|
|||
|
|
const itemSize = (containerWidth - totalGap) / columns;
|
|||
|
|
|
|||
|
|
// 计算行数
|
|||
|
|
const rows = Math.ceil(count / columns);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
itemSize,
|
|||
|
|
rows,
|
|||
|
|
containerHeight: rows * itemSize + (rows - 1) * gap,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ─── 单张图片子组件 ───────────────────────────────────────────────────────────
|
|||
|
|
// 独立成组件,方便用 useState 管理异步加载到的实际尺寸
|
|||
|
|
|
|||
|
|
interface SingleImageItemProps {
|
|||
|
|
image: ImageGridItem;
|
|||
|
|
containerWidth: number;
|
|||
|
|
maxHeight: number;
|
|||
|
|
borderRadiusValue: number;
|
|||
|
|
onPress: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||
|
|
image,
|
|||
|
|
containerWidth,
|
|||
|
|
maxHeight,
|
|||
|
|
borderRadiusValue,
|
|||
|
|
onPress,
|
|||
|
|
}) => {
|
|||
|
|
const [aspectRatio, setAspectRatio] = useState<number | null>(null);
|
|||
|
|
const uri = image.uri || image.url || '';
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!uri) return;
|
|||
|
|
let cancelled = false;
|
|||
|
|
|
|||
|
|
// 添加超时处理,防止高分辨率图片加载卡住
|
|||
|
|
const timeoutId = setTimeout(() => {
|
|||
|
|
if (!cancelled && aspectRatio === null) {
|
|||
|
|
setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO);
|
|||
|
|
}
|
|||
|
|
}, 3000);
|
|||
|
|
|
|||
|
|
Image.getSize(
|
|||
|
|
uri,
|
|||
|
|
(w, h) => {
|
|||
|
|
if (!cancelled) {
|
|||
|
|
clearTimeout(timeoutId);
|
|||
|
|
setAspectRatio(w / h);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
() => {
|
|||
|
|
if (!cancelled) {
|
|||
|
|
clearTimeout(timeoutId);
|
|||
|
|
setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
return () => {
|
|||
|
|
cancelled = true;
|
|||
|
|
clearTimeout(timeoutId);
|
|||
|
|
};
|
|||
|
|
}, [uri]);
|
|||
|
|
|
|||
|
|
const effectiveContainerWidth = containerWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING;
|
|||
|
|
|
|||
|
|
// 尺寸还没拿到时先占位,避免闪烁
|
|||
|
|
if (aspectRatio === null) {
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
style={[
|
|||
|
|
styles.singleContainer,
|
|||
|
|
{
|
|||
|
|
width: effectiveContainerWidth,
|
|||
|
|
height: SINGLE_IMAGE_MIN_HEIGHT,
|
|||
|
|
borderRadius: borderRadiusValue,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 适配最大边界框,保证宽高比不变
|
|||
|
|
let width: number;
|
|||
|
|
let height: number;
|
|||
|
|
if (aspectRatio > effectiveContainerWidth / maxHeight) {
|
|||
|
|
// 宽图:宽度撑满容器
|
|||
|
|
width = effectiveContainerWidth;
|
|||
|
|
height = effectiveContainerWidth / aspectRatio;
|
|||
|
|
} else {
|
|||
|
|
// 高图:高度触及上限
|
|||
|
|
height = maxHeight;
|
|||
|
|
width = maxHeight * aspectRatio;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 最小高度兜底
|
|||
|
|
if (height < SINGLE_IMAGE_MIN_HEIGHT) {
|
|||
|
|
height = SINGLE_IMAGE_MIN_HEIGHT;
|
|||
|
|
width = Math.min(SINGLE_IMAGE_MIN_HEIGHT * aspectRatio, effectiveContainerWidth);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Pressable
|
|||
|
|
onPress={onPress}
|
|||
|
|
style={[styles.singleContainer, { width, height, borderRadius: borderRadiusValue }]}
|
|||
|
|
>
|
|||
|
|
<SmartImage
|
|||
|
|
source={{ uri }}
|
|||
|
|
style={styles.fullSize}
|
|||
|
|
resizeMode="cover"
|
|||
|
|
borderRadius={borderRadiusValue}
|
|||
|
|
/>
|
|||
|
|
</Pressable>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 图片网格组件
|
|||
|
|
* 智能布局:根据图片数量自动选择最佳展示方式
|
|||
|
|
*/
|
|||
|
|
export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||
|
|
images,
|
|||
|
|
style,
|
|||
|
|
maxDisplayCount = 9,
|
|||
|
|
mode = 'auto',
|
|||
|
|
gap = 4,
|
|||
|
|
borderRadius: borderRadiusValue = borderRadius.md,
|
|||
|
|
showMoreOverlay = true,
|
|||
|
|
singleImageMaxHeight = 300,
|
|||
|
|
gridColumns = 3,
|
|||
|
|
onImagePress,
|
|||
|
|
testID,
|
|||
|
|
}) => {
|
|||
|
|
// 通过 onLayout 拿到容器实际宽度
|
|||
|
|
const [containerWidth, setContainerWidth] = useState(0);
|
|||
|
|
|
|||
|
|
// 过滤有效图片 - 支持 uri 或 url 字段
|
|||
|
|
const validImages = useMemo(() => {
|
|||
|
|
const filtered = images.filter(img => img.uri || img.url || typeof img === 'string');
|
|||
|
|
return filtered;
|
|||
|
|
}, [images]);
|
|||
|
|
|
|||
|
|
// 实际显示的图片
|
|||
|
|
const displayImages = useMemo(() => {
|
|||
|
|
return validImages.slice(0, maxDisplayCount);
|
|||
|
|
}, [validImages, maxDisplayCount]);
|
|||
|
|
|
|||
|
|
// 剩余图片数量
|
|||
|
|
const remainingCount = useMemo(() => {
|
|||
|
|
return Math.max(0, validImages.length - maxDisplayCount);
|
|||
|
|
}, [validImages, maxDisplayCount]);
|
|||
|
|
|
|||
|
|
// 处理图片点击
|
|||
|
|
const handleImagePress = useCallback(
|
|||
|
|
(index: number) => {
|
|||
|
|
onImagePress?.(validImages, index);
|
|||
|
|
},
|
|||
|
|
[onImagePress, validImages]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 确定布局模式
|
|||
|
|
const layoutMode = useMemo(() => {
|
|||
|
|
if (mode !== 'auto') return mode;
|
|||
|
|
|
|||
|
|
const count = displayImages.length;
|
|||
|
|
if (count === 1) return 'single';
|
|||
|
|
if (count === 2) return 'horizontal';
|
|||
|
|
return 'grid';
|
|||
|
|
}, [mode, displayImages.length]);
|
|||
|
|
|
|||
|
|
// 渲染单张图片
|
|||
|
|
const renderSingleImage = () => {
|
|||
|
|
const image = displayImages[0];
|
|||
|
|
if (!image) return null;
|
|||
|
|
return (
|
|||
|
|
<SingleImageItem
|
|||
|
|
image={image}
|
|||
|
|
containerWidth={containerWidth}
|
|||
|
|
maxHeight={singleImageMaxHeight}
|
|||
|
|
borderRadiusValue={borderRadiusValue}
|
|||
|
|
onPress={() => handleImagePress(0)}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染横向双图
|
|||
|
|
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>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染网格布局
|
|||
|
|
const renderGrid = () => {
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.gridContainer, { gap }]}>
|
|||
|
|
{displayImages.map((image, index) => {
|
|||
|
|
const isLastVisible = index === displayImages.length - 1;
|
|||
|
|
const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Pressable
|
|||
|
|
key={image.id || index}
|
|||
|
|
onPress={() => handleImagePress(index)}
|
|||
|
|
style={[
|
|||
|
|
styles.gridItem,
|
|||
|
|
gridColumns === 3 ? styles.gridItem3 : styles.gridItem2,
|
|||
|
|
{
|
|||
|
|
borderRadius: borderRadiusValue,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<SmartImage
|
|||
|
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
|||
|
|
style={styles.fullSize}
|
|||
|
|
resizeMode="cover"
|
|||
|
|
borderRadius={borderRadiusValue}
|
|||
|
|
/>
|
|||
|
|
{showOverlay && (
|
|||
|
|
<View style={styles.moreOverlay}>
|
|||
|
|
<Text style={styles.moreText}>+{remainingCount}</Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
</Pressable>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染瀑布流布局
|
|||
|
|
const renderMasonry = () => {
|
|||
|
|
const containerWidth = SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING;
|
|||
|
|
const columns = 2;
|
|||
|
|
const itemWidth = (containerWidth - gap) / columns;
|
|||
|
|
|
|||
|
|
// 将图片分配到两列
|
|||
|
|
const leftColumn: ImageGridItem[] = [];
|
|||
|
|
const rightColumn: ImageGridItem[] = [];
|
|||
|
|
|
|||
|
|
displayImages.forEach((image, index) => {
|
|||
|
|
if (index % 2 === 0) {
|
|||
|
|
leftColumn.push(image);
|
|||
|
|
} else {
|
|||
|
|
rightColumn.push(image);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const renderColumn = (columnImages: ImageGridItem[], columnIndex: number) => {
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.masonryColumn, { gap }]}>
|
|||
|
|
{columnImages.map((image, index) => {
|
|||
|
|
const actualIndex = columnIndex + index * 2;
|
|||
|
|
const aspectRatio = image.width && image.height
|
|||
|
|
? image.width / image.height
|
|||
|
|
: 1;
|
|||
|
|
const height = itemWidth / aspectRatio;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Pressable
|
|||
|
|
key={image.id || actualIndex}
|
|||
|
|
onPress={() => handleImagePress(actualIndex)}
|
|||
|
|
style={[
|
|||
|
|
styles.masonryItem,
|
|||
|
|
{
|
|||
|
|
width: itemWidth,
|
|||
|
|
height: Math.max(height, itemWidth * 0.7),
|
|||
|
|
borderRadius: borderRadiusValue,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<SmartImage
|
|||
|
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
|||
|
|
style={styles.fullSize}
|
|||
|
|
resizeMode="cover"
|
|||
|
|
borderRadius={borderRadiusValue}
|
|||
|
|
/>
|
|||
|
|
</Pressable>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.masonryContainer, { gap }]}>
|
|||
|
|
{renderColumn(leftColumn, 0)}
|
|||
|
|
{renderColumn(rightColumn, 1)}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 根据布局模式渲染
|
|||
|
|
const renderContent = () => {
|
|||
|
|
switch (layoutMode) {
|
|||
|
|
case 'single':
|
|||
|
|
return renderSingleImage();
|
|||
|
|
case 'horizontal':
|
|||
|
|
return renderHorizontal();
|
|||
|
|
case 'masonry':
|
|||
|
|
return renderMasonry();
|
|||
|
|
case 'grid':
|
|||
|
|
default:
|
|||
|
|
return renderGrid();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果没有图片,返回null
|
|||
|
|
if (displayImages.length === 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
style={[styles.container, style]}
|
|||
|
|
testID={testID}
|
|||
|
|
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}
|
|||
|
|
>
|
|||
|
|
{renderContent()}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 紧凑模式 - 用于评论等小空间场景
|
|||
|
|
export interface CompactImageGridProps extends Omit<ImageGridProps, 'mode' | 'gridColumns'> {
|
|||
|
|
/** 最大尺寸限制 */
|
|||
|
|
maxWidth?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 紧凑图片网格
|
|||
|
|
* 适用于评论等空间有限的场景
|
|||
|
|
*/
|
|||
|
|
export const CompactImageGrid: React.FC<CompactImageGridProps> = ({
|
|||
|
|
maxWidth,
|
|||
|
|
gap = 4,
|
|||
|
|
borderRadius: borderRadiusValue = borderRadius.sm,
|
|||
|
|
...props
|
|||
|
|
}) => {
|
|||
|
|
const containerWidth = maxWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING - 36 - spacing.sm; // 36是头像宽度
|
|||
|
|
|
|||
|
|
const renderCompactGrid = () => {
|
|||
|
|
const { images } = props;
|
|||
|
|
const count = images.length;
|
|||
|
|
|
|||
|
|
if (count === 0) return null;
|
|||
|
|
|
|||
|
|
if (count === 1) {
|
|||
|
|
const image = images[0];
|
|||
|
|
const size = Math.min(containerWidth * 0.6, 150);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Pressable
|
|||
|
|
onPress={() => props.onImagePress?.(images, 0)}
|
|||
|
|
style={[
|
|||
|
|
styles.compactItem,
|
|||
|
|
{
|
|||
|
|
width: size,
|
|||
|
|
height: size,
|
|||
|
|
borderRadius: borderRadiusValue,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<SmartImage
|
|||
|
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
|||
|
|
style={styles.fullSize}
|
|||
|
|
resizeMode="cover"
|
|||
|
|
borderRadius={borderRadiusValue}
|
|||
|
|
/>
|
|||
|
|
</Pressable>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 多张图片使用小网格
|
|||
|
|
const columns = count <= 4 ? 2 : 3;
|
|||
|
|
const { itemSize } = calculateGridDimensions(count, containerWidth, gap, columns);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.compactGrid, { gap }]}>
|
|||
|
|
{images.slice(0, 6).map((image, index) => (
|
|||
|
|
<Pressable
|
|||
|
|
key={image.id || index}
|
|||
|
|
onPress={() => props.onImagePress?.(images, index)}
|
|||
|
|
style={[
|
|||
|
|
styles.compactItem,
|
|||
|
|
{
|
|||
|
|
width: itemSize,
|
|||
|
|
height: itemSize,
|
|||
|
|
borderRadius: borderRadiusValue,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<SmartImage
|
|||
|
|
source={{ uri: image.uri || image.url, width: image.width, height: image.height }}
|
|||
|
|
style={styles.fullSize}
|
|||
|
|
resizeMode="cover"
|
|||
|
|
borderRadius={borderRadiusValue}
|
|||
|
|
/>
|
|||
|
|
{index === 5 && images.length > 6 && (
|
|||
|
|
<View style={styles.moreOverlay}>
|
|||
|
|
<Text style={styles.moreText}>+{images.length - 6}</Text>
|
|||
|
|
</View>
|
|||
|
|
)}
|
|||
|
|
</Pressable>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return <View style={styles.compactContainer}>{renderCompactGrid()}</View>;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: {
|
|||
|
|
marginTop: spacing.sm,
|
|||
|
|
},
|
|||
|
|
fullSize: {
|
|||
|
|
flex: 1,
|
|||
|
|
width: '100%',
|
|||
|
|
height: '100%',
|
|||
|
|
},
|
|||
|
|
// 单图样式
|
|||
|
|
singleContainer: {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
backgroundColor: colors.background.disabled,
|
|||
|
|
},
|
|||
|
|
// 横向布局样式
|
|||
|
|
horizontalContainer: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
},
|
|||
|
|
horizontalItem: {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
backgroundColor: colors.background.disabled,
|
|||
|
|
},
|
|||
|
|
// 网格布局样式
|
|||
|
|
gridContainer: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
flexWrap: 'wrap',
|
|||
|
|
},
|
|||
|
|
gridItem: {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
backgroundColor: colors.background.disabled,
|
|||
|
|
aspectRatio: 1,
|
|||
|
|
},
|
|||
|
|
gridItem2: {
|
|||
|
|
width: '48%', // 2列布局,每列约48%宽度,留有余量避免换行
|
|||
|
|
},
|
|||
|
|
gridItem3: {
|
|||
|
|
width: '31%', // 3列布局,每列约31%宽度,留有余量避免换行
|
|||
|
|
},
|
|||
|
|
// 瀑布流样式
|
|||
|
|
masonryContainer: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
},
|
|||
|
|
masonryColumn: {
|
|||
|
|
flex: 1,
|
|||
|
|
},
|
|||
|
|
masonryItem: {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
backgroundColor: colors.background.disabled,
|
|||
|
|
},
|
|||
|
|
// 更多遮罩 - 类似微博的灰色蒙版
|
|||
|
|
moreOverlay: {
|
|||
|
|
...StyleSheet.absoluteFillObject,
|
|||
|
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
borderRadius: borderRadius.md,
|
|||
|
|
},
|
|||
|
|
moreText: {
|
|||
|
|
color: colors.text.inverse,
|
|||
|
|
fontSize: fontSizes.xl,
|
|||
|
|
fontWeight: '500',
|
|||
|
|
},
|
|||
|
|
// 紧凑模式样式
|
|||
|
|
compactContainer: {
|
|||
|
|
marginTop: spacing.xs,
|
|||
|
|
},
|
|||
|
|
compactGrid: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
flexWrap: 'wrap',
|
|||
|
|
},
|
|||
|
|
compactItem: {
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
backgroundColor: colors.background.disabled,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 导入字体大小
|
|||
|
|
import { fontSizes } from '../../theme';
|
|||
|
|
|
|||
|
|
export default ImageGrid;
|