From 29b17381627da3289d1aabb8ce91e32bced7a122 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 16 Sep 2026 17:24:29 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E9=A1=B5=20build=EF=BC=8C=E6=8A=BD=E5=87=BA=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=B0=94=E6=B3=A1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _TurnBubble:单条对话气泡(翻译、播放/翻译/句型解析操作) - _TurnAction:气泡下方的图标文字按钮,替代三段重复布局 - 提示按钮逻辑移到 _showHint() _DialoguePageState.build 从约 320 行降到约 170 行,无用户可见变化。 Co-Authored-By: Claude Opus 5 --- .../lib/features/dialogue/dialogue_flow.dart | 324 +++++++++--------- 1 file changed, 158 insertions(+), 166 deletions(-) diff --git a/kouyu_english/lib/features/dialogue/dialogue_flow.dart b/kouyu_english/lib/features/dialogue/dialogue_flow.dart index 8a64b15..2536895 100644 --- a/kouyu_english/lib/features/dialogue/dialogue_flow.dart +++ b/kouyu_english/lib/features/dialogue/dialogue_flow.dart @@ -11,11 +11,7 @@ import '../../widgets/lexicon_lookup.dart'; import '../../widgets/voice_answer.dart'; class DialogueScenePage extends StatelessWidget { - const DialogueScenePage({ - super.key, - required this.onStart, - this.onBack, - }); + const DialogueScenePage({super.key, required this.onStart, this.onBack}); final VoidCallback onStart; final VoidCallback? onBack; @@ -174,7 +170,8 @@ class _DialoguePageState extends State // 3. Check all lesson dialogues for (final dialogue in a0Dialogues.values) { for (var i = 0; i < dialogue.prompts.length; i++) { - if (dialogue.prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) { + if (dialogue.prompts[i].trim().toLowerCase() == + cleanText.toLowerCase()) { if (i < dialogue.translations.length) { return dialogue.translations[i]; } @@ -184,7 +181,8 @@ class _DialoguePageState extends State // 4. Check all segment dialogues for (final dialogue in a0SegmentDialogues.values) { for (var i = 0; i < dialogue.prompts.length; i++) { - if (dialogue.prompts[i].trim().toLowerCase() == cleanText.toLowerCase()) { + if (dialogue.prompts[i].trim().toLowerCase() == + cleanText.toLowerCase()) { if (i < dialogue.translations.length) { return dialogue.translations[i]; } @@ -261,6 +259,7 @@ class _DialoguePageState extends State controller.dispose(); super.dispose(); } + void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { @@ -273,7 +272,6 @@ class _DialoguePageState extends State }); } - Future send() async { final text = controller.text.trim(); if (text.isEmpty || stage >= script.prompts.length || waitingForReply) { @@ -281,7 +279,8 @@ class _DialoguePageState extends State } if (!_matchesCurrentTask(text)) { setState( - () => validationError = '这一轮要“${_currentTaskLabel()}”,这句还没做到。' + () => validationError = + '这一轮要“${_currentTaskLabel()}”,这句还没做到。' '可以点“提示”看示范,再补充一次。', ); return; @@ -332,7 +331,8 @@ class _DialoguePageState extends State .toList(), ); if (!mounted) return; - final replyText = aiResponse?.reply ?? + final replyText = + aiResponse?.reply ?? (nextStage < script.prompts.length ? script.prompts[nextStage] : _closingLine); @@ -469,8 +469,9 @@ class _DialoguePageState extends State text: turn.text, ); if (!mounted) return; - final finalTrans = - (fetched != null && fetched.isNotEmpty) ? fetched : "暂无该句中文翻译"; + final finalTrans = (fetched != null && fetched.isNotEmpty) + ? fetched + : "暂无该句中文翻译"; setState(() { turns[index] = turn.copyWith(translation: finalTrans); }); @@ -513,8 +514,9 @@ class _DialoguePageState extends State text: latestAi.text, ); if (!mounted) return; - final finalTrans = - (fetched != null && fetched.isNotEmpty) ? fetched : "暂无该句中文翻译"; + final finalTrans = (fetched != null && fetched.isNotEmpty) + ? fetched + : "暂无该句中文翻译"; setState(() { hint = "对方说:$finalTrans"; turns[latestAiIndex] = latestAi.copyWith(translation: finalTrans); @@ -551,6 +553,18 @@ class _DialoguePageState extends State await startVoiceInput(); } + void _showHint() { + setState(() { + usedHelp = true; + final hintIdx = stage < script.hints.length + ? stage + : (script.hints.isNotEmpty ? script.hints.length - 1 : 0); + hint = script.hints.isNotEmpty ? script.hints[hintIdx] : null; + }); + _saveDraft(); + _scrollToBottom(); + } + void _showWord() => showLexiconLookup( context, state: widget.state, @@ -583,137 +597,12 @@ class _DialoguePageState extends State physics: const NeverScrollableScrollPhysics(), itemCount: turns.length, separatorBuilder: (_, _) => const SizedBox(height: 10), - itemBuilder: (context, index) { - final turn = turns[index]; - return Align( - alignment: turn.isLearner - ? Alignment.centerRight - : Alignment.centerLeft, - child: Container( - constraints: const BoxConstraints(maxWidth: 290), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: turn.isLearner - ? AppColors.warm - : AppColors.softGreen, - borderRadius: BorderRadius.circular(15), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LexiconText(turn.text, state: widget.state), - if (!turn.isLearner) ...[ - if (_shownTranslations.contains(index) && - turn.translation != null && - turn.translation!.isNotEmpty) ...[ - const SizedBox(height: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - turn.translation!, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF2D3748), - ), - ), - ), - ], - const SizedBox(height: 6), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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, - ), - ), - ], - ), - ), - const SizedBox(width: 14), - GestureDetector( - onTap: () => _toggleTurnTranslation(index), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _shownTranslations.contains(index) - ? Icons.translate - : Icons.translate_outlined, - size: 16, - color: AppColors.green, - ), - const SizedBox(width: 4), - Text( - _shownTranslations.contains(index) - ? "隐藏翻译" - : "翻译", - style: const TextStyle( - fontSize: 12, - color: AppColors.green, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - const SizedBox(width: 14), - GestureDetector( - onTap: () => showLexiconLookup( - context, - state: widget.state, - initialText: turn.text, - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.psychology_alt_outlined, - size: 16, - color: AppColors.green, - ), - SizedBox(width: 4), - Text( - "句型解析", - style: TextStyle( - fontSize: 12, - color: AppColors.green, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - ], - ], - ), - ), - ); - }, + itemBuilder: (context, index) => _TurnBubble( + turn: turns[index], + state: widget.state, + showTranslation: _shownTranslations.contains(index), + onToggleTranslation: () => _toggleTurnTranslation(index), + ), ), ), if (!finished) ...[ @@ -721,24 +610,8 @@ class _DialoguePageState extends State spacing: 8, runSpacing: 8, children: [ - _AssistChip( - label: '提示', - onTap: () { - setState(() { - usedHelp = true; - final hintIdx = stage < script.hints.length - ? stage - : (script.hints.isNotEmpty ? script.hints.length - 1 : 0); - hint = script.hints.isNotEmpty ? script.hints[hintIdx] : null; - }); - _saveDraft(); - _scrollToBottom(); - }, - ), - _AssistChip( - label: '翻译', - onTap: _showLatestAiTranslation, - ), + _AssistChip(label: '提示', onTap: _showHint), + _AssistChip(label: '翻译', onTap: _showLatestAiTranslation), _AssistChip( label: '慢一点', onTap: () => _playLatestAi(slow: true), @@ -809,9 +682,7 @@ class _DialoguePageState extends State child: CircularProgressIndicator(strokeWidth: 2), ) : Icon( - listening - ? Icons.stop_circle - : Icons.mic_none, + listening ? Icons.stop_circle : Icons.mic_none, color: listening ? Colors.redAccent : null, ), onPressed: transcribing ? null : _toggleListening, @@ -877,6 +748,127 @@ class _DialoguePageState extends State } } +class _TurnBubble extends StatelessWidget { + const _TurnBubble({ + required this.turn, + required this.state, + required this.showTranslation, + required this.onToggleTranslation, + }); + + final DialogueTurn turn; + final AppState state; + final bool showTranslation; + final VoidCallback onToggleTranslation; + + @override + Widget build(BuildContext context) { + final translation = turn.translation; + return Align( + alignment: turn.isLearner ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + constraints: const BoxConstraints(maxWidth: 290), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: turn.isLearner ? AppColors.warm : AppColors.softGreen, + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LexiconText(turn.text, state: state), + if (!turn.isLearner) ...[ + if (showTranslation && + translation != null && + translation.isNotEmpty) ...[ + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + translation, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF2D3748), + ), + ), + ), + ], + const SizedBox(height: 6), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _TurnAction( + icon: Icons.volume_up_outlined, + label: '播放', + onTap: () => VoiceService.instance.speak(turn.text), + ), + const SizedBox(width: 14), + _TurnAction( + icon: showTranslation + ? Icons.translate + : Icons.translate_outlined, + label: showTranslation ? '隐藏翻译' : '翻译', + onTap: onToggleTranslation, + ), + const SizedBox(width: 14), + _TurnAction( + icon: Icons.psychology_alt_outlined, + label: '句型解析', + onTap: () => showLexiconLookup( + context, + state: state, + initialText: turn.text, + ), + ), + ], + ), + ], + ], + ), + ), + ); + } +} + +class _TurnAction extends StatelessWidget { + const _TurnAction({ + required this.icon, + required this.label, + required this.onTap, + }); + + final IconData icon; + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: onTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: AppColors.green), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: AppColors.green, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); +} + class _AssistChip extends StatelessWidget { const _AssistChip({required this.label, required this.onTap}); final String label;