213 lines
5.3 KiB
TypeScript
213 lines
5.3 KiB
TypeScript
|
|
/**
|
|||
|
|
* 响应式堆叠布局组件
|
|||
|
|
* 移动端垂直堆叠,平板/桌面端水平排列
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useMemo } from 'react';
|
|||
|
|
import {
|
|||
|
|
View,
|
|||
|
|
StyleProp,
|
|||
|
|
ViewStyle,
|
|||
|
|
StyleSheet,
|
|||
|
|
FlexAlignType,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import {
|
|||
|
|
useResponsive,
|
|||
|
|
useResponsiveSpacing,
|
|||
|
|
FineBreakpointKey,
|
|||
|
|
useBreakpointGTE,
|
|||
|
|
} from '../../hooks/useResponsive';
|
|||
|
|
|
|||
|
|
export type StackDirection = 'horizontal' | 'vertical' | 'responsive';
|
|||
|
|
export type StackAlignment = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|||
|
|
export type StackJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
|||
|
|
|
|||
|
|
export interface ResponsiveStackProps {
|
|||
|
|
/** 子元素 */
|
|||
|
|
children: React.ReactNode;
|
|||
|
|
/** 布局方向 */
|
|||
|
|
direction?: StackDirection;
|
|||
|
|
/** 切换为水平布局的断点(仅在 direction='responsive' 时有效) */
|
|||
|
|
horizontalBreakpoint?: FineBreakpointKey;
|
|||
|
|
/** 间距 */
|
|||
|
|
gap?: Partial<Record<FineBreakpointKey, number>> | number;
|
|||
|
|
/** 是否允许换行 */
|
|||
|
|
wrap?: boolean;
|
|||
|
|
/** 对齐方式(交叉轴) */
|
|||
|
|
align?: StackAlignment;
|
|||
|
|
/** 分布方式(主轴) */
|
|||
|
|
justify?: StackJustify;
|
|||
|
|
/** 自定义样式 */
|
|||
|
|
style?: StyleProp<ViewStyle>;
|
|||
|
|
/** 子元素样式 */
|
|||
|
|
itemStyle?: StyleProp<ViewStyle>;
|
|||
|
|
/** 是否反转顺序 */
|
|||
|
|
reverse?: boolean;
|
|||
|
|
/** 是否等分空间 */
|
|||
|
|
equalItem?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const alignMap: Record<StackAlignment, FlexAlignType> = {
|
|||
|
|
start: 'flex-start',
|
|||
|
|
center: 'center',
|
|||
|
|
end: 'flex-end',
|
|||
|
|
stretch: 'stretch',
|
|||
|
|
baseline: 'baseline',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const justifyMap: Record<StackJustify, 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'> = {
|
|||
|
|
start: 'flex-start',
|
|||
|
|
center: 'center',
|
|||
|
|
end: 'flex-end',
|
|||
|
|
between: 'space-between',
|
|||
|
|
around: 'space-around',
|
|||
|
|
evenly: 'space-evenly',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 响应式堆叠布局组件
|
|||
|
|
*
|
|||
|
|
* - 移动端垂直堆叠
|
|||
|
|
* - 平板/桌面端水平排列
|
|||
|
|
* - 支持间距和换行配置
|
|||
|
|
*
|
|||
|
|
* @example
|
|||
|
|
* // 基础用法 - 自动响应式
|
|||
|
|
* <ResponsiveStack>
|
|||
|
|
* <Item1 />
|
|||
|
|
* <Item2 />
|
|||
|
|
* <Item3 />
|
|||
|
|
* </ResponsiveStack>
|
|||
|
|
*
|
|||
|
|
* @example
|
|||
|
|
* // 自定义断点和间距
|
|||
|
|
* <ResponsiveStack
|
|||
|
|
* direction="responsive"
|
|||
|
|
* horizontalBreakpoint="md"
|
|||
|
|
* gap={{ xs: 8, md: 16, lg: 24 }}
|
|||
|
|
* align="center"
|
|||
|
|
* justify="between"
|
|||
|
|
* >
|
|||
|
|
* <Item1 />
|
|||
|
|
* <Item2 />
|
|||
|
|
* </ResponsiveStack>
|
|||
|
|
*
|
|||
|
|
* @example
|
|||
|
|
* // 固定水平方向
|
|||
|
|
* <ResponsiveStack direction="horizontal" wrap gap={16}>
|
|||
|
|
* {items.map(item => <Tag key={item.id} {...item} />)}
|
|||
|
|
* </ResponsiveStack>
|
|||
|
|
*/
|
|||
|
|
export function ResponsiveStack({
|
|||
|
|
children,
|
|||
|
|
direction = 'responsive',
|
|||
|
|
horizontalBreakpoint = 'lg',
|
|||
|
|
gap: gapConfig,
|
|||
|
|
wrap = false,
|
|||
|
|
align = 'stretch',
|
|||
|
|
justify = 'start',
|
|||
|
|
style,
|
|||
|
|
itemStyle,
|
|||
|
|
reverse = false,
|
|||
|
|
equalItem = false,
|
|||
|
|
}: ResponsiveStackProps) {
|
|||
|
|
const { isMobile, isTablet } = useResponsive();
|
|||
|
|
const isHorizontalBreakpoint = useBreakpointGTE(horizontalBreakpoint);
|
|||
|
|
|
|||
|
|
// 计算间距
|
|||
|
|
const gap = useMemo(() => {
|
|||
|
|
if (typeof gapConfig === 'number') {
|
|||
|
|
return gapConfig;
|
|||
|
|
}
|
|||
|
|
// 使用 hook 获取响应式间距
|
|||
|
|
return gapConfig;
|
|||
|
|
}, [gapConfig]);
|
|||
|
|
|
|||
|
|
const responsiveGap = useResponsiveSpacing(typeof gap === 'number' ? undefined : gap);
|
|||
|
|
const finalGap = typeof gap === 'number' ? gap : responsiveGap;
|
|||
|
|
|
|||
|
|
// 确定布局方向
|
|||
|
|
const isHorizontal = useMemo(() => {
|
|||
|
|
if (direction === 'horizontal') return true;
|
|||
|
|
if (direction === 'vertical') return false;
|
|||
|
|
// direction === 'responsive'
|
|||
|
|
return isHorizontalBreakpoint;
|
|||
|
|
}, [direction, isHorizontalBreakpoint]);
|
|||
|
|
|
|||
|
|
// 构建容器样式
|
|||
|
|
const containerStyle = useMemo((): ViewStyle => {
|
|||
|
|
const flexDirection = isHorizontal
|
|||
|
|
? (reverse ? 'row-reverse' : 'row')
|
|||
|
|
: (reverse ? 'column-reverse' : 'column');
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
flexDirection,
|
|||
|
|
flexWrap: wrap ? 'wrap' : 'nowrap',
|
|||
|
|
alignItems: alignMap[align],
|
|||
|
|
justifyContent: justifyMap[justify],
|
|||
|
|
gap: finalGap,
|
|||
|
|
};
|
|||
|
|
}, [isHorizontal, reverse, wrap, align, justify, finalGap]);
|
|||
|
|
|
|||
|
|
// 处理子元素
|
|||
|
|
const processedChildren = useMemo(() => {
|
|||
|
|
const childrenArray = React.Children.toArray(children);
|
|||
|
|
|
|||
|
|
return childrenArray.map((child, index) => {
|
|||
|
|
if (!React.isValidElement(child)) {
|
|||
|
|
return child;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const childStyle: ViewStyle = {};
|
|||
|
|
|
|||
|
|
if (equalItem) {
|
|||
|
|
childStyle.flex = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果不是最后一个元素,添加间距
|
|||
|
|
// 注意:使用 gap 后不需要手动添加 margin
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
key={child.key ?? `stack-item-${index}`}
|
|||
|
|
style={[equalItem && styles.equalItem, itemStyle, childStyle]}
|
|||
|
|
>
|
|||
|
|
{child}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
}, [children, equalItem, itemStyle]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={[styles.container, containerStyle, style]}>
|
|||
|
|
{processedChildren}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: {
|
|||
|
|
width: '100%',
|
|||
|
|
},
|
|||
|
|
equalItem: {
|
|||
|
|
flex: 1,
|
|||
|
|
minWidth: 0, // 防止 flex item 溢出
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 水平堆叠组件(快捷方式)
|
|||
|
|
*/
|
|||
|
|
export function HStack(props: Omit<ResponsiveStackProps, 'direction'>) {
|
|||
|
|
return <ResponsiveStack {...props} direction="horizontal" />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 垂直堆叠组件(快捷方式)
|
|||
|
|
*/
|
|||
|
|
export function VStack(props: Omit<ResponsiveStackProps, 'direction'>) {
|
|||
|
|
return <ResponsiveStack {...props} direction="vertical" />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default ResponsiveStack;
|