fix: improve voice call page UI centering and error display
1. 布局居中:将 Column 包裹在 Center 中,所有文本添加 textAlign: TextAlign.center,确保头像、标题、副标题 在各种屏幕尺寸上居中显示。 2. 错误展示优化:将 SnackBar 大面积红色块替换为行内错误卡片, 采用圆角容器 + error icon + 简洁文案,视觉上更融洽。 新增 _errorMessage 状态字段 + _friendlyError() 方法, 将 DioException 等异常转换为中文友好提示(如 "语音服务 暂不可用 (503)"),避免用户看到大段英文 stacktrace。 3. 错误状态清理:点击接听时自动清除上一次的 _errorMessage。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b7814d42a9
commit
45c54acb87
|
|
@ -26,6 +26,7 @@ class AgentCallPage extends ConsumerStatefulWidget {
|
|||
class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
_CallPhase _phase = _CallPhase.ringing;
|
||||
String? _errorMessage;
|
||||
String? _sessionId;
|
||||
WebSocketChannel? _audioChannel;
|
||||
StreamSubscription? _audioSubscription;
|
||||
|
|
@ -77,7 +78,10 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
|||
|
||||
/// Accept call: create voice session, connect WebSocket, start mic + player.
|
||||
Future<void> _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<AgentCallPage>
|
|||
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<AgentCallPage>
|
|||
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<AgentCallPage>
|
|||
}
|
||||
|
||||
String get _subtitleText {
|
||||
if (_errorMessage != null && _phase == _CallPhase.ringing) {
|
||||
return '';
|
||||
}
|
||||
switch (_phase) {
|
||||
case _CallPhase.ringing:
|
||||
return '智能运维语音助手';
|
||||
|
|
@ -534,6 +577,31 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
|||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue