1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
379 lines
13 KiB
Dart
379 lines
13 KiB
Dart
/// @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),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|