// ============================================================ // 闲言APP — 应用布局壳 // 创建时间: 2026-04-20 // 更新时间: 2026-06-23 // 作用: ShellRoute 布局壳,宽屏工作台三栏 + 窄屏底部导航 // 上次更新: 任务10扩展-简化floatingNavBar鸿蒙端防御性冗余判断(工作台模式已过滤鸿蒙端) // ============================================================ import 'package:badges/badges.dart' as badges; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; import 'package:go_router/go_router.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; import '../../core/providers/split_view_provider.dart'; import '../../core/theme/app_theme.dart'; import '../../core/utils/platform/glass_quality_resolver.dart'; import '../../core/utils/ui/interaction_animations.dart'; import '../../features/discover/providers/chat_provider.dart'; import '../../features/settings/providers/theme_settings_provider.dart'; import '../../l10n/translations.dart'; import '../../shared/widgets/animation/tab_icon_sprite.dart'; import '../../main.dart' show liquidGlassReady; import '../../shared/widgets/containers/wallpaper_background.dart'; import '../../shared/widgets/containers/glass_bottom_nav_bar.dart'; import '../../shared/widgets/feedback/app_error_boundary.dart'; import '../../core/layout/adaptive_split_view.dart'; import '../../core/layout/workbench/workbench_layout.dart'; import '../../core/layout/workbench/right_panel_navigator.dart'; import '../../features/desktop/desktop_window_title_bar.dart'; import 'adaptive_nav_bar.dart'; import 'overview_dashboard.dart'; class AppShell extends ConsumerStatefulWidget { const AppShell({super.key, required this.child}); final StatefulNavigationShell child; static bool get _isOhos => pu.isOhos; @override ConsumerState createState() => _AppShellState(); } class _AppShellState extends ConsumerState { @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); final int currentIndex = widget.child.currentIndex; final screenWidth = MediaQuery.sizeOf(context).width; final splitState = ref.watch(splitViewProvider); // 工作台模式判断:宽屏 + 工作台开关开启 + 非鸿蒙端 final isWorkbench = isWorkbenchWidth(screenWidth) && splitState.workbenchEnabled && !pu.isOhos; // 同步当前Tab索引到SplitViewProvider if (splitState.currentTab != currentIndex) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ref.read(splitViewProvider.notifier).setCurrentTab(currentIndex); } }); } // 构建主内容 Widget content; if (isWorkbench) { content = _buildWorkbenchLayout(context, currentIndex); } else { content = _buildNarrowLayout(context, ext, currentIndex); } // 桌面端在顶部添加自定义软件样式标题栏 if (pu.isDesktop) { return Column( children: [ const DesktopWindowTitleBar(), Expanded(child: content), ], ); } return content; } // ============================================================ // 工作台布局(宽屏三栏/双栏) // ============================================================ Widget _buildWorkbenchLayout(BuildContext context, int currentIndex) { final Widget navBar = AdaptiveNavBar( currentIndex: currentIndex, onTabSelected: (index) => _onTabTap(context, index), ); // 任务8:顶/底导航栏使用 app 模式的 GlassBottomBar(悬浮不占位) // 任务10扩展:简化鸿蒙端防御性冗余判断 // 工作台模式已在 build() 中通过 !pu.isOhos 过滤鸿蒙端,此处无需重复判断 final ext = AppTheme.ext(context); final Widget floatingNavBar = _buildGlassBottomBar(context, currentIndex, ext); // 右栏默认内容:统一显示概览仪表盘(合并三个Tab的仪表盘为一个) const Widget defaultRightPanel = OverviewDashboard(); final Widget body = WorkbenchLayout( navigationShell: RepaintBoundary( child: AppErrorBoundary(label: '主页面', child: widget.child), ), navBar: navBar, floatingNavBar: floatingNavBar, defaultRightPanel: defaultRightPanel, ); // 桌面端添加键盘快捷键 if (pu.isDesktop) { return Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit1): const _SwitchTabIntent(0), LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit2): const _SwitchTabIntent(1), LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit3): const _SwitchTabIntent(2), LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyW): const _ClosePanelIntent(), // 工作台右栏返回快捷键 LogicalKeySet(LogicalKeyboardKey.escape): const _RightPanelBackIntent(), LogicalKeySet( LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const _RightPanelBackIntent(), }, child: Actions( actions: >{ _SwitchTabIntent: _SwitchTabAction( onSwitch: (index) => _onTabTap(context, index), ), _ClosePanelIntent: _ClosePanelAction(ref: ref), _RightPanelBackIntent: _RightPanelBackAction(ref: ref), }, child: Focus(child: body), ), ); } return body; } // ============================================================ // 窄屏布局(原有逻辑) // ============================================================ /// 构建 GlassBottomBar(app 模式 tap 栏) /// /// 任务8:提取为独立方法,供窄屏布局和工作台顶/底布局复用 /// 使用液态玻璃 GPU Shader 实现,悬浮不占位 Widget _buildGlassBottomBar( BuildContext context, int currentIndex, AppThemeExtension ext, ) { final unreadCount = ref.watch(chatProvider).unreadCount; final settings = ref.watch(themeSettingsProvider); final expressionStyle = settings.tabExpressionStyle; final characterId = settings.tabCharacterStyleId; final animIntensity = settings.animationIntensity.durationMultiplier; int adjacentFor(int index) { if (index == currentIndex) return 0; if (index < currentIndex) return -1; return 1; } Widget buildSpriteIcon(TabSpriteType type, int index, String label) { return TabIconSprite( type: type, label: label, isSelected: index == currentIndex, adjacentDirection: adjacentFor(index), animationIntensity: animIntensity, characterId: characterId, eyeScale: expressionStyle.eyeScale, mouthCurve: expressionStyle.mouthCurve, bounceMultiplier: expressionStyle.bounceMultiplier, ); } return GlassBottomBar( tabs: [ GlassBottomBarTab( label: '', icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), glowColor: const Color(0xFFE8E8ED), ), GlassBottomBarTab( label: '', icon: badges.Badge( showBadge: unreadCount > 0, badgeContent: Text( '', style: TextStyle( color: ext.textOnAccent, fontSize: 9, fontWeight: FontWeight.bold, ), ), badgeStyle: const badges.BadgeStyle( badgeColor: CupertinoColors.systemRed, padding: EdgeInsets.all(3), ), position: badges.BadgePosition.topEnd(top: -4, end: -6), child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), ), activeIcon: badges.Badge( showBadge: unreadCount > 0, badgeContent: Text( '', style: TextStyle( color: ext.textOnAccent, fontSize: 9, fontWeight: FontWeight.bold, ), ), badgeStyle: const badges.BadgeStyle( badgeColor: CupertinoColors.systemRed, padding: EdgeInsets.all(3), ), position: badges.BadgePosition.topEnd(top: -4, end: -6), child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), ), glowColor: const Color(0xFFE8E8ED), ), GlassBottomBarTab( label: '', icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), glowColor: const Color(0xFFE8E8ED), ), ], selectedIndex: currentIndex, onTabSelected: (index) => _onTabTap(context, index), quality: GlassQualityResolver.quality, selectedIconColor: ext.accent, unselectedIconColor: ext.isDark ? ext.accent.withValues(alpha: 0.38) : const Color(0xFFAEAEB2), barHeight: 68, barBorderRadius: 34, horizontalPadding: 16, verticalPadding: 16, indicatorColor: ext.isDark ? ext.textInverse.withValues(alpha: 0.08) : ext.overlaySubtle, indicatorSettings: LiquidGlassSettings( thickness: 24, blur: 12, refractiveIndex: 1.45, chromaticAberration: 0.3, lightIntensity: 1.2, ambientStrength: 0.6, glassColor: ext.isDark ? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1) : const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1), ), settings: LiquidGlassSettings( blur: 1.0, refractiveIndex: 1.35, chromaticAberration: 0.2, lightIntensity: 0.8, saturation: 1.0, ambientStrength: 0.4, glassColor: ext.isDark ? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1) : const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1), ), magnification: 1.08, innerBlur: 1.0, glowOpacity: 0.3, glowBlurRadius: 16, glowSpreadRadius: 2, ); } Widget _buildNarrowLayout( BuildContext context, AppThemeExtension ext, int currentIndex, ) { final Widget bottomBar = (AppShell._isOhos && !liquidGlassReady) ? _buildFallbackNavBar(context, currentIndex, ext, ref.watch(themeSettingsProvider)) : _buildGlassBottomBar(context, currentIndex, ext); // 检测横屏窄屏模式:横屏时tab栏移到左侧,不显示底部导航栏 final size = MediaQuery.sizeOf(context); final isLandscapeNarrow = size.width > 600 && size.width < kCompactBreakpoint && size.height < size.width; return CelebrationOverlay( child: PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; }, child: Scaffold( extendBody: true, body: _buildNarrowBody(), bottomNavigationBar: isLandscapeNarrow ? null : bottomBar, ), ), ); } void _onTabTap(BuildContext context, int index) { widget.child.goBranch( index, initialLocation: index == widget.child.currentIndex, ); } /// 窄屏布局body:横屏未达工作台断点时左侧显示tab栏,竖屏保持原样 /// /// 修复:横屏窄屏(600 600 && size.width < kCompactBreakpoint && size.height < size.width; final content = Stack( children: [ const WallpaperBackground(), RepaintBoundary( child: AppErrorBoundary(label: '主页面', child: widget.child), ), ], ); if (isLandscapeNarrow) { // 横屏窄屏:左侧显示垂直tab栏,右侧显示页面内容 return Row( children: [ _buildVerticalTabBar(), Expanded(child: content), ], ); } return content; } /// 横屏窄屏模式下的垂直tab栏 Widget _buildVerticalTabBar() { final ext = AppTheme.ext(context); final settings = ref.watch(themeSettingsProvider); final expressionStyle = settings.tabExpressionStyle; final characterId = settings.tabCharacterStyleId; final animIntensity = settings.animationIntensity.durationMultiplier; final currentIndex = widget.child.currentIndex; final unreadCount = ref.watch(chatProvider).unreadCount; int adjacentFor(int index) { if (index == currentIndex) return 0; if (index < currentIndex) return -1; return 1; } Widget buildSpriteIcon(TabSpriteType type, int index, String label) { return TabIconSprite( type: type, label: label, isSelected: index == currentIndex, adjacentDirection: adjacentFor(index), animationIntensity: animIntensity, characterId: characterId, eyeScale: expressionStyle.eyeScale, mouthCurve: expressionStyle.mouthCurve, bounceMultiplier: expressionStyle.bounceMultiplier, ); } final tabs = []; // 闲言 tabs.add(_buildVerticalTabItem( ext: ext, icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), isSelected: currentIndex == 0, onTap: () => _onTabTap(context, 0), )); // 发现(带未读badge) tabs.add(_buildVerticalTabItem( ext: ext, icon: badges.Badge( showBadge: unreadCount > 0, badgeContent: Text( '$unreadCount', style: TextStyle( color: ext.textOnAccent, fontSize: 9, fontWeight: FontWeight.bold, ), ), badgeStyle: const badges.BadgeStyle( badgeColor: CupertinoColors.systemRed, padding: EdgeInsets.all(3), ), position: badges.BadgePosition.topEnd(top: -4, end: -6), child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), ), isSelected: currentIndex == 1, onTap: () => _onTabTap(context, 1), )); // 我的 tabs.add(_buildVerticalTabItem( ext: ext, icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), isSelected: currentIndex == 2, onTap: () => _onTabTap(context, 2), )); return Container( width: 56, decoration: BoxDecoration( color: ext.bgPrimary.withValues(alpha: 0.85), border: Border( right: BorderSide( color: ext.textHint.withValues(alpha: 0.1), width: 0.5, ), ), ), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: tabs, ), ), ); } /// 垂直tab栏单个item Widget _buildVerticalTabItem({ required AppThemeExtension ext, required Widget icon, required bool isSelected, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, width: 44, height: 44, decoration: BoxDecoration( color: isSelected ? ext.accent.withValues(alpha: 0.12) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Center(child: icon), ), ); } Widget _buildFallbackNavBar( BuildContext context, int currentIndex, AppThemeExtension ext, ThemeSettingsState settings, ) { final expressionStyle = settings.tabExpressionStyle; final characterId = settings.tabCharacterStyleId; final animIntensity = settings.animationIntensity.durationMultiplier; final unreadCount = ref.watch(chatProvider).unreadCount; final t = ref.watch(translationsProvider); return GlassBottomNavBar( items: [ GlassBottomNavBarItem(spriteType: TabSpriteType.home, label: t.navHome), GlassBottomNavBarItem( spriteType: TabSpriteType.discover, label: t.navDiscover, badgeCount: unreadCount, ), GlassBottomNavBarItem( spriteType: TabSpriteType.profile, label: t.navProfile, ), ], selectedIndex: currentIndex, onTabSelected: (index) => _onTabTap(context, index), ext: ext, animationIntensity: animIntensity, expressionStyle: expressionStyle, characterId: characterId, ); } } // ============================================================ // 桌面端键盘快捷键 — Intent & Action // ============================================================ /// 切换Tab意图 class _SwitchTabIntent extends Intent { const _SwitchTabIntent(this.index); final int index; } /// 关闭面板意图 class _ClosePanelIntent extends Intent { const _ClosePanelIntent(); } /// 切换Tab动作 class _SwitchTabAction extends Action<_SwitchTabIntent> { _SwitchTabAction({required this.onSwitch}); final void Function(int) onSwitch; @override Object? invoke(_SwitchTabIntent intent) { onSwitch(intent.index); return null; } } /// 关闭面板动作 class _ClosePanelAction extends Action<_ClosePanelIntent> { _ClosePanelAction({required this.ref}); final WidgetRef ref; @override Object? invoke(_ClosePanelIntent intent) { // 关闭右栏当前页面:清空当前 Tab 的右栏栈 final currentTab = ref.read(splitViewProvider).currentTab; ref.read(rightPanelStackProvider.notifier).clear(currentTab); return null; } } /// 右栏返回意图 class _RightPanelBackIntent extends Intent { const _RightPanelBackIntent(); } /// 右栏返回动作 — pop 栈顶,逐页返回上一页 class _RightPanelBackAction extends Action<_RightPanelBackIntent> { _RightPanelBackAction({required this.ref}); final WidgetRef ref; @override Object? invoke(_RightPanelBackIntent intent) { // pop 右栏栈顶条目,逐页返回(符合 iOS 标准返回语义) final currentTab = ref.read(splitViewProvider).currentTab; ref.read(rightPanelStackProvider.notifier).pop(currentTab); return null; } }