Files
xianyan/lib/features/task/daily_task_page.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

379 lines
13 KiB
Dart
Raw Permalink 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.
/// @name 每日任务页面
/// @date 2026-05-14
/// @desc 展示今日任务列表+进度+领取+完美日
/// @update 2026-06-19 类型安全修复(int vs num): _showRewardDialog 使用 SafeJson.parseInt
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/utils/safe_json.dart';
import 'task_core.dart';
import '../../shared/widgets/cards/task_card.dart';
import '../../shared/widgets/containers/glass_container.dart';
import '../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/app_typography.dart';
import '../../core/theme/app_radius.dart';
import '../../l10n/translations.dart';
class DailyTaskPage extends ConsumerStatefulWidget {
const DailyTaskPage({super.key});
@override
ConsumerState<DailyTaskPage> createState() => _DailyTaskPageState();
}
class _DailyTaskPageState extends ConsumerState<DailyTaskPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(taskProvider.notifier).loadTodayTasks();
});
}
@override
Widget build(BuildContext context) {
final taskState = ref.watch(taskProvider);
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
t.profile.dailyTask,
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
child: taskState.isLoading
? Center(child: CupertinoActivityIndicator(color: ext.iconSecondary))
: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// ---- 摘要卡片 ----
if (taskState.summary != null)
_buildSummary(taskState.summary!, ext, t),
// ---- 错误提示 ----
if (taskState.error != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Text(
taskState.error!,
style: AppTypography.body.copyWith(
color: ext.errorColor,
),
textAlign: TextAlign.center,
),
),
),
// ---- 空状态 ----
if (taskState.tasks.isEmpty && taskState.error == null)
SliverToBoxAdapter(child: _buildEmptyState(ext, t)),
// ---- 任务列表 ----
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final task = taskState.tasks[index];
return TaskCard(
icon: task.icon,
name: task.name,
progress: task.progress,
target: task.target,
percent: task.percent,
completed: task.completed,
claimed: task.claimed,
expReward: task.expReward,
scoreReward: task.scoreReward,
onClaim: task.completed && !task.claimed
? () => _claimTask(task.id, task.name)
: null,
);
}, childCount: taskState.tasks.length),
),
// ---- 完美日卡片 ----
if (taskState.summary?.isPerfectDay == true &&
taskState.summary?.perfectClaimed == false)
SliverToBoxAdapter(child: _buildPerfectDayCard(ext, t)),
const SliverPadding(
padding: EdgeInsets.only(bottom: AppSpacing.xl + AppSpacing.sm),
),
],
),
),
);
}
// ============================================================
// 摘要卡片 — 使用 GlassContainer
// ============================================================
Widget _buildSummary(TaskSummary summary, AppThemeExtension ext, T t) {
return SliverToBoxAdapter(
child: GlassContainer(
depth: GlassDepth.elevated,
margin: const EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.sm + AppSpacing.xs,
AppSpacing.md, AppSpacing.xs,
),
child: Column(
children: [
// ---- 统计行 ----
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
CupertinoIcons.list_bullet, t.profile.totalTasks,
summary.total, ext,
),
_buildStatItem(
CupertinoIcons.checkmark_circle_fill, t.progress.completed,
summary.completed, ext,
),
_buildStatItem(
CupertinoIcons.gift_fill, t.profile.taskClaimed,
summary.claimed, ext,
),
],
),
const SizedBox(height: AppSpacing.sm),
// ---- 进度条 ----
ClipRRect(
borderRadius: AppRadius.smBorder,
child: LinearProgressIndicator(
value: summary.total > 0
? summary.completed / summary.total
: 0,
backgroundColor: ext.isDark ? ext.bgSecondary : ext.bgElevated,
valueColor: AlwaysStoppedAnimation<Color>(
summary.isPerfectDay ? ext.warningColor : ext.infoColor,
),
minHeight: 8,
),
),
// ---- 完美日提示 ----
if (summary.isPerfectDay) ...[
const SizedBox(height: AppSpacing.sm),
Row(
children: [
Icon(
CupertinoIcons.sparkles,
size: 16,
color: ext.warningColor,
),
const SizedBox(width: AppSpacing.xs + AppSpacing.xs),
Text(
'${t.profile.perfectDay}${t.profile.perfectDayAllDone}',
style: AppTypography.subhead.copyWith(
fontWeight: FontWeight.w600,
color: ext.warningColor,
),
),
],
),
],
],
),
),
);
}
// ============================================================
// 统计项
// ============================================================
Widget _buildStatItem(
IconData icon,
String label,
int value,
AppThemeExtension ext,
) {
return Column(
children: [
Icon(icon, size: 22, color: ext.iconSecondary),
const SizedBox(height: AppSpacing.xs),
Text(
'$value',
style: AppTypography.headline.copyWith(
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
Text(
label,
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
),
],
);
}
// ============================================================
// 空状态
// ============================================================
Widget _buildEmptyState(AppThemeExtension ext, T t) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xxl),
child: Column(
children: [
Icon(
CupertinoIcons.checkmark_seal,
size: 64,
color: ext.iconDisabled,
),
const SizedBox(height: AppSpacing.md),
Text(
t.profile.noTasks,
style: AppTypography.title3.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.xs),
Text(
t.profile.noTasksDesc,
style: AppTypography.subhead.copyWith(color: ext.textHint),
textAlign: TextAlign.center,
),
],
),
);
}
// ============================================================
// 完美日卡片 — 渐变风格
// ============================================================
Widget _buildPerfectDayCard(AppThemeExtension ext, T t) {
return Container(
margin: const EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.md, AppSpacing.md, AppSpacing.xs,
),
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.warningColor, ext.destructiveColor],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: AppRadius.xlBorder,
boxShadow: [
BoxShadow(
color: ext.warningColor.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
children: [
const Icon(
CupertinoIcons.star_fill,
size: 20,
color: CupertinoColors.white,
),
const SizedBox(width: AppSpacing.sm),
Text(
t.profile.perfectDayReward,
style: AppTypography.title3.copyWith(
fontWeight: FontWeight.bold,
color: ext.textInverse,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
t.profile.perfectDayRewardDesc,
style: AppTypography.subhead.copyWith(color: ext.textInverse),
),
const SizedBox(height: AppSpacing.xs),
Text(
'+20 ${t.profile.expUnit} +10 ${t.profile.scoreUnit}',
style: AppTypography.callout.copyWith(
fontWeight: FontWeight.w600,
color: ext.textInverse,
),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.xs,
),
borderRadius: AppRadius.mdBorder,
color: CupertinoColors.white,
onPressed: _claimPerfectDay,
child: Text(
t.profile.claimPerfectDayReward,
style: AppTypography.callout.copyWith(
fontWeight: FontWeight.w600,
color: ext.warningColor,
),
),
),
],
),
);
}
// ============================================================
// 领取任务奖励
// ============================================================
Future<void> _claimTask(int taskId, String taskName) async {
final result = await ref.read(taskProvider.notifier).claimReward(taskId);
if (result != null && mounted) {
_showRewardDialog(taskName, result);
}
}
// ============================================================
// 领取完美日奖励
// ============================================================
Future<void> _claimPerfectDay() async {
final t = ref.read(translationsProvider);
final result = await ref.read(taskProvider.notifier).claimPerfectDay();
if (result != null && mounted) {
_showRewardDialog(t.profile.perfectDay, result);
}
}
// ============================================================
// 奖励弹窗
// ============================================================
void _showRewardDialog(String name, Map<String, dynamic> data) {
final t = ref.read(translationsProvider);
final expReward = SafeJson.parseInt(data['exp_reward']);
final scoreReward = SafeJson.parseInt(data['score_reward']);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text('$name ${t.profile.rewardSuffix}'),
content: Column(
children: [
const SizedBox(height: AppSpacing.sm),
if (expReward > 0)
Text('+$expReward ${t.profile.expUnit}'),
if (scoreReward > 0)
Text('+$scoreReward ${t.profile.scoreUnit}'),
],
),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(t.profile.great),
),
],
),
);
}
}