169 lines
4.3 KiB
TypeScript
169 lines
4.3 KiB
TypeScript
|
|
/**
|
|||
|
|
* 响应式网格布局组件
|
|||
|
|
* 根据断点自动调整列数,支持间距配置
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useMemo } from 'react';
|
|||
|
|
import {
|
|||
|
|
View,
|
|||
|
|
StyleProp,
|
|||
|
|
ViewStyle,
|
|||
|
|
StyleSheet,
|
|||
|
|
Dimensions,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import {
|
|||
|
|
useResponsive,
|
|||
|
|
useColumnCount,
|
|||
|
|
useResponsiveSpacing,
|
|||
|
|
FineBreakpointKey,
|
|||
|
|
} from '../../hooks/useResponsive';
|
|||
|
|
|
|||
|
|
export interface ResponsiveGridProps {
|
|||
|
|
/** 子元素 */
|
|||
|
|
children: React.ReactNode[];
|
|||
|
|
/** 自定义样式 */
|
|||
|
|
style?: StyleProp<ViewStyle>;
|
|||
|
|
/** 容器样式 */
|
|||
|
|
containerStyle?: StyleProp<ViewStyle>;
|
|||
|
|
/** 项目样式 */
|
|||
|
|
itemStyle?: StyleProp<ViewStyle>;
|
|||
|
|
/** 列数配置,根据断点设置 */
|
|||
|
|
columns?: Partial<Record<FineBreakpointKey, number>>;
|
|||
|
|
/** 间距配置 */
|
|||
|
|
gap?: Partial<Record<FineBreakpointKey, number>>;
|
|||
|
|
/** 行间距(默认等于 gap) */
|
|||
|
|
rowGap?: Partial<Record<FineBreakpointKey, number>>;
|
|||
|
|
/** 列间距(默认等于 gap) */
|
|||
|
|
columnGap?: Partial<Record<FineBreakpointKey, number>>;
|
|||
|
|
/** 是否启用等宽列 */
|
|||
|
|
equalColumns?: boolean;
|
|||
|
|
/** 自定义列宽计算函数 */
|
|||
|
|
getColumnWidth?: (containerWidth: number, columns: number, gap: number) => number;
|
|||
|
|
/** 渲染空状态 */
|
|||
|
|
renderEmpty?: () => React.ReactNode;
|
|||
|
|
/** key 提取函数 */
|
|||
|
|
keyExtractor?: (item: React.ReactNode, index: number) => string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 响应式网格布局组件
|
|||
|
|
*
|
|||
|
|
* 根据屏幕宽度自动调整列数,支持平板/桌面端的多列布局
|
|||
|
|
*
|
|||
|
|
* @example
|
|||
|
|
* <ResponsiveGrid
|
|||
|
|
* columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
|
|||
|
|
* gap={{ xs: 8, md: 16 }}
|
|||
|
|
* >
|
|||
|
|
* {items.map(item => <Card key={item.id} {...item} />)}
|
|||
|
|
* </ResponsiveGrid>
|
|||
|
|
*/
|
|||
|
|
export function ResponsiveGrid({
|
|||
|
|
children,
|
|||
|
|
style,
|
|||
|
|
containerStyle,
|
|||
|
|
itemStyle,
|
|||
|
|
columns: columnsConfig,
|
|||
|
|
gap: gapConfig,
|
|||
|
|
rowGap: rowGapConfig,
|
|||
|
|
columnGap: columnGapConfig,
|
|||
|
|
equalColumns = true,
|
|||
|
|
getColumnWidth,
|
|||
|
|
renderEmpty,
|
|||
|
|
keyExtractor,
|
|||
|
|
}: ResponsiveGridProps) {
|
|||
|
|
const { width } = useResponsive();
|
|||
|
|
const columns = useColumnCount(columnsConfig);
|
|||
|
|
const defaultGap = useResponsiveSpacing(gapConfig);
|
|||
|
|
const rowGap = useResponsiveSpacing(rowGapConfig ?? gapConfig);
|
|||
|
|
const columnGap = useResponsiveSpacing(columnGapConfig ?? gapConfig);
|
|||
|
|
|
|||
|
|
// 计算列宽
|
|||
|
|
const columnWidth = useMemo(() => {
|
|||
|
|
if (getColumnWidth) {
|
|||
|
|
return getColumnWidth(width, columns, columnGap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (equalColumns) {
|
|||
|
|
// 等宽列计算:(总宽度 - (列数 - 1) * 列间距) / 列数
|
|||
|
|
return (width - (columns - 1) * columnGap) / columns;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return undefined;
|
|||
|
|
}, [width, columns, columnGap, equalColumns, getColumnWidth]);
|
|||
|
|
|
|||
|
|
// 将子元素分组为行
|
|||
|
|
const rows = useMemo(() => {
|
|||
|
|
const items = React.Children.toArray(children);
|
|||
|
|
if (items.length === 0) return [];
|
|||
|
|
|
|||
|
|
const result: React.ReactNode[][] = [];
|
|||
|
|
for (let i = 0; i < items.length; i += columns) {
|
|||
|
|
result.push(items.slice(i, i + columns));
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}, [children, columns]);
|
|||
|
|
|
|||
|
|
// 空状态处理
|
|||
|
|
if (rows.length === 0 && renderEmpty) {
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.container, containerStyle]}>
|
|||
|
|
{renderEmpty()}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.container, containerStyle]}>
|
|||
|
|
{rows.map((row, rowIndex) => (
|
|||
|
|
<View
|
|||
|
|
key={`row-${rowIndex}`}
|
|||
|
|
style={[
|
|||
|
|
styles.row,
|
|||
|
|
{
|
|||
|
|
marginBottom: rowIndex < rows.length - 1 ? rowGap : 0,
|
|||
|
|
},
|
|||
|
|
style,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{row.map((child, colIndex) => {
|
|||
|
|
const index = rowIndex * columns + colIndex;
|
|||
|
|
const key = keyExtractor?.(child, index) ?? `grid-item-${index}`;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
key={key}
|
|||
|
|
style={[
|
|||
|
|
styles.item,
|
|||
|
|
{
|
|||
|
|
width: columnWidth,
|
|||
|
|
marginRight: colIndex < row.length - 1 ? columnGap : 0,
|
|||
|
|
},
|
|||
|
|
itemStyle,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{child}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: {
|
|||
|
|
width: '100%',
|
|||
|
|
},
|
|||
|
|
row: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'flex-start',
|
|||
|
|
},
|
|||
|
|
item: {
|
|||
|
|
flexShrink: 0,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export default ResponsiveGrid;
|