refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
This commit is contained in:
@@ -1,380 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 自适应导航栏
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 宽屏时垂直导航栏,窄屏时底部导航栏,支持4种停靠位置
|
||||
/// 上次更新: 替换bitsdojo_window为window_manager桌面端窗口控制
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../providers/split_view_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../theme/glass_tokens.dart';
|
||||
import '../utils/platform/platform_utils.dart' as pu;
|
||||
import '../../features/discover/providers/chat_provider.dart';
|
||||
import '../../features/mine/settings/providers/theme_settings_provider.dart';
|
||||
import '../../shared/widgets/animation/tab_icon_sprite.dart';
|
||||
|
||||
class AdaptiveNavBar extends ConsumerWidget {
|
||||
const AdaptiveNavBar({
|
||||
required this.currentIndex,
|
||||
required this.onTabSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onTabSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final position = splitState.navBarPosition;
|
||||
return switch (position) {
|
||||
NavBarPosition.left ||
|
||||
NavBarPosition.right => _buildVertical(context, ref),
|
||||
NavBarPosition.top ||
|
||||
NavBarPosition.bottom => _buildHorizontal(context, ref),
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildVertical(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final settings = ref.watch(themeSettingsProvider);
|
||||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||||
final expressionStyle = settings.tabExpressionStyle;
|
||||
final characterId = settings.tabCharacterStyleId;
|
||||
|
||||
Widget buildTab(TabSpriteType type, int index, String label) {
|
||||
final isSelected = index == currentIndex;
|
||||
final showBadge = index == 1 && unreadCount > 0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onTabSelected(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedScale(
|
||||
scale: isSelected ? 1.08 : 1.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
child: showBadge
|
||||
? badges.Badge(
|
||||
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: TabIconSprite(
|
||||
type: type,
|
||||
label: '',
|
||||
isSelected: isSelected,
|
||||
adjacentDirection: 0,
|
||||
animationIntensity: animIntensity,
|
||||
characterId: characterId,
|
||||
eyeScale: expressionStyle.eyeScale,
|
||||
mouthCurve: expressionStyle.mouthCurve,
|
||||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||||
),
|
||||
)
|
||||
: TabIconSprite(
|
||||
type: type,
|
||||
label: '',
|
||||
isSelected: isSelected,
|
||||
adjacentDirection: 0,
|
||||
animationIntensity: animIntensity,
|
||||
characterId: characterId,
|
||||
eyeScale: expressionStyle.eyeScale,
|
||||
mouthCurve: expressionStyle.mouthCurve,
|
||||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
color: isSelected
|
||||
? (ext.isDark ? ext.textInverse : ext.accent)
|
||||
: (ext.isDark
|
||||
? ext.textInverse.withValues(alpha: 0.38)
|
||||
: const Color(0xFFAEAEB2)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final isRight = splitState.navBarPosition == NavBarPosition.right;
|
||||
|
||||
return Container(
|
||||
width: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.glassColor.withValues(
|
||||
alpha: ext.isDark
|
||||
? GlassTokens.elevatedOpacityDark
|
||||
: GlassTokens.elevatedOpacityLight,
|
||||
),
|
||||
),
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||||
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||||
),
|
||||
child: SafeArea(
|
||||
right: isRight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
buildTab(TabSpriteType.home, 0, '闲言'),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
buildTab(TabSpriteType.discover, 1, '发现'),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
buildTab(TabSpriteType.profile, 2, '我的'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHorizontal(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final settings = ref.watch(themeSettingsProvider);
|
||||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||||
final expressionStyle = settings.tabExpressionStyle;
|
||||
final characterId = settings.tabCharacterStyleId;
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final isTop = splitState.navBarPosition == NavBarPosition.top;
|
||||
|
||||
Widget buildTab(TabSpriteType type, int index, String label) {
|
||||
final isSelected = index == currentIndex;
|
||||
final showBadge = index == 1 && unreadCount > 0;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTabSelected(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedScale(
|
||||
scale: isSelected ? 1.08 : 1.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
child: showBadge
|
||||
? badges.Badge(
|
||||
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: TabIconSprite(
|
||||
type: type,
|
||||
label: '',
|
||||
isSelected: isSelected,
|
||||
adjacentDirection: 0,
|
||||
animationIntensity: animIntensity,
|
||||
characterId: characterId,
|
||||
eyeScale: expressionStyle.eyeScale,
|
||||
mouthCurve: expressionStyle.mouthCurve,
|
||||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||||
),
|
||||
)
|
||||
: TabIconSprite(
|
||||
type: type,
|
||||
label: '',
|
||||
isSelected: isSelected,
|
||||
adjacentDirection: 0,
|
||||
animationIntensity: animIntensity,
|
||||
characterId: characterId,
|
||||
eyeScale: expressionStyle.eyeScale,
|
||||
mouthCurve: expressionStyle.mouthCurve,
|
||||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
color: isSelected
|
||||
? (ext.isDark ? ext.textInverse : ext.accent)
|
||||
: (ext.isDark
|
||||
? ext.textInverse.withValues(alpha: 0.38)
|
||||
: const Color(0xFFAEAEB2)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.glassColor.withValues(
|
||||
alpha: ext.isDark
|
||||
? GlassTokens.elevatedOpacityDark
|
||||
: GlassTokens.elevatedOpacityLight,
|
||||
),
|
||||
),
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||||
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: isTop,
|
||||
bottom: !isTop,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
buildTab(TabSpriteType.home, 0, '闲言'),
|
||||
buildTab(TabSpriteType.discover, 1, '发现'),
|
||||
buildTab(TabSpriteType.profile, 2, '我的'),
|
||||
if (isTop && pu.isDesktop) _buildDesktopWindowControls(ext),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 桌面端窗口控制按钮(最小化/最大化/关闭)+ 可拖拽标题栏区域
|
||||
Widget _buildDesktopWindowControls(AppThemeExtension ext) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onPanStart: (_) => windowManager.startDragging(),
|
||||
child: const SizedBox(height: 52, width: 40),
|
||||
),
|
||||
_WindowControlBtn(
|
||||
icon: CupertinoIcons.minus,
|
||||
ext: ext,
|
||||
onPressed: () => windowManager.minimize(),
|
||||
),
|
||||
_WindowControlBtn(
|
||||
icon: CupertinoIcons.square,
|
||||
ext: ext,
|
||||
iconSize: 12,
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
await windowManager.unmaximize();
|
||||
} else {
|
||||
await windowManager.maximize();
|
||||
}
|
||||
},
|
||||
),
|
||||
_WindowControlBtn(
|
||||
icon: CupertinoIcons.xmark,
|
||||
ext: ext,
|
||||
onPressed: () => windowManager.close(),
|
||||
isClose: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 窗口控制按钮(最小化/最大化/关闭)
|
||||
class _WindowControlBtn extends StatefulWidget {
|
||||
const _WindowControlBtn({
|
||||
required this.icon,
|
||||
required this.ext,
|
||||
required this.onPressed,
|
||||
this.iconSize = 14,
|
||||
this.isClose = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final AppThemeExtension ext;
|
||||
final VoidCallback onPressed;
|
||||
final double iconSize;
|
||||
final bool isClose;
|
||||
|
||||
@override
|
||||
State<_WindowControlBtn> createState() => _WindowControlBtnState();
|
||||
}
|
||||
|
||||
class _WindowControlBtnState extends State<_WindowControlBtn> {
|
||||
bool _isHovering = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = widget.ext;
|
||||
final bgColor = _isHovering
|
||||
? (widget.isClose
|
||||
? CupertinoColors.systemRed.withValues(alpha: 0.9)
|
||||
: ext.textHint.withValues(alpha: 0.15))
|
||||
: const Color(0x00000000);
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovering = true),
|
||||
onExit: (_) => setState(() => _isHovering = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onPressed,
|
||||
child: Container(
|
||||
width: 46,
|
||||
height: 52,
|
||||
color: bgColor,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: widget.iconSize,
|
||||
color: _isHovering && widget.isClose
|
||||
? CupertinoColors.white
|
||||
: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 应用布局壳
|
||||
// 创建时间: 2026-04-20
|
||||
// 更新时间: 2026-06-05
|
||||
// 作用: ShellRoute 布局壳,宽屏分屏 + 窄屏底部导航
|
||||
// 上次更新: 移除build中的高频日志,减少IDE卡顿
|
||||
// ============================================================
|
||||
|
||||
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 '../providers/split_view_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../utils/ui/interaction_animations.dart';
|
||||
import '../../features/discover/providers/chat_provider.dart';
|
||||
import '../../features/mine/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 'adaptive_split_view.dart';
|
||||
import 'adaptive_nav_bar.dart';
|
||||
import 'overview_dashboard.dart';
|
||||
import 'right_panel_registry.dart';
|
||||
import 'triple_column_view.dart';
|
||||
import '../../features/mine/profile/presentation/panels/profile_dashboard.dart';
|
||||
|
||||
class AppShell extends ConsumerStatefulWidget {
|
||||
const AppShell({super.key, required this.child});
|
||||
|
||||
final StatefulNavigationShell child;
|
||||
|
||||
static bool get _isOhos => pu.isOhos;
|
||||
|
||||
@override
|
||||
ConsumerState<AppShell> createState() => _AppShellState();
|
||||
}
|
||||
|
||||
class _AppShellState extends ConsumerState<AppShell> {
|
||||
@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 isWidescreen =
|
||||
screenWidth >= kSplitViewBreakpoint && splitState.splitViewEnabled;
|
||||
|
||||
// 鸿蒙端不再每次build打印日志,避免高频日志导致IDE卡顿
|
||||
|
||||
// 同步当前Tab索引到SplitViewProvider
|
||||
if (splitState.currentTab != currentIndex) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
ref.read(splitViewProvider.notifier).setCurrentTab(currentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isWidescreen) {
|
||||
return _buildWidescreenLayout(context, currentIndex);
|
||||
}
|
||||
|
||||
return _buildNarrowLayout(context, ext, currentIndex);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 宽屏分屏布局
|
||||
// ============================================================
|
||||
|
||||
Widget _buildWidescreenLayout(BuildContext context, int currentIndex) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final navBarPosition = splitState.navBarPosition;
|
||||
final isNavBarVertical =
|
||||
navBarPosition == NavBarPosition.left ||
|
||||
navBarPosition == NavBarPosition.right;
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final isTripleColumn =
|
||||
screenWidth >= kTripleColumnBreakpoint &&
|
||||
splitState.thirdPanelContent != null;
|
||||
|
||||
final Widget navBar = AdaptiveNavBar(
|
||||
currentIndex: currentIndex,
|
||||
onTabSelected: (index) => _onTabTap(context, index),
|
||||
);
|
||||
|
||||
final Widget splitView = isTripleColumn
|
||||
? TripleColumnView(
|
||||
leftPanel: RepaintBoundary(
|
||||
child: AppErrorBoundary(label: '主页面', child: widget.child),
|
||||
),
|
||||
centerPanel: _buildRightPanel(context),
|
||||
rightPanel: _buildThirdPanel(context),
|
||||
)
|
||||
: AdaptiveSplitView(
|
||||
leftPanel: RepaintBoundary(
|
||||
child: AppErrorBoundary(label: '主页面', child: widget.child),
|
||||
),
|
||||
rightPanel: _buildRightPanel(context),
|
||||
);
|
||||
|
||||
/// 根据导航栏位置构建布局骨架
|
||||
Widget buildLayout() {
|
||||
if (isNavBarVertical) {
|
||||
final isLeft = navBarPosition == NavBarPosition.left;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: WallpaperBackground()),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isLeft) navBar,
|
||||
Expanded(child: splitView),
|
||||
if (!isLeft) navBar,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isTop = navBarPosition == NavBarPosition.top;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: WallpaperBackground()),
|
||||
Column(
|
||||
children: [
|
||||
if (isTop) navBar,
|
||||
Expanded(child: splitView),
|
||||
if (!isTop) navBar,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Widget body = CelebrationOverlay(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
},
|
||||
child: buildLayout(),
|
||||
),
|
||||
);
|
||||
|
||||
/// 桌面端添加键盘快捷键 (Ctrl+1/2/3 切换Tab, Ctrl+W 关闭面板)
|
||||
if (pu.isDesktop) {
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
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(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
_SwitchTabIntent: _SwitchTabAction(
|
||||
onSwitch: (index) => _onTabTap(context, index),
|
||||
),
|
||||
_ClosePanelIntent: _ClosePanelAction(ref: ref),
|
||||
},
|
||||
child: Focus(child: body),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// 构建右侧面板内容
|
||||
Widget _buildRightPanel(BuildContext context) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final activePanel = splitState.activeRightPanel;
|
||||
|
||||
if (activePanel == null) {
|
||||
if (splitState.currentTab == 2) {
|
||||
return const ProfileDashboard();
|
||||
}
|
||||
return const OverviewDashboard();
|
||||
}
|
||||
|
||||
return RightPanelRegistry.build(
|
||||
activePanel,
|
||||
context,
|
||||
args: splitState.rightPanelArgs,
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建第三栏面板内容(超宽屏三栏布局)
|
||||
Widget _buildThirdPanel(BuildContext context) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final thirdPanel = splitState.thirdPanelContent;
|
||||
|
||||
if (thirdPanel == null) {
|
||||
return const OverviewDashboard();
|
||||
}
|
||||
|
||||
return RightPanelRegistry.build(
|
||||
thirdPanel,
|
||||
context,
|
||||
args: splitState.thirdPanelArgs,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 窄屏布局(原有逻辑)
|
||||
// ============================================================
|
||||
|
||||
Widget _buildNarrowLayout(
|
||||
BuildContext context,
|
||||
AppThemeExtension ext,
|
||||
int currentIndex,
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
final Widget bottomBar = (AppShell._isOhos && !liquidGlassReady)
|
||||
? _buildFallbackNavBar(context, currentIndex, ext, settings)
|
||||
: 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: GlassQuality.premium,
|
||||
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),
|
||||
),
|
||||
glassSettings: 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,
|
||||
);
|
||||
|
||||
return CelebrationOverlay(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
body: _buildNarrowBody(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTabTap(BuildContext context, int index) {
|
||||
widget.child.goBranch(
|
||||
index,
|
||||
initialLocation: index == widget.child.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏布局body:横屏未达分屏断点时居中限宽,竖屏保持原样
|
||||
Widget _buildNarrowBody() {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final isLandscapeNarrow =
|
||||
size.width > 600 &&
|
||||
size.width < kSplitViewBreakpoint &&
|
||||
size.height < size.width;
|
||||
|
||||
final content = Stack(
|
||||
children: [
|
||||
const WallpaperBackground(),
|
||||
RepaintBoundary(
|
||||
child: AppErrorBoundary(label: '主页面', child: widget.child),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (isLandscapeNarrow) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
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) {
|
||||
ref.read(splitViewProvider.notifier).clearActivePanel();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 鸿蒙端专用布局壳
|
||||
/// 创建时间: 2026-05-18
|
||||
/// 更新时间: 2026-06-06
|
||||
/// 作用: 鸿蒙端使用 Scaffold+GlassBottomBar 替代 GoRouter+StatefulShellRoute
|
||||
/// 上次更新: 修复鸿蒙端杀后台重启时引导页重复弹出的问题(添加KvStorage.isReady检查)
|
||||
/// ============================================================
|
||||
///
|
||||
/// 根因: 鸿蒙端 Flutter 引擎中 MaterialApp.router + 额外包导入 = 白屏
|
||||
/// 方案: 鸿蒙端仅阉割路由(GoRouter),保留液态玻璃效果
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||
|
||||
import '../../core/storage/kv_storage.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/utils/ui/interaction_animations.dart';
|
||||
import '../../core/utils/logger.dart';
|
||||
import '../../core/utils/platform/platform_utils.dart'
|
||||
show OhosDeviceCapabilities;
|
||||
import '../../features/onboarding/presentation/onboarding_page.dart';
|
||||
import '../../features/discover/providers/chat_provider.dart';
|
||||
import '../../features/discover/presentation/pages/home/discover_page.dart';
|
||||
import '../../features/home/presentation/home_page.dart';
|
||||
import '../../features/mine/profile/presentation/profile_page.dart';
|
||||
import '../../features/mine/settings/providers/theme_settings_provider.dart';
|
||||
import '../../l10n/translations.dart';
|
||||
import '../../main.dart' show liquidGlassReady;
|
||||
import '../../shared/widgets/containers/glass_bottom_nav_bar.dart';
|
||||
import '../../shared/widgets/animation/tab_icon_sprite.dart';
|
||||
|
||||
class OhosAppShell extends ConsumerStatefulWidget {
|
||||
const OhosAppShell({super.key});
|
||||
|
||||
static _OhosAppShellState? _instance;
|
||||
|
||||
/// 切换到指定 Tab 索引(供 OhosNavBridge.go() 调用)
|
||||
static void switchTab(int index) {
|
||||
_instance?.switchTab(index);
|
||||
}
|
||||
|
||||
@override
|
||||
ConsumerState<OhosAppShell> createState() => _OhosAppShellState();
|
||||
}
|
||||
|
||||
class _OhosAppShellState extends ConsumerState<OhosAppShell> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
static const List<Widget> _tabPages = [
|
||||
HomePage(),
|
||||
DiscoverPage(),
|
||||
ProfilePage(),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
OhosAppShell._instance = this;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkOnboarding();
|
||||
});
|
||||
}
|
||||
|
||||
void _checkOnboarding() {
|
||||
if (!mounted) return;
|
||||
|
||||
// 等待KvStorage初始化完成再检查引导页状态
|
||||
// 避免Hive未初始化时默认值导致引导页重复弹出
|
||||
if (!KvStorage.isReady) {
|
||||
Log.i('🟢 [OHOS] 引导页检查: KvStorage未就绪,延迟检查');
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (mounted) _checkOnboarding();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldShow =
|
||||
KvStorage.isFirstLaunch || KvStorage.shouldShowOnboarding;
|
||||
if (shouldShow) {
|
||||
Log.i('🟢 [OHOS] 引导页检查: 首次启动=$isFirstLaunch, 应显示引导=$shouldShow → 推送引导页');
|
||||
Navigator.of(context).push<void>(
|
||||
CupertinoPageRoute<void>(
|
||||
fullscreenDialog: true,
|
||||
builder: (_) => const OnboardingPage(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Log.i('🟢 [OHOS] 引导页检查: 已完成引导,直接进入主页');
|
||||
}
|
||||
}
|
||||
|
||||
bool get isFirstLaunch => KvStorage.isFirstLaunch;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
OhosAppShell._instance = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 切换到指定 Tab 索引
|
||||
void switchTab(int index) {
|
||||
if (index >= 0 && index < _tabPages.length) {
|
||||
setState(() => _currentIndex = index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
OhosDeviceCapabilities.init(context);
|
||||
|
||||
final ext = AppTheme.ext(context);
|
||||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||||
final settings = ref.watch(themeSettingsProvider);
|
||||
final expressionStyle = settings.tabExpressionStyle;
|
||||
final characterId = settings.tabCharacterStyleId;
|
||||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||||
|
||||
Log.i(
|
||||
'🟢 [OHOS] OhosAppShell.build() — currentIndex=$_currentIndex liquidGlass=$liquidGlassReady',
|
||||
);
|
||||
|
||||
final Widget bottomBar = liquidGlassReady
|
||||
? _buildGlassBottomBar(
|
||||
ext: ext,
|
||||
unreadCount: unreadCount,
|
||||
expressionStyle: expressionStyle,
|
||||
characterId: characterId,
|
||||
animIntensity: animIntensity,
|
||||
)
|
||||
: _buildFallbackNavBar(
|
||||
ext: ext,
|
||||
unreadCount: unreadCount,
|
||||
expressionStyle: expressionStyle,
|
||||
characterId: characterId,
|
||||
animIntensity: animIntensity,
|
||||
);
|
||||
|
||||
return CelebrationOverlay(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
body: IndexedStack(index: _currentIndex, children: _tabPages),
|
||||
bottomNavigationBar: bottomBar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// GPU Shader 液态玻璃底部导航栏 (LiquidGlassWidgets 可用时)
|
||||
Widget _buildGlassBottomBar({
|
||||
required AppThemeExtension ext,
|
||||
required int unreadCount,
|
||||
required TabExpressionStyleOption expressionStyle,
|
||||
required String characterId,
|
||||
required double animIntensity,
|
||||
}) {
|
||||
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: _buildBadgeIcon(
|
||||
unreadCount: unreadCount,
|
||||
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
|
||||
),
|
||||
activeIcon: _buildBadgeIcon(
|
||||
unreadCount: unreadCount,
|
||||
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) => setState(() => _currentIndex = index),
|
||||
quality: GlassQuality.premium,
|
||||
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
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.black.withValues(alpha: 0.04),
|
||||
indicatorSettings: LiquidGlassSettings(
|
||||
thickness: 40,
|
||||
blur: 25,
|
||||
refractiveIndex: 1.8,
|
||||
chromaticAberration: 1.2,
|
||||
lightIntensity: 3.5,
|
||||
ambientStrength: 1.2,
|
||||
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),
|
||||
),
|
||||
glassSettings: LiquidGlassSettings(
|
||||
thickness: 30,
|
||||
blur: 1.5,
|
||||
refractiveIndex: 1.5,
|
||||
chromaticAberration: 0.8,
|
||||
lightIntensity: 1.2,
|
||||
saturation: 1.0,
|
||||
ambientStrength: 0.6,
|
||||
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.12,
|
||||
innerBlur: 1.5,
|
||||
glowOpacity: 0.4,
|
||||
glowBlurRadius: 24,
|
||||
glowSpreadRadius: 4,
|
||||
);
|
||||
}
|
||||
|
||||
/// 纯Dart BackdropFilter 液态玻璃底部导航栏 (降级方案)
|
||||
Widget _buildFallbackNavBar({
|
||||
required AppThemeExtension ext,
|
||||
required int unreadCount,
|
||||
required TabExpressionStyleOption expressionStyle,
|
||||
required String characterId,
|
||||
required double animIntensity,
|
||||
}) {
|
||||
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) => setState(() => _currentIndex = index),
|
||||
ext: ext,
|
||||
animationIntensity: animIntensity,
|
||||
expressionStyle: expressionStyle,
|
||||
characterId: characterId,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadgeIcon({required int unreadCount, required Widget child}) {
|
||||
if (unreadCount <= 0) return child;
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
'$unreadCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
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: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 概览仪表盘
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-06-01
|
||||
/// 作用: 宽屏分屏右侧面板的空状态页面,显示概览信息
|
||||
/// 上次更新: 最近浏览接入RecentRouteService,展示真实浏览记录
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../features/home/providers/home_provider.dart';
|
||||
import '../../features/home/providers/favorite_provider.dart';
|
||||
import '../../features/home/providers/likes_provider.dart';
|
||||
import '../../features/auth/providers/auth_provider.dart';
|
||||
import '../../features/mine/signin/providers/signin_provider.dart';
|
||||
import '../../features/home/providers/tool_center_recent_provider.dart';
|
||||
import '../../features/home/presentation/home_tool_center.dart';
|
||||
import '../router/app_routes.dart';
|
||||
import '../router/app_nav_extension.dart';
|
||||
import '../services/navigation/recent_route_service.dart';
|
||||
|
||||
class OverviewDashboard extends ConsumerWidget {
|
||||
const OverviewDashboard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildGreeting(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildTodayRecommend(context, ref),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildRecentHistory(context, ref),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildStats(context, ref),
|
||||
const SizedBox(height: AppSpacing.xxl),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGreeting(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final hour = DateTime.now().hour;
|
||||
final greeting = switch (hour) {
|
||||
>= 6 && < 12 => '早上好 ☀️',
|
||||
>= 12 && < 14 => '中午好 🌤️',
|
||||
>= 14 && < 18 => '下午好 🌅',
|
||||
>= 18 && < 22 => '晚上好 🌙',
|
||||
_ => '夜深了 🌛',
|
||||
};
|
||||
|
||||
return GlassContainer(
|
||||
depth: GlassDepth.elevated,
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
greeting,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'选择左侧内容查看详情',
|
||||
style: TextStyle(fontSize: 14, color: ext.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodayRecommend(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final homeState = ref.watch(homeProvider);
|
||||
final recommends = homeState.dailySentences;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'✨ 今日推荐',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: recommends.isEmpty
|
||||
? GlassContainer(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('📭', style: TextStyle(fontSize: 28)),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'暂无推荐内容',
|
||||
style: TextStyle(fontSize: 13, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: recommends.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
itemBuilder: (context, index) {
|
||||
final sentence = recommends[index];
|
||||
return GlassContainer(
|
||||
width: 180,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sentence.text,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
sentence.author != null
|
||||
? '—— ${sentence.author}'
|
||||
: '—— 佚名',
|
||||
style: TextStyle(fontSize: 11, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final actions = <({String emoji, String label, String route})>[
|
||||
(emoji: '🔍', label: '搜索', route: AppRoutes.search),
|
||||
(emoji: '⭐', label: '收藏', route: AppRoutes.favorites),
|
||||
(emoji: '📖', label: '稍后读', route: AppRoutes.readLater),
|
||||
(emoji: '🕐', label: '历史', route: AppRoutes.history),
|
||||
(emoji: '✅', label: '签到', route: AppRoutes.signin),
|
||||
(emoji: '📊', label: '使用报告', route: AppRoutes.readingReport),
|
||||
(emoji: '🌤️', label: '每日推荐', route: AppRoutes.dailyCard),
|
||||
(emoji: '⚙️', label: '设置', route: AppRoutes.generalSettings),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🚀 快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
AnimationLimiter(
|
||||
child: Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: actions.asMap().entries.map((entry) {
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: entry.key,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: GestureDetector(
|
||||
onTap: () => context.appPush(entry.value.route),
|
||||
child: GlassContainer(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(entry.value.emoji,
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
entry.value.label,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: ext.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentHistory(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
ref.watch(toolCenterRecentProvider);
|
||||
final recentRoutes = RecentRouteService.getRecentRoutes();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🕐 最近浏览',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
recentRoutes.isEmpty
|
||||
? GlassContainer(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('📭', style: TextStyle(fontSize: 32)),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'暂无浏览记录',
|
||||
style: TextStyle(fontSize: 13, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: AnimationLimiter(
|
||||
child: Column(
|
||||
children: recentRoutes.take(6).toList().asMap().entries.map((entry) {
|
||||
final String route = entry.value;
|
||||
final String iconText = ToolCenterIconMap.getIconText(route);
|
||||
final String name = ToolCenterIconMap.getName(route);
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: entry.key,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 30.0,
|
||||
child: FadeInAnimation(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
||||
child: GestureDetector(
|
||||
onTap: () => context.appPush(route),
|
||||
child: GlassContainer(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(iconText, style: const TextStyle(fontSize: 18)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
CupertinoIcons.chevron_right,
|
||||
size: 14,
|
||||
color: ext.textHint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStats(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final authState = ref.watch(authProvider);
|
||||
final favoriteState = ref.watch(favoriteProvider);
|
||||
final likesState = ref.watch(likesProvider);
|
||||
final signinState = ref.watch(signinProvider);
|
||||
|
||||
final readCount = authState.user?.signinDays ?? 0;
|
||||
final favCount = favoriteState.total;
|
||||
final likeCount = likesState.total;
|
||||
final streakDays = signinState.continuous;
|
||||
|
||||
final stats = [
|
||||
('📖', '阅读', '$readCount'),
|
||||
('⭐', '收藏', '$favCount'),
|
||||
('👍', '点赞', '$likeCount'),
|
||||
('🔥', '连续', '$streakDays天'),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'📊 数据统计',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
AnimationLimiter(
|
||||
child: Row(
|
||||
children: stats.asMap().entries.map((entry) {
|
||||
final stat = entry.value;
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: entry.key,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: Expanded(
|
||||
child: GlassContainer(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xs / 2,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(stat.$1, style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
stat.$3,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
stat.$2,
|
||||
style: TextStyle(fontSize: 11, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user