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:
402
src/components/common/AdaptiveLayout.tsx
Normal file
402
src/components/common/AdaptiveLayout.tsx
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* 自适应布局组件
|
||||
* 支持主内容区 + 侧边栏布局,根据屏幕宽度自动调整侧边栏显示/隐藏
|
||||
* 支持移动端抽屉式侧边栏
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
Pressable,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import {
|
||||
useResponsive,
|
||||
useBreakpointGTE,
|
||||
FineBreakpointKey,
|
||||
} from '../../hooks/useResponsive';
|
||||
|
||||
export interface AdaptiveLayoutProps {
|
||||
/** 主内容区 */
|
||||
children: React.ReactNode;
|
||||
/** 侧边栏内容 */
|
||||
sidebar?: React.ReactNode;
|
||||
/** 自定义头部 */
|
||||
header?: React.ReactNode;
|
||||
/** 自定义底部 */
|
||||
footer?: React.ReactNode;
|
||||
/** 布局样式 */
|
||||
style?: StyleProp<ViewStyle>;
|
||||
/** 主内容区样式 */
|
||||
contentStyle?: StyleProp<ViewStyle>;
|
||||
/** 侧边栏样式 */
|
||||
sidebarStyle?: StyleProp<ViewStyle>;
|
||||
/** 侧边栏宽度(桌面端) */
|
||||
sidebarWidth?: number;
|
||||
/** 移动端抽屉宽度 */
|
||||
drawerWidth?: number;
|
||||
/** 显示侧边栏的断点 */
|
||||
showSidebarBreakpoint?: FineBreakpointKey;
|
||||
/** 侧边栏位置 */
|
||||
sidebarPosition?: 'left' | 'right';
|
||||
/** 是否强制显示侧边栏(覆盖响应式逻辑) */
|
||||
forceShowSidebar?: boolean;
|
||||
/** 是否强制隐藏侧边栏(覆盖响应式逻辑) */
|
||||
forceHideSidebar?: boolean;
|
||||
/** 移动端抽屉是否打开(受控模式) */
|
||||
drawerOpen?: boolean;
|
||||
/** 移动端抽屉状态变化回调 */
|
||||
onDrawerOpenChange?: (open: boolean) => void;
|
||||
/** 渲染移动端抽屉触发按钮 */
|
||||
renderDrawerTrigger?: (props: { onPress: () => void; isOpen: boolean }) => React.ReactNode;
|
||||
/** 抽屉遮罩层颜色 */
|
||||
overlayColor?: string;
|
||||
/** 抽屉动画时长(毫秒) */
|
||||
animationDuration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应布局组件
|
||||
*
|
||||
* 支持主内容区 + 侧边栏布局,根据屏幕宽度自动调整:
|
||||
* - 桌面端:并排显示侧边栏
|
||||
* - 移动端:侧边栏变为抽屉式,通过按钮触发
|
||||
*
|
||||
* @example
|
||||
* // 基础用法
|
||||
* <AdaptiveLayout
|
||||
* sidebar={<SidebarContent />}
|
||||
* sidebarWidth={280}
|
||||
* >
|
||||
* <MainContent />
|
||||
* </AdaptiveLayout>
|
||||
*
|
||||
* @example
|
||||
* // 自定义断点和抽屉宽度
|
||||
* <AdaptiveLayout
|
||||
* sidebar={<SidebarContent />}
|
||||
* showSidebarBreakpoint="xl"
|
||||
* sidebarWidth={320}
|
||||
* drawerWidth={280}
|
||||
* sidebarPosition="right"
|
||||
* >
|
||||
* <MainContent />
|
||||
* </AdaptiveLayout>
|
||||
*
|
||||
* @example
|
||||
* // 受控模式
|
||||
* const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
*
|
||||
* <AdaptiveLayout
|
||||
* sidebar={<SidebarContent />}
|
||||
* drawerOpen={drawerOpen}
|
||||
* onDrawerOpenChange={setDrawerOpen}
|
||||
* renderDrawerTrigger={({ onPress, isOpen }) => (
|
||||
* <Button onPress={onPress}>
|
||||
* {isOpen ? '关闭' : '菜单'}
|
||||
* </Button>
|
||||
* )}
|
||||
* >
|
||||
* <MainContent />
|
||||
* </AdaptiveLayout>
|
||||
*/
|
||||
export function AdaptiveLayout({
|
||||
children,
|
||||
sidebar,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
contentStyle,
|
||||
sidebarStyle,
|
||||
sidebarWidth = 280,
|
||||
drawerWidth = 280,
|
||||
showSidebarBreakpoint = 'lg',
|
||||
sidebarPosition = 'left',
|
||||
forceShowSidebar,
|
||||
forceHideSidebar,
|
||||
drawerOpen: controlledDrawerOpen,
|
||||
onDrawerOpenChange,
|
||||
renderDrawerTrigger,
|
||||
overlayColor = 'rgba(0, 0, 0, 0.5)',
|
||||
animationDuration = 300,
|
||||
}: AdaptiveLayoutProps) {
|
||||
const { width, isMobile } = useResponsive();
|
||||
const shouldShowSidebar = useBreakpointGTE(showSidebarBreakpoint);
|
||||
|
||||
// 内部抽屉状态(非受控模式)
|
||||
const [internalDrawerOpen, setInternalDrawerOpen] = useState(false);
|
||||
|
||||
// 动画值
|
||||
const [slideAnim] = useState(new Animated.Value(0));
|
||||
const [fadeAnim] = useState(new Animated.Value(0));
|
||||
|
||||
// 确定最终抽屉状态
|
||||
const isDrawerOpen = controlledDrawerOpen ?? internalDrawerOpen;
|
||||
const setDrawerOpen = useCallback((open: boolean) => {
|
||||
if (onDrawerOpenChange) {
|
||||
onDrawerOpenChange(open);
|
||||
} else {
|
||||
setInternalDrawerOpen(open);
|
||||
}
|
||||
}, [onDrawerOpenChange]);
|
||||
|
||||
// 切换抽屉状态
|
||||
const toggleDrawer = useCallback(() => {
|
||||
setDrawerOpen(!isDrawerOpen);
|
||||
}, [isDrawerOpen, setDrawerOpen]);
|
||||
|
||||
// 关闭抽屉
|
||||
const closeDrawer = useCallback(() => {
|
||||
setDrawerOpen(false);
|
||||
}, [setDrawerOpen]);
|
||||
|
||||
// 抽屉动画
|
||||
useEffect(() => {
|
||||
if (isDrawerOpen) {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 1,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideAnim, {
|
||||
toValue: 0,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [isDrawerOpen, slideAnim, fadeAnim, animationDuration]);
|
||||
|
||||
// 计算侧边栏是否应该显示
|
||||
const isSidebarVisible = forceShowSidebar ?? (shouldShowSidebar && !forceHideSidebar);
|
||||
const isDrawerMode = !isSidebarVisible && !!sidebar;
|
||||
|
||||
// 抽屉滑动动画
|
||||
const drawerTranslateX = slideAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [
|
||||
sidebarPosition === 'left' ? -drawerWidth : drawerWidth,
|
||||
0,
|
||||
],
|
||||
});
|
||||
|
||||
// 渲染桌面端侧边栏
|
||||
const renderDesktopSidebar = () => {
|
||||
if (!sidebar || !isSidebarVisible) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.sidebar,
|
||||
{
|
||||
width: sidebarWidth,
|
||||
[sidebarPosition === 'left' ? 'marginRight' : 'marginLeft']: 16,
|
||||
},
|
||||
sidebarStyle,
|
||||
]}
|
||||
>
|
||||
{sidebar}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染移动端抽屉
|
||||
const renderMobileDrawer = () => {
|
||||
if (!sidebar || !isDrawerMode) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isDrawerOpen}
|
||||
transparent
|
||||
animationType="none"
|
||||
onRequestClose={closeDrawer}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
{/* 遮罩层 */}
|
||||
<TouchableOpacity
|
||||
style={styles.overlayTouchable}
|
||||
activeOpacity={1}
|
||||
onPress={closeDrawer}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{ backgroundColor: overlayColor, opacity: fadeAnim },
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 抽屉内容 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.drawer,
|
||||
{
|
||||
width: drawerWidth,
|
||||
[sidebarPosition]: 0,
|
||||
transform: [{ translateX: drawerTranslateX }],
|
||||
},
|
||||
sidebarStyle,
|
||||
]}
|
||||
>
|
||||
{sidebar}
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染抽屉触发按钮
|
||||
const renderTrigger = () => {
|
||||
if (!isDrawerMode || !renderDrawerTrigger) return null;
|
||||
|
||||
return renderDrawerTrigger({
|
||||
onPress: toggleDrawer,
|
||||
isOpen: isDrawerOpen,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{/* 头部 */}
|
||||
{header && (
|
||||
<View style={styles.header}>
|
||||
{header}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 主布局区域 */}
|
||||
<View style={styles.main}>
|
||||
{/* 左侧布局 */}
|
||||
{sidebarPosition === 'left' && renderDesktopSidebar()}
|
||||
|
||||
{/* 主内容区 */}
|
||||
<View style={[styles.content, contentStyle]}>
|
||||
{/* 抽屉触发按钮(仅在移动端抽屉模式显示) */}
|
||||
{renderTrigger()}
|
||||
{children}
|
||||
</View>
|
||||
|
||||
{/* 右侧布局 */}
|
||||
{sidebarPosition === 'right' && renderDesktopSidebar()}
|
||||
</View>
|
||||
|
||||
{/* 移动端抽屉 */}
|
||||
{renderMobileDrawer()}
|
||||
|
||||
{/* 底部 */}
|
||||
{footer && (
|
||||
<View style={styles.footer}>
|
||||
{footer}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
header: {
|
||||
width: '100%',
|
||||
},
|
||||
main: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
minWidth: 0, // 防止 flex item 溢出
|
||||
},
|
||||
sidebar: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
footer: {
|
||||
width: '100%',
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
overlayTouchable: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
},
|
||||
drawer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 8,
|
||||
elevation: 16,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 简化的侧边栏布局组件
|
||||
* 仅包含主内容和侧边栏,无头部底部
|
||||
*/
|
||||
export interface SidebarLayoutProps {
|
||||
children: React.ReactNode;
|
||||
sidebar: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
contentStyle?: StyleProp<ViewStyle>;
|
||||
sidebarStyle?: StyleProp<ViewStyle>;
|
||||
sidebarWidth?: number;
|
||||
showSidebarBreakpoint?: FineBreakpointKey;
|
||||
sidebarPosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export function SidebarLayout({
|
||||
children,
|
||||
sidebar,
|
||||
style,
|
||||
contentStyle,
|
||||
sidebarStyle,
|
||||
sidebarWidth = 280,
|
||||
showSidebarBreakpoint = 'lg',
|
||||
sidebarPosition = 'left',
|
||||
}: SidebarLayoutProps) {
|
||||
return (
|
||||
<AdaptiveLayout
|
||||
sidebar={sidebar}
|
||||
style={style}
|
||||
contentStyle={contentStyle}
|
||||
sidebarStyle={sidebarStyle}
|
||||
sidebarWidth={sidebarWidth}
|
||||
showSidebarBreakpoint={showSidebarBreakpoint}
|
||||
sidebarPosition={sidebarPosition}
|
||||
>
|
||||
{children}
|
||||
</AdaptiveLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdaptiveLayout;
|
||||
Reference in New Issue
Block a user