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>
|
class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
_CallPhase _phase = _CallPhase.ringing;
|
_CallPhase _phase = _CallPhase.ringing;
|
||||||
|
String? _errorMessage;
|
||||||
String? _sessionId;
|
String? _sessionId;
|
||||||
WebSocketChannel? _audioChannel;
|
WebSocketChannel? _audioChannel;
|
||||||
StreamSubscription? _audioSubscription;
|
StreamSubscription? _audioSubscription;
|
||||||
|
|
@ -77,7 +78,10 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
|
|
||||||
/// Accept call: create voice session, connect WebSocket, start mic + player.
|
/// Accept call: create voice session, connect WebSocket, start mic + player.
|
||||||
Future<void> _acceptCall() async {
|
Future<void> _acceptCall() async {
|
||||||
setState(() => _phase = _CallPhase.connecting);
|
setState(() {
|
||||||
|
_phase = _CallPhase.connecting;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final dio = ref.read(dioClientProvider);
|
final dio = ref.read(dioClientProvider);
|
||||||
|
|
@ -136,13 +140,10 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
setState(() => _phase = _CallPhase.active);
|
setState(() => _phase = _CallPhase.active);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_phase = _CallPhase.ringing;
|
||||||
content: Text('通话连接失败: $e'),
|
_errorMessage = '连接失败: ${_friendlyError(e)}';
|
||||||
backgroundColor: AppColors.error,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() => _phase = _CallPhase.ringing);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -434,22 +435,60 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Center(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Spacer(flex: 2),
|
const Spacer(flex: 2),
|
||||||
_buildAvatar(),
|
_buildAvatar(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
_statusText,
|
_statusText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24, fontWeight: FontWeight.bold),
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_subtitleText,
|
_subtitleText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textSecondary, fontSize: 15),
|
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),
|
const SizedBox(height: 32),
|
||||||
if (_phase == _CallPhase.active)
|
if (_phase == _CallPhase.active)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -468,6 +507,7 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Reconnecting overlay
|
// Reconnecting overlay
|
||||||
if (_isReconnecting)
|
if (_isReconnecting)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
@ -522,6 +562,9 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _subtitleText {
|
String get _subtitleText {
|
||||||
|
if (_errorMessage != null && _phase == _CallPhase.ringing) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
switch (_phase) {
|
switch (_phase) {
|
||||||
case _CallPhase.ringing:
|
case _CallPhase.ringing:
|
||||||
return '智能运维语音助手';
|
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() {
|
Widget _buildAvatar() {
|
||||||
final isActive = _phase == _CallPhase.active;
|
final isActive = _phase == _CallPhase.active;
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue