Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
168
src/components/common/ResponsiveGrid.tsx
Normal file
168
src/components/common/ResponsiveGrid.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 响应式网格布局组件
|
||||
* 根据断点自动调整列数,支持间距配置
|
||||
*/
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user