318 lines
10 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|