Files
xianyan/lib/app/layout/overview_dashboard.dart
Developer 88a3f6d65f feat: 新增仪表盘页面与macOS多项优化
1. 新增TDashboard翻译类型与多语言文案
2. 完善macOS权限管理与Impeller渲染适配
3. 更新服务器部署配置与协议文件上传脚本
4. 修复翻译导入服务与根类型编译问题
2026-06-26 06:34:05 +08:00

588 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 概览仪表盘
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-26
/// 作用: 宽屏分屏右侧面板的空状态页面,显示概览信息
/// 上次更新: 全面国际化+动态主题/样式+触觉反馈+骨架屏+无障碍降级+时段自动刷新+空指针防护
/// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shimmer/shimmer.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_spacing.dart';
import '../../features/auth/providers/auth_provider.dart';
import '../../features/home/providers/favorite_provider.dart';
import '../../features/home/providers/home_provider.dart';
import '../../features/home/providers/likes_provider.dart';
import '../../features/home/providers/tool_center_recent_provider.dart';
import '../../features/home/presentation/home_tool_center.dart';
import '../../features/settings/providers/general_settings_provider.dart';
import '../../features/signin/providers/signin_provider.dart';
import '../../core/router/app_nav_extension.dart';
import '../../core/router/app_routes.dart';
import '../../core/services/recent_route_service.dart';
import '../../l10n/translation_resolver.dart';
import '../../l10n/types/t.dart';
import '../../shared/widgets/containers/glass_container.dart';
import '../../shared/widgets/display/app_icon.dart';
/// 工作台概览仪表盘 — 右栏默认面板
///
/// 特性:
/// - 全文案多语言([translationsProvider] → [TDashboard]
/// - 动态主题/样式([AppThemeExtension] 字号缩放/字重/玻璃/圆角)
/// - 触觉反馈([HapticFeedback]
/// - 骨架屏加载([Shimmer]
/// - 无障碍动画降级(响应 reduceAnimations 设置)
/// - 时段问候语自动刷新(每分钟校验小时变更)
/// - 空指针防护provider 异常时回退 0
class OverviewDashboard extends ConsumerStatefulWidget {
const OverviewDashboard({super.key});
@override
ConsumerState<OverviewDashboard> createState() => _OverviewDashboardState();
}
class _OverviewDashboardState extends ConsumerState<OverviewDashboard> {
/// 当前小时缓存 — 用于检测时段切换触发刷新
int _currentHour = DateTime.now().hour;
/// 时段轮询定时器 — 每分钟校验一次,跨午时/傍晚时自动刷新问候语
late final Timer _hourTimer;
@override
void initState() {
super.initState();
_hourTimer = Timer.periodic(const Duration(minutes: 1), (_) {
final h = DateTime.now().hour;
if (h != _currentHour && mounted) {
setState(() => _currentHour = h);
}
});
}
@override
void dispose() {
_hourTimer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final t = ref.watch(translationsProvider);
final ext = AppTheme.ext(context);
final reduceAnim = ref.watch(generalSettingsProvider).reduceAnimations;
final db = t.dashboard;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGreeting(context, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
_buildTodayRecommend(context, ref, db, ext),
const SizedBox(height: AppSpacing.lg),
_buildQuickActions(context, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
_buildRecentHistory(context, ref, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
_buildStats(context, ref, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.xxl),
],
),
);
}
// ============================================================
// 问候区
// ============================================================
Widget _buildGreeting(BuildContext context, TDashboard db, AppThemeExtension ext, bool reduceAnim) {
// 时段问候语 + 图标(基于 _currentHour跨时段自动刷新
final greeting = switch (_currentHour) {
>= 6 && < 12 => db.goodMorning,
>= 12 && < 14 => db.goodNoon,
>= 14 && < 18 => db.goodAfternoon,
>= 18 && < 22 => db.goodEvening,
_ => db.goodNight,
};
final greetingIcon = switch (_currentHour) {
>= 6 && < 12 => CupertinoIcons.sun_max,
>= 12 && < 14 => CupertinoIcons.cloud_sun,
>= 14 && < 18 => CupertinoIcons.sunset,
>= 18 && < 22 => CupertinoIcons.moon,
_ => CupertinoIcons.moon_stars,
};
return GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
children: [
AppIcon(
cupertinoIcon: greetingIcon,
size: 28,
animate: !reduceAnim,
semanticLabel: greeting,
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
greeting,
style: TextStyle(
fontSize: 22 * ext.fontScale,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
db.greetingHint,
style: TextStyle(
fontSize: 14 * ext.fontScale,
fontWeight: ext.fontWeight,
color: ext.textSecondary,
),
),
],
),
),
],
),
);
}
// ============================================================
// 今日推荐
// ============================================================
Widget _buildTodayRecommend(
BuildContext context,
WidgetRef ref,
TDashboard db,
AppThemeExtension ext,
) {
final homeState = ref.watch(homeProvider);
final recommends = homeState.dailySentences;
final isLoading = homeState.isLoading;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, CupertinoIcons.sparkles, db.sectionRecommend, ext),
const SizedBox(height: AppSpacing.sm),
SizedBox(
height: 120,
// 加载中且无数据 → 骨架屏;无数据 → 空状态;有数据 → 列表
child: isLoading && recommends.isEmpty
? _buildRecommendSkeleton(ext)
: recommends.isEmpty
? _buildEmptyState(context, CupertinoIcons.tray, db.emptyRecommend, ext)
: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: recommends.length,
separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
itemBuilder: (context, index) {
final sentence = recommends[index];
final author = sentence.author != null && sentence.author!.isNotEmpty
? '$db.authorPrefix${sentence.author}'
: '$db.authorPrefix${db.anonymousAuthor}';
return GlassContainer(
width: 180,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sentence.text,
style: TextStyle(
fontSize: 13 * ext.fontScale,
fontWeight: ext.fontWeight,
color: ext.textPrimary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
author,
style: TextStyle(
fontSize: 11 * ext.fontScale,
color: ext.textHint,
),
),
],
),
);
},
),
),
],
);
}
/// 今日推荐骨架屏 — 加载态占位
Widget _buildRecommendSkeleton(AppThemeExtension ext) {
return Shimmer.fromColors(
baseColor: ext.isDark ? ext.bgCard : ext.bgSecondary,
highlightColor: ext.isDark ? ext.bgElevated : ext.bgPrimary,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
itemCount: 3,
separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
itemBuilder: (_, __) => GlassContainer(
width: 180,
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_skeletonBlock(ext, double.infinity, 12),
const SizedBox(height: AppSpacing.xs),
_skeletonBlock(ext, 120, 12),
const Spacer(),
_skeletonBlock(ext, 80, 10),
],
),
),
),
);
}
// ============================================================
// 快捷操作
// ============================================================
Widget _buildQuickActions(
BuildContext context,
TDashboard db,
AppThemeExtension ext,
bool reduceAnim,
) {
final actions = <({IconData icon, String label, String route})>[
(icon: CupertinoIcons.search, label: db.actionSearch, route: AppRoutes.search),
(icon: CupertinoIcons.star, label: db.actionFavorites, route: AppRoutes.favorites),
(icon: CupertinoIcons.book, label: db.actionReadLater, route: AppRoutes.readLater),
(icon: CupertinoIcons.clock, label: db.actionHistory, route: AppRoutes.history),
(icon: CupertinoIcons.checkmark_seal, label: db.actionSignin, route: AppRoutes.signin),
(icon: CupertinoIcons.chart_bar, label: db.actionReadingReport, route: AppRoutes.readingReport),
(icon: CupertinoIcons.cloud_sun, label: db.actionDailyCard, route: AppRoutes.dailyCard),
(icon: CupertinoIcons.settings, label: db.actionSettings, route: AppRoutes.generalSettings),
];
final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, CupertinoIcons.bolt, db.sectionQuickActions, ext),
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: animDuration,
child: SlideAnimation(
verticalOffset: reduceAnim ? 0 : 50.0,
child: FadeInAnimation(
child: GestureDetector(
onTap: () {
HapticFeedback.selectionClick();
context.appPush(entry.value.route);
},
child: GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppIcon(
cupertinoIcon: entry.value.icon,
size: 16,
animate: !reduceAnim,
animationDelay: entry.key * 50,
semanticLabel: entry.value.label,
),
const SizedBox(width: AppSpacing.xs),
Text(
entry.value.label,
style: TextStyle(
fontSize: 13 * ext.fontScale,
fontWeight: ext.fontWeight,
color: ext.textPrimary,
),
),
],
),
),
),
),
),
);
}).toList(),
),
),
],
);
}
// ============================================================
// 最近浏览
// ============================================================
Widget _buildRecentHistory(
BuildContext context,
WidgetRef ref,
TDashboard db,
AppThemeExtension ext,
bool reduceAnim,
) {
ref.watch(toolCenterRecentProvider);
final recentRoutes = RecentRouteService.getRecentRoutes();
final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, CupertinoIcons.clock, db.sectionRecent, ext),
const SizedBox(height: AppSpacing.sm),
recentRoutes.isEmpty
? _buildEmptyState(context, CupertinoIcons.tray, db.emptyRecent, ext)
: 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: animDuration,
child: SlideAnimation(
verticalOffset: reduceAnim ? 0 : 30.0,
child: FadeInAnimation(
child: Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: GestureDetector(
onTap: () {
HapticFeedback.selectionClick();
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 * ext.fontScale,
fontWeight: ext.fontWeight,
color: ext.textPrimary,
),
),
),
Icon(
CupertinoIcons.chevron_right,
size: 14,
color: ext.textHint,
),
],
),
),
),
),
),
),
);
}).toList(),
),
),
],
);
}
// ============================================================
// 数据统计
// ============================================================
Widget _buildStats(
BuildContext context,
WidgetRef ref,
TDashboard db,
AppThemeExtension ext,
bool reduceAnim,
) {
final authState = ref.watch(authProvider);
final favoriteState = ref.watch(favoriteProvider);
final likesState = ref.watch(likesProvider);
final signinState = ref.watch(signinProvider);
// 空指针防护 — provider 异常或字段缺失时回退 0
final readCount = authState.user?.signinDays ?? 0;
final favCount = favoriteState.total;
final likeCount = likesState.total;
final streakDays = signinState.continuous;
final streakText = '$streakDays${db.streakDayUnit}';
final stats = <(IconData, String, String)>[
(CupertinoIcons.book, db.statRead, '$readCount'),
(CupertinoIcons.star, db.statFavorites, '$favCount'),
(CupertinoIcons.hand_thumbsup, db.statLikes, '$likeCount'),
(CupertinoIcons.flame, db.statStreak, streakText),
];
final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(context, CupertinoIcons.chart_bar, db.sectionStats, ext),
const SizedBox(height: AppSpacing.sm),
AnimationLimiter(
child: Row(
children: stats.asMap().entries.map((entry) {
final stat = entry.value;
// Expanded 必须是 Row 的直接子节点,因此放在动画包裹链的最外层
return Expanded(
child: AnimationConfiguration.staggeredList(
position: entry.key,
duration: animDuration,
child: SlideAnimation(
verticalOffset: reduceAnim ? 0 : 50.0,
child: FadeInAnimation(
child: GlassContainer(
padding: const EdgeInsets.all(AppSpacing.sm),
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.xs / 2,
),
child: Column(
children: [
AppIcon(
cupertinoIcon: stat.$1,
style: AppIconStyle.accent,
animate: !reduceAnim,
animationType: AppIconAnimation.bounceIn,
animationDelay: entry.key * 80,
semanticLabel: stat.$2,
),
const SizedBox(height: 4),
Text(
stat.$3,
style: TextStyle(
fontSize: 16 * ext.fontScale,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
Text(
stat.$2,
style: TextStyle(
fontSize: 11 * ext.fontScale,
color: ext.textHint,
),
),
],
),
),
),
),
),
);
}).toList(),
),
),
],
);
}
// ============================================================
// 通用子组件
// ============================================================
/// 区块标题 — 图标 + 标题文字,统一强调色图标
Widget _buildSectionHeader(
BuildContext context,
IconData icon,
String title,
AppThemeExtension ext,
) {
return Row(
children: [
AppIcon(
cupertinoIcon: icon,
size: 16,
style: AppIconStyle.accent,
semanticLabel: title,
),
const SizedBox(width: 6),
Text(
title,
style: TextStyle(
fontSize: 16 * ext.fontScale,
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
),
],
);
}
/// 空状态占位 — 图标 + 提示文案
Widget _buildEmptyState(
BuildContext context,
IconData icon,
String message,
AppThemeExtension ext,
) {
return GlassContainer(
padding: const EdgeInsets.all(AppSpacing.md),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 28, color: ext.textHint),
const SizedBox(height: AppSpacing.xs),
Text(
message,
style: TextStyle(
fontSize: 13 * ext.fontScale,
color: ext.textHint,
),
),
],
),
),
);
}
/// 骨架屏色块 — 圆角灰色占位
Widget _skeletonBlock(AppThemeExtension ext, double width, double height) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: ext.textPrimary,
borderRadius: BorderRadius.circular(4),
),
);
}
}