Files
frontend/src/components/common/ResponsiveGrid.tsx

169 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
*
*
*/
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;