Files

318 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/app_state.dart';
import '../../core/app_theme.dart';
import '../../core/courses/courses.dart';
import '../../core/sync/sync_coordinator.dart';
import '../../widgets/app_widgets.dart';
import 'ai_service_page.dart';
import 'assessment_list_page.dart';
import 'learning_records_page.dart';
import 'level_progress_page.dart';
import 'preference_pages.dart';
import 'profile_widgets.dart';
import 'recordings_page.dart';
import 'sync_settings_sheet.dart';
/// 我的:概览卡、升级进度,以及进入各二级页面的入口。
class ProfilePage extends StatelessWidget {
const ProfilePage({
super.key,
required this.state,
required this.onOpenAssessment,
required this.onOpenReview,
});
final AppState state;
final ValueChanged<AssessmentPack> onOpenAssessment;
final VoidCallback onOpenReview;
void _open(BuildContext context, String title, WidgetBuilder builder) =>
openProfilePage(context, state: state, title: title, builder: builder);
void _openLevel(BuildContext context) =>
_open(context, '升级进度', (_) => LevelProgressView(state: state));
void _openRecords(BuildContext context) =>
_open(context, '学习记录', (_) => LearningRecordsView(state: state));
@override
Widget build(BuildContext context) {
final passedPacks = state.passedAssessmentPackCount;
return AppPage(
child: SpacedColumn(
spacing: 16,
children: [
Text('我的', style: Theme.of(context).textTheme.headlineMedium),
_OverviewCard(
state: state,
onOpenLevel: () => _openLevel(context),
onOpenRecords: () => _openRecords(context),
onOpenReview: onOpenReview,
),
_StageCard(state: state, onTap: () => _openLevel(context)),
MenuGroup(
title: '学习',
children: [
MenuTile(
icon: Icons.trending_up,
title: '升级进度',
value: '${(state.a0StageProgress * 100).round()}%',
onTap: () => _openLevel(context),
),
MenuTile(
icon: Icons.quiz_outlined,
title: '四技能评估',
value: '$passedPacks/2 通过',
onTap: () => _open(
context,
'四技能评估',
(context) => AssessmentListView(
state: state,
onOpenAssessment: (pack) {
Navigator.of(context).pop();
onOpenAssessment(pack);
},
),
),
),
MenuTile(
icon: Icons.history,
title: '学习记录',
onTap: () => _openRecords(context),
),
MenuTile(
icon: Icons.library_music_outlined,
title: '我的录音',
onTap: () =>
_open(context, '我的录音', (_) => RecordingsView(state: state)),
),
],
),
MenuGroup(
title: '设置',
children: [
MenuTile(
icon: Icons.tune,
title: '学习偏好',
value: '${state.dailyMinutes} 分钟',
onTap: () => _open(
context,
'学习偏好',
(_) => LearningPreferencesView(state: state),
),
),
MenuTile(
icon: Icons.palette_outlined,
title: '外观',
value: themeModeLabel(state.themeMode),
onTap: () =>
_open(context, '外观', (_) => AppearanceView(state: state)),
),
ListenableBuilder(
listenable: SyncCoordinator.instance,
builder: (context, _) {
final loggedIn = SyncCoordinator.instance.isLoggedIn;
return MenuTile(
icon: loggedIn
? Icons.cloud_done_outlined
: Icons.cloud_queue_outlined,
iconColor: loggedIn ? AppColors.green : null,
title: '云同步',
value: loggedIn ? '已登录' : '未登录',
onTap: () => SyncSettingsSheet.show(context, state),
);
},
),
MenuTile(
icon: Icons.smart_toy_outlined,
title: 'AI 服务',
value: aiProviderLabel(state.aiProvider),
onTap: () =>
_open(context, 'AI 服务', (_) => AiServiceView(state: state)),
),
],
),
MenuGroup(
children: [
DangerAction(
label: '清除学习进度',
message:
'这会清除本机的课程/分段进度、草稿、复习队列、学习作答证据、掌握记录、对话草稿、AI 补练缓存和阶段评估结果,且不可恢复。',
details: '不会清除:已保存录音、AI API Key 与应用设置。录音请在“我的录音”中单独删除。',
acknowledgement: '我了解这些本机学习数据无法恢复',
onConfirm: state.clearProgress,
),
],
),
],
),
);
}
}
class _OverviewCard extends StatelessWidget {
const _OverviewCard({
required this.state,
required this.onOpenLevel,
required this.onOpenRecords,
required this.onOpenReview,
});
final AppState state;
final VoidCallback onOpenLevel;
final VoidCallback onOpenRecords;
final VoidCallback onOpenReview;
@override
Widget build(BuildContext context) {
final due = state.dueReviewCount;
return SectionCard(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: AppColors.softGreen,
child: Icon(Icons.person, color: AppColors.green),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.a0Passed ? 'A0 已通过' : 'A0 起步',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'每天 ${state.dailyMinutes} 分钟',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
],
),
Divider(height: 28, color: AppColors.line),
Row(
children: [
_Stat(
value: state.completedLessonIds.length,
label: '已学课',
onTap: onOpenRecords,
),
_Stat(
value: state.coreUsableCount,
label: '可使用',
onTap: onOpenLevel,
),
_Stat(
value: state.coreMasteredCount,
label: '已掌握',
onTap: onOpenLevel,
),
_Stat(
value: due,
label: '待复习',
color: due > 0 ? AppColors.warmInk : null,
onTap: onOpenReview,
),
],
),
],
),
);
}
}
class _Stat extends StatelessWidget {
const _Stat({
required this.value,
required this.label,
required this.onTap,
this.color,
});
final int value;
final String label;
final VoidCallback onTap;
final Color? color;
@override
Widget build(BuildContext context) => Expanded(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
children: [
Text(
'$value',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: color ?? AppColors.ink,
),
),
Text(label, style: TextStyle(fontSize: 12, color: AppColors.muted)),
],
),
),
),
);
}
/// 距离下一阶段的总进度,点击进入升级进度。
class _StageCard extends StatelessWidget {
const _StageCard({required this.state, required this.onTap});
final AppState state;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final missing = [
if (state.coreUsableCount < a0UsableGoal)
'${a0UsableGoal - state.coreUsableCount} 项可使用',
if (state.coreMasteredCount < a0MasteredGoal)
'${a0MasteredGoal - state.coreMasteredCount} 项掌握',
if (state.passedAssessmentPackCount < 2)
'${2 - state.passedAssessmentPackCount} 套评估',
];
return SectionCard(
tint: AppColors.softGreen,
onTap: onTap,
child: SpacedColumn(
spacing: 8,
children: [
Row(
children: [
const Expanded(
child: Text(
'距离 A1',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
Text(
'${(state.a0StageProgress * 100).round()}%',
style: TextStyle(color: AppColors.green),
),
Icon(Icons.chevron_right, color: AppColors.green),
],
),
ProgressBar(
value: state.a0StageProgress,
background: AppColors.surface,
),
Text(
missing.isEmpty
? 'A0 已通过。A1 主线内容尚未提供,可继续进行 A0 巩固。'
: '还需:${missing.join(' · ')}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
}