2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 自适应布局组件
|
|
|
|
|
|
* 支持主内容区 + 侧边栏布局,根据屏幕宽度自动调整侧边栏显示/隐藏
|
|
|
|
|
|
* 支持移动端抽屉式侧边栏
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
StyleProp,
|
|
|
|
|
|
ViewStyle,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
Animated,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Pressable,
|
|
|
|
|
|
Dimensions,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
Platform,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} from 'react-native';
|
|
|
|
|
|
import {
|
|
|
|
|
|
useResponsive,
|
|
|
|
|
|
useBreakpointGTE,
|
|
|
|
|
|
FineBreakpointKey,
|
|
|
|
|
|
} from '../../hooks/useResponsive';
|
2026-04-11 22:35:11 +08:00
|
|
|
|
import { blurActiveElement } from '../../infrastructure/platform';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-04-11 22:35:11 +08:00
|
|
|
|
if (isDrawerOpen) {
|
|
|
|
|
|
blurActiveElement();
|
2026-04-04 08:01:45 +08:00
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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;
|