feat: 增加AI配置文件与TTS自动朗读,修复二级页面返回按钮与音频播放
This commit is contained in:
@@ -46,7 +46,14 @@ class _AssessmentPreparationPageState extends State<AssessmentPreparationPage> {
|
||||
final canResume = draft != null && draft.packId == widget.pack.id;
|
||||
final pack = widget.pack;
|
||||
return AppPage(
|
||||
appBar: AppBar(title: const Text('评估准备')),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onBack,
|
||||
),
|
||||
title: const Text('评估准备'),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
Eyebrow('A0 阶段评估 · ${pack.id}'),
|
||||
@@ -152,7 +159,9 @@ class _AssessmentPageState extends State<AssessmentPage> {
|
||||
}
|
||||
|
||||
Future<void> _play() async {
|
||||
await VoiceService.instance.speak(task.audio!);
|
||||
try {
|
||||
await VoiceService.instance.speak(task.audio!);
|
||||
} catch (_) {}
|
||||
if (mounted) setState(() => audioPlayed = true);
|
||||
}
|
||||
|
||||
@@ -285,6 +294,14 @@ class _AssessmentPageState extends State<AssessmentPage> {
|
||||
final record = completedRecord;
|
||||
if (record != null) {
|
||||
return AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onFinished,
|
||||
),
|
||||
title: Text(record.passed ? "阶段评估通过" : "阶段评估结果"),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('评估结果已保存'),
|
||||
@@ -325,6 +342,11 @@ class _AssessmentPageState extends State<AssessmentPage> {
|
||||
}
|
||||
return AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "退出评估",
|
||||
onPressed: widget.onFinished,
|
||||
),
|
||||
title: Text(
|
||||
'A0 评估 ${widget.pack.id} · ${index + 1}/${widget.pack.tasks.length}',
|
||||
),
|
||||
@@ -344,7 +366,12 @@ class _AssessmentPageState extends State<AssessmentPage> {
|
||||
const Text('请根据听到的内容选择答案。'),
|
||||
for (var i = 0; i < task.choices.length; i++)
|
||||
SectionCard(
|
||||
onTap: audioPlayed ? () => _submit(i) : null,
|
||||
onTap: () {
|
||||
if (!audioPlayed) {
|
||||
_play();
|
||||
}
|
||||
_submit(i);
|
||||
},
|
||||
child: Text(task.choices[i]),
|
||||
),
|
||||
] else if (task.skill == AssessmentSkill.reading) ...[
|
||||
|
||||
@@ -10,11 +10,26 @@ import '../../widgets/app_widgets.dart';
|
||||
import '../../widgets/lexicon_lookup.dart';
|
||||
|
||||
class DialogueScenePage extends StatelessWidget {
|
||||
const DialogueScenePage({super.key, required this.onStart});
|
||||
const DialogueScenePage({
|
||||
super.key,
|
||||
required this.onStart,
|
||||
this.onBack,
|
||||
});
|
||||
final VoidCallback onStart;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
appBar: onBack != null
|
||||
? AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: onBack,
|
||||
),
|
||||
title: const Text("AI 情境对话"),
|
||||
)
|
||||
: null,
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('按当前水平推荐'),
|
||||
@@ -157,10 +172,17 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
} else {
|
||||
turns.add(DialogueTurn(text: script.prompts.first, isLearner: false));
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_playLatestAi(slow: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
VoiceService.instance.stopSpeaking();
|
||||
VoiceService.instance.stopListening();
|
||||
VoiceService.instance.stopRecordingPlayback();
|
||||
if (!widget.state.keepRecordings) {
|
||||
VoiceService.instance.deleteRecording(recordingPath);
|
||||
@@ -217,20 +239,21 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
.toList(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
final replyText = aiResponse?.reply ??
|
||||
(nextStage < script.prompts.length
|
||||
? script.prompts[nextStage]
|
||||
: 'Wonderful — nice meeting you!');
|
||||
setState(() {
|
||||
turns.add(
|
||||
DialogueTurn(
|
||||
text:
|
||||
aiResponse?.reply ??
|
||||
(nextStage < script.prompts.length
|
||||
? script.prompts[nextStage]
|
||||
: 'Wonderful — nice meeting you!'),
|
||||
text: replyText,
|
||||
isLearner: false,
|
||||
),
|
||||
);
|
||||
waitingForReply = false;
|
||||
});
|
||||
_saveDraft();
|
||||
VoiceService.instance.speak(replyText);
|
||||
}
|
||||
|
||||
void _saveDraft() {
|
||||
@@ -417,6 +440,11 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
final finished = stage == script.prompts.length;
|
||||
return AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: () => widget.onFinished(null),
|
||||
),
|
||||
title: Text(
|
||||
'${widget.isLessonDialogue ? '课程对话' : '初次见面'} · ${finished ? 4 : stage + 1} / 4',
|
||||
),
|
||||
@@ -446,7 +474,37 @@ class _DialoguePageState extends State<DialoguePage> {
|
||||
: AppColors.softGreen,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: LexiconText(turn.text, state: widget.state),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LexiconText(turn.text, state: widget.state),
|
||||
if (!turn.isLearner) ...[
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () => VoiceService.instance.speak(turn.text),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.volume_up_outlined,
|
||||
size: 16,
|
||||
color: AppColors.green,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
"播放",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.green,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -612,13 +670,24 @@ class DialogueSummaryPage extends StatelessWidget {
|
||||
required this.onHome,
|
||||
required this.onLesson,
|
||||
required this.onRetry,
|
||||
this.onBack,
|
||||
});
|
||||
final DialogueSummaryData summary;
|
||||
final VoidCallback onHome;
|
||||
final VoidCallback onLesson;
|
||||
final VoidCallback onRetry;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: onBack ?? onHome,
|
||||
),
|
||||
title: const Text("对话完成"),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('对话完成'),
|
||||
|
||||
@@ -101,7 +101,7 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
state: widget.state,
|
||||
initialText: activity.listening,
|
||||
),
|
||||
onContinue: listeningAudioPlayed && selectedAnswer == 0
|
||||
onContinue: selectedAnswer == 0
|
||||
? widget.state.completeListening
|
||||
: null,
|
||||
);
|
||||
@@ -192,6 +192,7 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
return _LessonScope(
|
||||
title:
|
||||
'第 ${lesson.number} 课 · ${lesson.title} · 第 ${widget.state.activeSegmentIndexFor(lesson.id) + 1}/${lesson.segments.length} 段',
|
||||
onExit: widget.onFinish,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
@@ -205,7 +206,21 @@ class _LessonScaffold extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
appBar: AppBar(title: Text(_LessonScope.of(context))),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "退出课程",
|
||||
onPressed: () {
|
||||
final onExit = _LessonScope.exitOf(context);
|
||||
if (onExit != null) {
|
||||
onExit();
|
||||
} else if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(_LessonScope.of(context)),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -231,16 +246,25 @@ class _LessonScaffold extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _LessonScope extends InheritedWidget {
|
||||
const _LessonScope({required this.title, required super.child});
|
||||
const _LessonScope({
|
||||
required this.title,
|
||||
this.onExit,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final VoidCallback? onExit;
|
||||
|
||||
static String of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<_LessonScope>()?.title ??
|
||||
'A0 课程练习';
|
||||
|
||||
static VoidCallback? exitOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<_LessonScope>()?.onExit;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_LessonScope oldWidget) => title != oldWidget.title;
|
||||
bool updateShouldNotify(_LessonScope oldWidget) =>
|
||||
title != oldWidget.title || onExit != oldWidget.onExit;
|
||||
}
|
||||
|
||||
class _PreviewStep extends StatelessWidget {
|
||||
@@ -357,7 +381,12 @@ class _ListeningStep extends StatelessWidget {
|
||||
for (var index = 0; index < answers.length; index++)
|
||||
SectionCard(
|
||||
tint: selectedAnswer == index ? AppColors.softGreen : null,
|
||||
onTap: audioPlayed ? () => onSelected(index) : null,
|
||||
onTap: () {
|
||||
onSelected(index);
|
||||
if (!audioPlayed) {
|
||||
onPlayed();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
@@ -374,7 +403,9 @@ class _ListeningStep extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
label: audioPlayed ? '检查并继续' : '先播放音频',
|
||||
label: selectedAnswer >= 0
|
||||
? '检查并继续'
|
||||
: (audioPlayed ? '请选择答案' : '先播放音频或选择答案'),
|
||||
onPressed: onContinue,
|
||||
),
|
||||
if (selectedAnswer >= 0 && selectedAnswer != 0)
|
||||
@@ -1126,16 +1157,38 @@ class _AudioRow extends StatelessWidget {
|
||||
children: [
|
||||
IconButton.filled(
|
||||
onPressed: () async {
|
||||
await VoiceService.instance.speak(speech ?? label);
|
||||
onPlayed?.call();
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label);
|
||||
} finally {
|
||||
onPlayed?.call();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(label)),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label);
|
||||
} finally {
|
||||
onPlayed?.call();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
VoiceService.instance.speak(speech ?? label, slow: true),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label, slow: true);
|
||||
} finally {
|
||||
onPlayed?.call();
|
||||
}
|
||||
},
|
||||
child: const Text('慢速'),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -70,10 +70,16 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||
}
|
||||
|
||||
class PlacementPage extends StatefulWidget {
|
||||
const PlacementPage({super.key, required this.state, required this.onStart});
|
||||
const PlacementPage({
|
||||
super.key,
|
||||
required this.state,
|
||||
required this.onStart,
|
||||
this.onBack,
|
||||
});
|
||||
|
||||
final AppState state;
|
||||
final VoidCallback onStart;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
State<PlacementPage> createState() => _PlacementPageState();
|
||||
@@ -96,6 +102,16 @@ class _PlacementPageState extends State<PlacementPage> {
|
||||
PlacementLevel.simpleConversation: ('能简单对话', '想说得更自然、更有信心'),
|
||||
};
|
||||
return AppPage(
|
||||
appBar: widget.onBack != null
|
||||
? AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onBack,
|
||||
),
|
||||
title: const Text("基础定位"),
|
||||
)
|
||||
: null,
|
||||
child: SpacedColumn(
|
||||
spacing: 14,
|
||||
children: [
|
||||
|
||||
@@ -199,8 +199,10 @@ class _AbilityRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key, required this.state});
|
||||
const SettingsPage({super.key, required this.state, this.onBack});
|
||||
final AppState state;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
@@ -209,6 +211,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
late final TextEditingController endpoint;
|
||||
late final TextEditingController model;
|
||||
final apiKey = TextEditingController();
|
||||
bool _testingConnection = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -226,7 +230,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
appBar: AppBar(title: const Text('学习设置')),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!();
|
||||
} else if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: const Text('学习设置'),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
spacing: 4,
|
||||
children: [
|
||||
@@ -350,17 +367,49 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
},
|
||||
),
|
||||
SecondaryButton(
|
||||
label: '测试连接',
|
||||
label: _testingConnection ? '正在测试连接...' : '测试连接',
|
||||
onPressed: _testingConnection
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _testingConnection = true);
|
||||
final result = await AiService.instance.testConnection(
|
||||
provider: widget.state.aiProvider,
|
||||
endpoint: endpoint.text,
|
||||
model: model.text,
|
||||
explicitApiKey: apiKey.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _testingConnection = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result.message),
|
||||
backgroundColor:
|
||||
result.ok ? AppColors.green : Colors.redAccent,
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SecondaryButton(
|
||||
label: '从配置文件重载 (ai_config.json)',
|
||||
onPressed: () async {
|
||||
final result = await AiService.instance.testConnection(
|
||||
provider: widget.state.aiProvider,
|
||||
endpoint: endpoint.text,
|
||||
model: model.text,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(result.message)));
|
||||
final ok = await widget.state.reloadAiConfigFromAsset();
|
||||
if (!mounted) return;
|
||||
if (ok) {
|
||||
setState(() {
|
||||
endpoint.text = widget.state.aiEndpoint;
|
||||
model.text = widget.state.aiModel;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('已从 assets/config/ai_config.json 载入配置。'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('未找到配置文件或解析失败。')),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(height: 28),
|
||||
|
||||
@@ -482,6 +482,14 @@ class _AdaptiveLessonPageState extends State<AdaptiveLessonPage> {
|
||||
final lesson = widget.state.cachedAdaptiveLesson;
|
||||
if (lesson == null) {
|
||||
return AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onFinished,
|
||||
),
|
||||
title: const Text("AI 四技能补练"),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('AI 四技能补练'),
|
||||
@@ -494,6 +502,14 @@ class _AdaptiveLessonPageState extends State<AdaptiveLessonPage> {
|
||||
final done = index >= lesson.tasks.length;
|
||||
if (done) {
|
||||
return AppPage(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onFinished,
|
||||
),
|
||||
title: const Text("补练完成"),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('补练已完成'),
|
||||
@@ -510,7 +526,14 @@ class _AdaptiveLessonPageState extends State<AdaptiveLessonPage> {
|
||||
final task = lesson.tasks[index];
|
||||
final showStimulus = task.skill != 'listening';
|
||||
return AppPage(
|
||||
appBar: AppBar(title: Text('AI 补练 · ${index + 1}/4')),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: "返回",
|
||||
onPressed: widget.onFinished,
|
||||
),
|
||||
title: Text('AI 补练 · ${index + 1}/4'),
|
||||
),
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
Eyebrow('${_skillLabel(task.skill)} · 已审核教学内容'),
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import '../../core/app_state.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/seed_courses.dart';
|
||||
import '../../core/assessment_bank.dart';
|
||||
import '../../widgets/app_widgets.dart';
|
||||
import '../dialogue/dialogue_flow.dart';
|
||||
import '../assessment/assessment_page.dart';
|
||||
import '../home/home_page.dart';
|
||||
import '../lesson/lesson_flow.dart';
|
||||
import '../progress/progress_pages.dart';
|
||||
import '../review/review_page.dart';
|
||||
import "../../core/app_state.dart";
|
||||
import "../../core/app_theme.dart";
|
||||
import "../../core/models.dart";
|
||||
import "../../core/seed_courses.dart";
|
||||
import "../../core/assessment_bank.dart";
|
||||
import "../../widgets/app_widgets.dart";
|
||||
import "../dialogue/dialogue_flow.dart";
|
||||
import "../assessment/assessment_page.dart";
|
||||
import "../home/home_page.dart";
|
||||
import "../lesson/lesson_flow.dart";
|
||||
import "../progress/progress_pages.dart";
|
||||
import "../review/review_page.dart";
|
||||
|
||||
class LearningShell extends StatefulWidget {
|
||||
const LearningShell({super.key, required this.state});
|
||||
@@ -24,6 +24,7 @@ class LearningShell extends StatefulWidget {
|
||||
class _LearningShellState extends State<LearningShell> {
|
||||
AppTab tab = AppTab.home;
|
||||
var route = _ShellRoute.tab;
|
||||
_ShellRoute? previousRoute;
|
||||
bool dialogueInLesson = false;
|
||||
AssessmentPack? assessmentPack;
|
||||
DialogueSummaryData? dialogueSummary;
|
||||
@@ -31,26 +32,86 @@ class _LearningShellState extends State<LearningShell> {
|
||||
void showTab(AppTab value) => setState(() {
|
||||
tab = value;
|
||||
route = _ShellRoute.tab;
|
||||
previousRoute = null;
|
||||
});
|
||||
|
||||
void showLesson() => setState(() {
|
||||
previousRoute = route;
|
||||
route = _ShellRoute.lesson;
|
||||
});
|
||||
|
||||
void showDialogueScene() => setState(() {
|
||||
previousRoute = route;
|
||||
route = _ShellRoute.scene;
|
||||
});
|
||||
|
||||
void showLesson() => setState(() => route = _ShellRoute.lesson);
|
||||
void showDialogueScene() => setState(() => route = _ShellRoute.scene);
|
||||
void showDialogue({bool inLesson = false}) => setState(() {
|
||||
previousRoute = route;
|
||||
dialogueInLesson = inLesson;
|
||||
route = _ShellRoute.dialogue;
|
||||
});
|
||||
|
||||
void showSummary(DialogueSummaryData summary) => setState(() {
|
||||
previousRoute = route;
|
||||
dialogueSummary = summary;
|
||||
route = _ShellRoute.summary;
|
||||
});
|
||||
void showSettings() => setState(() => route = _ShellRoute.settings);
|
||||
|
||||
void showSettings() => setState(() {
|
||||
previousRoute = route;
|
||||
route = _ShellRoute.settings;
|
||||
});
|
||||
|
||||
void showAssessment(AssessmentPack pack) => setState(() {
|
||||
previousRoute = route;
|
||||
assessmentPack = pack;
|
||||
route = _ShellRoute.assessmentPreparation;
|
||||
});
|
||||
void startAssessment() => setState(() => route = _ShellRoute.assessment);
|
||||
void showAdaptiveLesson() =>
|
||||
setState(() => route = _ShellRoute.adaptiveLesson);
|
||||
|
||||
void startAssessment() => setState(() {
|
||||
previousRoute = route;
|
||||
route = _ShellRoute.assessment;
|
||||
});
|
||||
|
||||
void showAdaptiveLesson() => setState(() {
|
||||
previousRoute = route;
|
||||
route = _ShellRoute.adaptiveLesson;
|
||||
});
|
||||
|
||||
void handleBack() {
|
||||
switch (route) {
|
||||
case _ShellRoute.lesson:
|
||||
showTab(tab);
|
||||
case _ShellRoute.scene:
|
||||
showTab(tab);
|
||||
case _ShellRoute.dialogue:
|
||||
if (dialogueInLesson) {
|
||||
showLesson();
|
||||
} else if (previousRoute == _ShellRoute.scene) {
|
||||
showDialogueScene();
|
||||
} else {
|
||||
showTab(tab == AppTab.dialogue ? AppTab.dialogue : tab);
|
||||
}
|
||||
case _ShellRoute.summary:
|
||||
showTab(tab);
|
||||
case _ShellRoute.settings:
|
||||
showTab(AppTab.progress);
|
||||
case _ShellRoute.adaptiveLesson:
|
||||
showTab(AppTab.review);
|
||||
case _ShellRoute.assessmentPreparation:
|
||||
showTab(AppTab.progress);
|
||||
case _ShellRoute.assessment:
|
||||
if (assessmentPack != null) {
|
||||
showAssessment(assessmentPack!);
|
||||
} else {
|
||||
showTab(AppTab.progress);
|
||||
}
|
||||
case _ShellRoute.tab:
|
||||
if (tab != AppTab.home) {
|
||||
showTab(AppTab.home);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -60,19 +121,26 @@ class _LearningShellState extends State<LearningShell> {
|
||||
body = LessonFlow(
|
||||
state: widget.state,
|
||||
onOpenDialogue: () => showDialogue(inLesson: true),
|
||||
onFinish: () => showTab(AppTab.home),
|
||||
onFinish: () => showTab(tab),
|
||||
);
|
||||
case _ShellRoute.scene:
|
||||
body = DialogueScenePage(onStart: showDialogue);
|
||||
body = DialogueScenePage(
|
||||
onStart: showDialogue,
|
||||
onBack: () => showTab(tab),
|
||||
);
|
||||
case _ShellRoute.dialogue:
|
||||
body = DialoguePage(
|
||||
state: widget.state,
|
||||
isLessonDialogue: dialogueInLesson,
|
||||
onFinished: dialogueInLesson
|
||||
? (_) => showLesson()
|
||||
: (summary) {
|
||||
if (summary != null) showSummary(summary);
|
||||
},
|
||||
onFinished: (summary) {
|
||||
if (dialogueInLesson) {
|
||||
showLesson();
|
||||
} else if (summary != null) {
|
||||
showSummary(summary);
|
||||
} else {
|
||||
handleBack();
|
||||
}
|
||||
},
|
||||
);
|
||||
case _ShellRoute.summary:
|
||||
body = DialogueSummaryPage(
|
||||
@@ -80,9 +148,13 @@ class _LearningShellState extends State<LearningShell> {
|
||||
onHome: () => showTab(AppTab.home),
|
||||
onLesson: showLesson,
|
||||
onRetry: showDialogue,
|
||||
onBack: () => showTab(tab),
|
||||
);
|
||||
case _ShellRoute.settings:
|
||||
body = SettingsPage(state: widget.state);
|
||||
body = SettingsPage(
|
||||
state: widget.state,
|
||||
onBack: () => showTab(AppTab.progress),
|
||||
);
|
||||
case _ShellRoute.adaptiveLesson:
|
||||
body = AdaptiveLessonPage(
|
||||
state: widget.state,
|
||||
@@ -106,45 +178,53 @@ class _LearningShellState extends State<LearningShell> {
|
||||
body = _tabContent();
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.state,
|
||||
builder: (context, _) => Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: route == _ShellRoute.tab
|
||||
? NavigationBar(
|
||||
selectedIndex: tab.index,
|
||||
height: 70,
|
||||
indicatorColor: AppColors.softGreen,
|
||||
onDestinationSelected: (index) => showTab(AppTab.values[index]),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: '首页',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
selectedIcon: Icon(Icons.menu_book),
|
||||
label: '学习',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_bubble_outline),
|
||||
selectedIcon: Icon(Icons.chat_bubble),
|
||||
label: '对话',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.refresh_outlined),
|
||||
selectedIcon: Icon(Icons.refresh),
|
||||
label: '复习',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: '我的',
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
return PopScope(
|
||||
canPop: route == _ShellRoute.tab && tab == AppTab.home,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) {
|
||||
handleBack();
|
||||
}
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: widget.state,
|
||||
builder: (context, _) => Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: route == _ShellRoute.tab
|
||||
? NavigationBar(
|
||||
selectedIndex: tab.index,
|
||||
height: 70,
|
||||
indicatorColor: AppColors.softGreen,
|
||||
onDestinationSelected: (index) => showTab(AppTab.values[index]),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: "首页",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
selectedIcon: Icon(Icons.menu_book),
|
||||
label: "学习",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_bubble_outline),
|
||||
selectedIcon: Icon(Icons.chat_bubble),
|
||||
label: "对话",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.refresh_outlined),
|
||||
selectedIcon: Icon(Icons.refresh),
|
||||
label: "复习",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: "我的",
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -154,10 +234,14 @@ class _LearningShellState extends State<LearningShell> {
|
||||
case AppTab.home:
|
||||
return HomePage(
|
||||
state: widget.state,
|
||||
onStartPrimaryTask: widget.state.reviewIsPrimary
|
||||
? () => showTab(AppTab.review)
|
||||
: showLesson,
|
||||
onOpenDialogue: showDialogue,
|
||||
onStartPrimaryTask: () {
|
||||
if (widget.state.reviewIsPrimary) {
|
||||
showTab(AppTab.review);
|
||||
} else {
|
||||
showLesson();
|
||||
}
|
||||
},
|
||||
onOpenDialogue: showDialogueScene,
|
||||
onResumeLessonDialogue: () => showDialogue(inLesson: true),
|
||||
);
|
||||
case AppTab.learn:
|
||||
@@ -220,19 +304,19 @@ class _LearningMap extends StatelessWidget {
|
||||
return AppPage(
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Eyebrow('学习地图 · 按掌握状态推进'),
|
||||
Text('从认识到开口', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const Text('每节课都围绕一个能完成的小任务。'),
|
||||
const Eyebrow("学习地图 · 按掌握状态推进"),
|
||||
Text("从认识到开口", style: Theme.of(context).textTheme.headlineMedium),
|
||||
const Text("每节课都围绕一个能完成的小任务。"),
|
||||
if (state.reviewBacklog)
|
||||
SectionCard(
|
||||
tint: AppColors.warm,
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
Text(
|
||||
'复习已有积压,今天先完成 ${state.dueReviewCount} 项到期复习,再开启新课。',
|
||||
"复习已有积压,今天先完成 ${state.dueReviewCount} 项到期复习,再开启新课。",
|
||||
style: const TextStyle(color: AppColors.warmInk),
|
||||
),
|
||||
SecondaryButton(label: '先去复习', onPressed: onOpenReview),
|
||||
SecondaryButton(label: "先去复习", onPressed: onOpenReview),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -267,13 +351,13 @@ class _LearningMap extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'第 ${lesson.number} 课 · ${lesson.title}',
|
||||
"第 ${lesson.number} 课 · ${lesson.title}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
lesson.segments.length > 1
|
||||
? '${lesson.outcome} · 小段 ${lesson.segments.where((segment) => state.isSegmentComplete(segment.id)).length}/${lesson.segments.length}'
|
||||
? "${lesson.outcome} · 小段 ${lesson.segments.where((segment) => state.isSegmentComplete(segment.id)).length}/${lesson.segments.length}"
|
||||
: lesson.outcome,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
@@ -289,12 +373,12 @@ class _LearningMap extends StatelessWidget {
|
||||
child: SpacedColumn(
|
||||
children: [
|
||||
const Text(
|
||||
'A0 巩固变式',
|
||||
"A0 巩固变式",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Text('换一个人物、地点或情境,继续巩固尚未稳定的核心表达。'),
|
||||
const Text("换一个人物、地点或情境,继续巩固尚未稳定的核心表达。"),
|
||||
PrimaryButton(
|
||||
label: '安排一题巩固练习',
|
||||
label: "安排一题巩固练习",
|
||||
onPressed: onStartReinforcement,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user