// 暖记响应式骨架 — 三级自适应布局 // 手机: 底部 TabBar + 中心凸起写日记按钮 | 平板: 侧边导航 | 桌面: 三栏 // Web 平台始终使用底部 TabBar(移动端布局)以保证导航交互正常 // 对齐 Open Design 原型稿: 首页/日历/写日记(FAB)/发现/我的 import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../core/constants/breakpoints.dart'; import '../core/theme/app_colors.dart'; import '../core/theme/app_typography.dart'; import '../core/theme/app_radius.dart'; /// 导航项数量(不含中心 FAB) const int kNavItemCount = 4; /// 暖记自适应 Scaffold class ResponsiveScaffold extends StatefulWidget { const ResponsiveScaffold({ super.key, required this.selectedIndex, required this.onDestinationSelected, required this.body, this.onCenterButtonPressed, this.appBarTitle, this.secondaryBody, }); /// 当前选中的导航索引 (0-3,对应首页/日历/发现/我的) final int selectedIndex; /// 导航项被选中回调 final ValueChanged onDestinationSelected; /// 主内容区 final Widget body; /// 中心写日记按钮回调 final VoidCallback? onCenterButtonPressed; final String? appBarTitle; final Widget? secondaryBody; @override State createState() => _ResponsiveScaffoldState(); } class _ResponsiveScaffoldState extends State { @override Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; // Web 平台:始终使用移动端底部 TabBar 布局 final deviceType = kIsWeb ? DeviceType.mobile : Breakpoints.getDeviceType(width); switch (deviceType) { case DeviceType.mobile: return _MobileLayout( selectedIndex: widget.selectedIndex, onDestinationSelected: widget.onDestinationSelected, body: widget.body, onCenterButtonPressed: widget.onCenterButtonPressed, appBarTitle: widget.appBarTitle, ); case DeviceType.tablet: return _TabletLayout( selectedIndex: widget.selectedIndex, onDestinationSelected: widget.onDestinationSelected, body: widget.body, onCenterButtonPressed: widget.onCenterButtonPressed, appBarTitle: widget.appBarTitle, ); case DeviceType.desktop: return _DesktopLayout( selectedIndex: widget.selectedIndex, onDestinationSelected: widget.onDestinationSelected, body: widget.body, onCenterButtonPressed: widget.onCenterButtonPressed, secondaryBody: widget.secondaryBody, appBarTitle: widget.appBarTitle, ); } } } // ===== 导航项定义(4 项,中间由 FAB 占位)===== const _navItems = [ NavigationDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: '首页', ), NavigationDestination( icon: Icon(Icons.calendar_month_outlined), selectedIcon: Icon(Icons.calendar_month), label: '日历', ), // 索引 2: 中心 FAB 占位(写日记) NavigationDestination( icon: Icon(Icons.explore_outlined), selectedIcon: Icon(Icons.explore), label: '发现', ), NavigationDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: '我的', ), ]; const _railItems = [ NavigationRailDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: Text('首页'), ), NavigationRailDestination( icon: Icon(Icons.calendar_month_outlined), selectedIcon: Icon(Icons.calendar_month), label: Text('日历'), ), NavigationRailDestination( icon: Icon(Icons.explore_outlined), selectedIcon: Icon(Icons.explore), label: Text('发现'), ), NavigationRailDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: Text('我的'), ), ]; // ===== 中心写日记 FAB 按钮 ===== class _CenterFabButton extends StatelessWidget { const _CenterFabButton({required this.onPressed}); final VoidCallback onPressed; @override Widget build(BuildContext context) { return FloatingActionButton( heroTag: 'center_write', onPressed: onPressed, backgroundColor: AppColors.accent, foregroundColor: const Color(0xFFFFF8F0), elevation: 4, shape: const CircleBorder(), child: const Icon(Icons.edit_rounded, size: 28), ); } } // ===== 手机布局 — 底部 TabBar + 中心凸起 FAB ===== class _MobileLayout extends StatelessWidget { const _MobileLayout({ required this.selectedIndex, required this.onDestinationSelected, required this.body, this.onCenterButtonPressed, this.appBarTitle, }); final int selectedIndex; final ValueChanged onDestinationSelected; final Widget body; final VoidCallback? onCenterButtonPressed; final String? appBarTitle; @override Widget build(BuildContext context) { return Scaffold( appBar: appBarTitle != null ? AppBar(title: Text(appBarTitle!)) : null, body: body, floatingActionButton: onCenterButtonPressed != null ? _CenterFabButton(onPressed: onCenterButtonPressed!) : null, floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: _BottomNavBar( selectedIndex: selectedIndex, onDestinationSelected: onDestinationSelected, ), ); } } /// 自定义底部导航栏 — 支持中心凹槽 class _BottomNavBar extends StatelessWidget { const _BottomNavBar({ required this.selectedIndex, required this.onDestinationSelected, }); final int selectedIndex; final ValueChanged onDestinationSelected; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return BottomAppBar( shape: const CircularNotchedRectangle(), padding: EdgeInsets.zero, height: 64, color: colorScheme.surface, surfaceTintColor: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // 首页 _NavItem( icon: Icons.home_outlined, activeIcon: Icons.home, label: '首页', isSelected: selectedIndex == 0, color: colorScheme.primary, inactiveColor: colorScheme.onSurfaceVariant, onTap: () => onDestinationSelected(0), ), // 日历 _NavItem( icon: Icons.calendar_month_outlined, activeIcon: Icons.calendar_month, label: '日历', isSelected: selectedIndex == 1, color: colorScheme.primary, inactiveColor: colorScheme.onSurfaceVariant, onTap: () => onDestinationSelected(1), ), // 中间留给 FAB 凹槽 — 占位 const SizedBox(width: 48), // 发现 _NavItem( icon: Icons.explore_outlined, activeIcon: Icons.explore, label: '发现', isSelected: selectedIndex == 2, color: colorScheme.primary, inactiveColor: colorScheme.onSurfaceVariant, onTap: () => onDestinationSelected(2), ), // 我的 _NavItem( icon: Icons.person_outline, activeIcon: Icons.person, label: '我的', isSelected: selectedIndex == 3, color: colorScheme.primary, inactiveColor: colorScheme.onSurfaceVariant, onTap: () => onDestinationSelected(3), ), ], ), ); } } /// 单个底部导航项 class _NavItem extends StatelessWidget { const _NavItem({ required this.icon, required this.activeIcon, required this.label, required this.isSelected, required this.color, required this.inactiveColor, required this.onTap, }); final IconData icon; final IconData activeIcon; final String label; final bool isSelected; final Color color; final Color inactiveColor; final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, customBorder: const CircleBorder(), child: SizedBox( width: 64, height: 56, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon( isSelected ? activeIcon : icon, size: 24, color: isSelected ? color : inactiveColor, ), const SizedBox(height: 2), Text( label, style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? color : inactiveColor, ), ), ], ), ), ); } } // ===== 平板布局 — 侧边 NavigationRail ===== class _TabletLayout extends StatelessWidget { const _TabletLayout({ required this.selectedIndex, required this.onDestinationSelected, required this.body, this.onCenterButtonPressed, this.appBarTitle, }); final int selectedIndex; final ValueChanged onDestinationSelected; final Widget body; final VoidCallback? onCenterButtonPressed; final String? appBarTitle; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: appBarTitle != null ? AppBar(title: Text(appBarTitle!)) : null, body: Row( children: [ NavigationRail( selectedIndex: selectedIndex, onDestinationSelected: onDestinationSelected, destinations: _railItems, leading: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.edit_note_rounded, size: 32, color: theme.colorScheme.primary, ), const SizedBox(height: 8), if (onCenterButtonPressed != null) FloatingActionButton.small( heroTag: 'rail_write', onPressed: onCenterButtonPressed, backgroundColor: AppColors.accent, foregroundColor: const Color(0xFFFFF8F0), child: const Icon(Icons.edit_rounded, size: 20), ), ], ), ), ), const VerticalDivider(thickness: 1, width: 1), Expanded(child: body), ], ), ); } } // ===== 桌面布局 — 三栏 ===== class _DesktopLayout extends StatelessWidget { const _DesktopLayout({ required this.selectedIndex, required this.onDestinationSelected, required this.body, this.onCenterButtonPressed, this.secondaryBody, this.appBarTitle, }); final int selectedIndex; final ValueChanged onDestinationSelected; final Widget body; final VoidCallback? onCenterButtonPressed; final Widget? secondaryBody; final String? appBarTitle; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: appBarTitle != null ? AppBar(title: Text(appBarTitle!)) : null, body: Row( children: [ NavigationRail( selectedIndex: selectedIndex, onDestinationSelected: onDestinationSelected, destinations: _railItems, extended: true, leading: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.edit_note_rounded, size: 32, color: theme.colorScheme.primary, ), const SizedBox(width: 12), Text( '暖记', style: theme.textTheme.headlineSmall?.copyWith( fontFamily: AppTypography.handwrittenFont, color: theme.colorScheme.primary, ), ), ], ), const SizedBox(height: 12), if (onCenterButtonPressed != null) Padding( padding: const EdgeInsets.only(right: 16), child: ElevatedButton.icon( onPressed: onCenterButtonPressed, icon: const Icon(Icons.edit_rounded, size: 18), label: const Text('写日记'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.accent, foregroundColor: const Color(0xFFFFF8F0), shape: RoundedRectangleBorder( borderRadius: AppRadius.pillBorder, ), ), ), ), ], ), ), ), const VerticalDivider(thickness: 1, width: 1), // 主内容区 Expanded( flex: 3, child: body, ), // 第二面板(日记详情/预览) if (secondaryBody != null) ...[ const VerticalDivider(thickness: 1, width: 1), Expanded( flex: 2, child: secondaryBody!, ), ], ], ), ); } }