From 45c54acb87c0746bc08fea595ac6dfaacf21e8f4 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 23 Feb 2026 21:10:49 -0800 Subject: [PATCH] fix: improve voice call page UI centering and error display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 布局居中:将 Column 包裹在 Center 中,所有文本添加 textAlign: TextAlign.center,确保头像、标题、副标题 在各种屏幕尺寸上居中显示。 2. 错误展示优化:将 SnackBar 大面积红色块替换为行内错误卡片, 采用圆角容器 + error icon + 简洁文案,视觉上更融洽。 新增 _errorMessage 状态字段 + _friendlyError() 方法, 将 DioException 等异常转换为中文友好提示(如 "语音服务 暂不可用 (503)"),避免用户看到大段英文 stacktrace。 3. 错误状态清理:点击接听时自动清除上一次的 _errorMessage。 Co-Authored-By: Claude Opus 4.6 --- .../presentation/pages/agent_call_page.dart | 144 +++++++++++++----- 1 file changed, 106 insertions(+), 38 deletions(-) diff --git a/it0_app/lib/features/agent_call/presentation/pages/agent_call_page.dart b/it0_app/lib/features/agent_call/presentation/pages/agent_call_page.dart index bc500d5..4f40da5 100644 --- a/it0_app/lib/features/agent_call/presentation/pages/agent_call_page.dart +++ b/it0_app/lib/features/agent_call/presentation/pages/agent_call_page.dart @@ -26,6 +26,7 @@ class AgentCallPage extends ConsumerStatefulWidget { class _AgentCallPageState extends ConsumerState with SingleTickerProviderStateMixin { _CallPhase _phase = _CallPhase.ringing; + String? _errorMessage; String? _sessionId; WebSocketChannel? _audioChannel; StreamSubscription? _audioSubscription; @@ -77,7 +78,10 @@ class _AgentCallPageState extends ConsumerState /// Accept call: create voice session, connect WebSocket, start mic + player. Future _acceptCall() async { - setState(() => _phase = _CallPhase.connecting); + setState(() { + _phase = _CallPhase.connecting; + _errorMessage = null; + }); try { final dio = ref.read(dioClientProvider); @@ -136,13 +140,10 @@ class _AgentCallPageState extends ConsumerState setState(() => _phase = _CallPhase.active); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('通话连接失败: $e'), - backgroundColor: AppColors.error, - ), - ); - setState(() => _phase = _CallPhase.ringing); + setState(() { + _phase = _CallPhase.ringing; + _errorMessage = '连接失败: ${_friendlyError(e)}'; + }); } } } @@ -434,39 +435,78 @@ class _AgentCallPageState extends ConsumerState body: SafeArea( child: Stack( children: [ - Column( - children: [ - const Spacer(flex: 2), - _buildAvatar(), - const SizedBox(height: 24), - Text( - _statusText, - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - _subtitleText, - style: const TextStyle( - color: AppColors.textSecondary, fontSize: 15), - ), - const SizedBox(height: 32), - if (_phase == _CallPhase.active) + Center( + child: Column( + children: [ + const Spacer(flex: 2), + _buildAvatar(), + const SizedBox(height: 24), Text( - _durationLabel, + _statusText, + textAlign: TextAlign.center, style: const TextStyle( - fontSize: 40, - fontWeight: FontWeight.w300, - color: AppColors.textPrimary, - letterSpacing: 4, - ), + fontSize: 24, fontWeight: FontWeight.bold), ), - const SizedBox(height: 24), - if (_phase == _CallPhase.active) _buildWaveform(), - const Spacer(flex: 3), - _buildControls(), - const SizedBox(height: 48), - ], + const SizedBox(height: 8), + Text( + _subtitleText, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.textSecondary, fontSize: 15), + ), + // Inline error message + if (_errorMessage != null) ...[ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColors.error.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, + size: 18, color: AppColors.error), + const SizedBox(width: 8), + Flexible( + child: Text( + _errorMessage!, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.error, + fontSize: 13, + ), + ), + ), + ], + ), + ), + ), + ], + const SizedBox(height: 32), + if (_phase == _CallPhase.active) + Text( + _durationLabel, + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.w300, + color: AppColors.textPrimary, + letterSpacing: 4, + ), + ), + const SizedBox(height: 24), + if (_phase == _CallPhase.active) _buildWaveform(), + const Spacer(flex: 3), + _buildControls(), + const SizedBox(height: 48), + ], + ), ), // Reconnecting overlay if (_isReconnecting) @@ -522,6 +562,9 @@ class _AgentCallPageState extends ConsumerState } String get _subtitleText { + if (_errorMessage != null && _phase == _CallPhase.ringing) { + return ''; + } switch (_phase) { case _CallPhase.ringing: return '智能运维语音助手'; @@ -534,6 +577,31 @@ class _AgentCallPageState extends ConsumerState } } + /// Extract a short user-friendly message from an exception. + String _friendlyError(dynamic e) { + final s = e.toString(); + // Extract status code info for Dio errors + final match = RegExp(r'status code of (\d+)').firstMatch(s); + if (match != null) { + final code = match.group(1); + switch (code) { + case '503': + return '语音服务暂不可用 ($code)'; + case '502': + return '语音服务网关错误 ($code)'; + case '404': + return '语音服务未找到 ($code)'; + default: + return '服务器错误 ($code)'; + } + } + if (s.contains('SocketException') || s.contains('Connection refused')) { + return '无法连接到服务器'; + } + if (s.length > 80) return '${s.substring(0, 80)}...'; + return s; + } + Widget _buildAvatar() { final isActive = _phase == _CallPhase.active; return AnimatedContainer(