/** * 自适应布局组件 * 支持主内容区 + 侧边栏布局,根据屏幕宽度自动调整侧边栏显示/隐藏 * 支持移动端抽屉式侧边栏 */ import React, { useState, useCallback, useEffect } from 'react'; import { View, StyleProp, ViewStyle, StyleSheet, Animated, TouchableOpacity, Modal, Pressable, Dimensions, Platform, } from 'react-native'; import { useResponsive, useBreakpointGTE, FineBreakpointKey, } from '../../hooks/useResponsive'; import { blurActiveElement } from '../../infrastructure/platform'; export interface AdaptiveLayoutProps { /** 主内容区 */ children: React.ReactNode; /** 侧边栏内容 */ sidebar?: React.ReactNode; /** 自定义头部 */ header?: React.ReactNode; /** 自定义底部 */ footer?: React.ReactNode; /** 布局样式 */ style?: StyleProp; /** 主内容区样式 */ contentStyle?: StyleProp; /** 侧边栏样式 */ sidebarStyle?: StyleProp; /** 侧边栏宽度(桌面端) */ 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 * // 基础用法 * } * sidebarWidth={280} * > * * * * @example * // 自定义断点和抽屉宽度 * } * showSidebarBreakpoint="xl" * sidebarWidth={320} * drawerWidth={280} * sidebarPosition="right" * > * * * * @example * // 受控模式 * const [drawerOpen, setDrawerOpen] = useState(false); * * } * drawerOpen={drawerOpen} * onDrawerOpenChange={setDrawerOpen} * renderDrawerTrigger={({ onPress, isOpen }) => ( * * )} * > * * */ 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) { if (isDrawerOpen) { blurActiveElement(); } 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 ( {sidebar} ); }; // 渲染移动端抽屉 const renderMobileDrawer = () => { if (!sidebar || !isDrawerMode) return null; return ( {/* 遮罩层 */} {/* 抽屉内容 */} {sidebar} ); }; // 渲染抽屉触发按钮 const renderTrigger = () => { if (!isDrawerMode || !renderDrawerTrigger) return null; return renderDrawerTrigger({ onPress: toggleDrawer, isOpen: isDrawerOpen, }); }; return ( {/* 头部 */} {header && ( {header} )} {/* 主布局区域 */} {/* 左侧布局 */} {sidebarPosition === 'left' && renderDesktopSidebar()} {/* 主内容区 */} {/* 抽屉触发按钮(仅在移动端抽屉模式显示) */} {renderTrigger()} {children} {/* 右侧布局 */} {sidebarPosition === 'right' && renderDesktopSidebar()} {/* 移动端抽屉 */} {renderMobileDrawer()} {/* 底部 */} {footer && ( {footer} )} ); } 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; contentStyle?: StyleProp; sidebarStyle?: StyleProp; sidebarWidth?: number; showSidebarBreakpoint?: FineBreakpointKey; sidebarPosition?: 'left' | 'right'; } export function SidebarLayout({ children, sidebar, style, contentStyle, sidebarStyle, sidebarWidth = 280, showSidebarBreakpoint = 'lg', sidebarPosition = 'left', }: SidebarLayoutProps) { return ( {children} ); } export default AdaptiveLayout;