Files
frontend/src/components/common/ResponsiveStack.tsx
lan 3968660048 Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
2026-03-09 21:29:03 +08:00

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;