主要变更: 1. 重构"国学"相关模块为"经典名句",统一命名规范 2. 重命名"阅读报告"为"使用报告",调整相关文案与配置 3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑 4. 新增设备活跃状态前端兜底判断,修复在线计数异常 5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示 6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用 7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置 8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题 9. 调整部分UI交互细节,优化内存占用与应用稳定性
400 lines
15 KiB
Dart
400 lines
15 KiB
Dart
/// ============================================================
|
||
/// 闲言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(),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|