15 changed files with 992 additions and 16 deletions
Unified View
Diff Options
-
BINassets/images/accept_call.png
-
BINassets/images/plat_voice_message.png
-
BINassets/images/plat_voice_message_self.png
-
BINassets/images/reject_call.png
-
BINassets/images/send_call.png
-
BINassets/images/send_video_call.png
-
BINassets/images/voice_bg_message.png
-
BINassets/images/voice_bg_message_self.png
-
395lib/controller/message/call_manager.dart
-
122lib/controller/message/chat_controller.dart
-
8lib/generated/assets.dart
-
74lib/pages/message/chat_page.dart
-
314lib/widget/message/call_item.dart
-
58lib/widget/message/chat_input_bar.dart
-
37lib/widget/message/message_item.dart
@ -0,0 +1,395 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:convert'; |
||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
||||
|
|
||||
|
import 'chat_controller.dart'; |
||||
|
|
||||
|
/// 通话类型 |
||||
|
enum CallType { |
||||
|
voice, // 语音通话 |
||||
|
video, // 视频通话 |
||||
|
} |
||||
|
|
||||
|
/// 通话状态 |
||||
|
enum CallStatus { |
||||
|
calling, // 通话中 |
||||
|
missed, // 未接听 |
||||
|
cancelled, // 已取消 |
||||
|
rejected, // 已拒绝 |
||||
|
connected, // 已接通(暂时用不到,但保留用于未来扩展) |
||||
|
} |
||||
|
|
||||
|
/// 通话管理器,单例模式,统一管理通话逻辑 |
||||
|
class CallManager extends GetxController { |
||||
|
static CallManager? _instance; |
||||
|
static CallManager get instance { |
||||
|
_instance ??= Get.put(CallManager()); |
||||
|
return _instance!; |
||||
|
} |
||||
|
|
||||
|
// 当前正在进行的通话 |
||||
|
final Rx<CallSession?> currentCall = Rx<CallSession?>(null); |
||||
|
|
||||
|
// 通话计时器(用于记录通话时长) |
||||
|
Timer? _callTimer; |
||||
|
int _callDurationSeconds = 0; |
||||
|
final RxInt callDurationSeconds = RxInt(0); |
||||
|
|
||||
|
CallManager() { |
||||
|
print('📞 [CallManager] 通话管理器已初始化'); |
||||
|
} |
||||
|
|
||||
|
/// 发起通话 |
||||
|
/// [targetUserId] 目标用户ID |
||||
|
/// [callType] 通话类型:语音或视频 |
||||
|
/// [chatController] 聊天控制器,用于发送通话消息 |
||||
|
Future<bool> initiateCall({ |
||||
|
required String targetUserId, |
||||
|
required CallType callType, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
if (currentCall.value != null) { |
||||
|
SmartDialog.showToast('已有通话正在进行中'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
print('📞 [CallManager] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId'); |
||||
|
|
||||
|
// 创建通话会话 |
||||
|
final session = CallSession( |
||||
|
targetUserId: targetUserId, |
||||
|
callType: callType, |
||||
|
status: CallStatus.calling, |
||||
|
isInitiator: true, |
||||
|
startTime: DateTime.now(), |
||||
|
); |
||||
|
currentCall.value = session; |
||||
|
|
||||
|
// 发送通话消息(发起) |
||||
|
final callTypeStr = callType == CallType.video ? 'video' : 'voice'; |
||||
|
await _sendCallMessage( |
||||
|
targetUserId: targetUserId, |
||||
|
callType: callTypeStr, |
||||
|
callStatus: 'missed', // 初始状态为未接听,等待对方响应 |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
|
||||
|
// TODO: 这里可以集成实际的通话SDK,发起真正的通话 |
||||
|
// 例如:await RTCManager.instance.startCall(targetUserId, callType); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 发起通话失败: $e'); |
||||
|
SmartDialog.showToast('发起通话失败: $e'); |
||||
|
currentCall.value = null; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 接听通话 |
||||
|
/// [message] 通话消息 |
||||
|
/// [chatController] 聊天控制器,用于更新通话消息 |
||||
|
Future<bool> acceptCall({ |
||||
|
required EMMessage message, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
final callInfo = _parseCallInfo(message); |
||||
|
if (callInfo == null) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
final targetUserId = message.from ?? ''; |
||||
|
final callTypeStr = callInfo['callType'] as String?; |
||||
|
final callType = callTypeStr == 'video' ? CallType.video : CallType.voice; |
||||
|
|
||||
|
print('📞 [CallManager] 接听${callType == CallType.video ? "视频" : "语音"}通话'); |
||||
|
|
||||
|
// 创建通话会话 |
||||
|
final session = CallSession( |
||||
|
targetUserId: targetUserId, |
||||
|
callType: callType, |
||||
|
status: CallStatus.calling, |
||||
|
isInitiator: false, |
||||
|
startTime: DateTime.now(), |
||||
|
); |
||||
|
currentCall.value = session; |
||||
|
|
||||
|
// 开始计时 |
||||
|
_startCallTimer(); |
||||
|
|
||||
|
// 更新通话消息状态为通话中 |
||||
|
await _updateCallMessageStatus( |
||||
|
message: message, |
||||
|
callStatus: 'calling', |
||||
|
callDuration: 0, |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
|
||||
|
// TODO: 这里可以集成实际的通话SDK,接听通话 |
||||
|
// 例如:await RTCManager.instance.acceptCall(targetUserId, callType); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 接听通话失败: $e'); |
||||
|
SmartDialog.showToast('接听通话失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 拒绝通话 |
||||
|
/// [message] 通话消息 |
||||
|
/// [chatController] 聊天控制器,用于更新通话消息 |
||||
|
Future<bool> rejectCall({ |
||||
|
required EMMessage message, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
print('📞 [CallManager] 拒绝通话'); |
||||
|
|
||||
|
// 更新通话消息状态为已拒绝 |
||||
|
await _updateCallMessageStatus( |
||||
|
message: message, |
||||
|
callStatus: 'rejected', |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
|
||||
|
// 清理通话会话 |
||||
|
currentCall.value = null; |
||||
|
|
||||
|
// TODO: 这里可以集成实际的通话SDK,拒绝通话 |
||||
|
// 例如:await RTCManager.instance.rejectCall(message.from ?? ''); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 拒绝通话失败: $e'); |
||||
|
SmartDialog.showToast('拒绝通话失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 取消通话 |
||||
|
/// [message] 通话消息(可选,如果是发起方取消) |
||||
|
/// [chatController] 聊天控制器,用于更新通话消息 |
||||
|
Future<bool> cancelCall({ |
||||
|
EMMessage? message, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
print('📞 [CallManager] 取消通话'); |
||||
|
|
||||
|
// 如果有消息,更新通话消息状态为已取消 |
||||
|
if (message != null) { |
||||
|
await _updateCallMessageStatus( |
||||
|
message: message, |
||||
|
callStatus: 'cancelled', |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 停止计时 |
||||
|
_stopCallTimer(); |
||||
|
|
||||
|
// 清理通话会话 |
||||
|
currentCall.value = null; |
||||
|
|
||||
|
// TODO: 这里可以集成实际的通话SDK,取消通话 |
||||
|
// 例如:await RTCManager.instance.cancelCall(); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 取消通话失败: $e'); |
||||
|
SmartDialog.showToast('取消通话失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 结束通话(通话完成) |
||||
|
/// [callDuration] 通话时长(秒) |
||||
|
/// [chatController] 聊天控制器,用于更新通话消息 |
||||
|
Future<bool> endCall({ |
||||
|
required int callDuration, |
||||
|
EMMessage? message, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
print('📞 [CallManager] 结束通话,时长: ${callDuration}秒'); |
||||
|
|
||||
|
// 停止计时 |
||||
|
_stopCallTimer(); |
||||
|
|
||||
|
// 如果有消息,更新通话消息状态为通话中(显示时长) |
||||
|
if (message != null) { |
||||
|
await _updateCallMessageStatus( |
||||
|
message: message, |
||||
|
callStatus: 'calling', |
||||
|
callDuration: callDuration, |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 清理通话会话 |
||||
|
currentCall.value = null; |
||||
|
|
||||
|
// TODO: 这里可以集成实际的通话SDK,结束通话 |
||||
|
// 例如:await RTCManager.instance.endCall(); |
||||
|
|
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 结束通话失败: $e'); |
||||
|
SmartDialog.showToast('结束通话失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 开始通话计时 |
||||
|
void _startCallTimer() { |
||||
|
_callDurationSeconds = 0; |
||||
|
callDurationSeconds.value = 0; |
||||
|
_callTimer?.cancel(); |
||||
|
_callTimer = Timer.periodic(Duration(seconds: 1), (timer) { |
||||
|
_callDurationSeconds++; |
||||
|
callDurationSeconds.value = _callDurationSeconds; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// 停止通话计时 |
||||
|
void _stopCallTimer() { |
||||
|
_callTimer?.cancel(); |
||||
|
_callTimer = null; |
||||
|
_callDurationSeconds = 0; |
||||
|
callDurationSeconds.value = 0; |
||||
|
} |
||||
|
|
||||
|
/// 发送通话消息 |
||||
|
Future<bool> _sendCallMessage({ |
||||
|
required String targetUserId, |
||||
|
required String callType, |
||||
|
required String callStatus, |
||||
|
int? callDuration, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
// 如果提供了 chatController,使用它发送消息 |
||||
|
if (chatController != null) { |
||||
|
return await chatController.sendCallMessage( |
||||
|
callType: callType, |
||||
|
callStatus: callStatus, |
||||
|
callDuration: callDuration, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 否则直接通过 IMManager 发送 |
||||
|
final callInfoMap = <String, dynamic>{ |
||||
|
'callType': callType, |
||||
|
'callStatus': callStatus, |
||||
|
}; |
||||
|
if (callDuration != null) { |
||||
|
callInfoMap['callDuration'] = callDuration; |
||||
|
} |
||||
|
final callInfoJson = jsonEncode(callInfoMap); |
||||
|
final content = '[CALL:]$callInfoJson'; |
||||
|
|
||||
|
final message = EMMessage.createTxtSendMessage( |
||||
|
targetId: targetUserId, |
||||
|
content: content, |
||||
|
); |
||||
|
|
||||
|
await EMClient.getInstance.chatManager.sendMessage(message); |
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 发送通话消息失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 更新通话消息状态 |
||||
|
Future<bool> _updateCallMessageStatus({ |
||||
|
required EMMessage message, |
||||
|
required String callStatus, |
||||
|
int? callDuration, |
||||
|
ChatController? chatController, |
||||
|
}) async { |
||||
|
try { |
||||
|
// 解析现有通话信息 |
||||
|
final callInfo = _parseCallInfo(message); |
||||
|
if (callInfo == null) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
final callType = callInfo['callType'] as String? ?? 'voice'; |
||||
|
final targetUserId = message.from ?? message.to ?? ''; |
||||
|
|
||||
|
// 发送更新的通话消息 |
||||
|
return await _sendCallMessage( |
||||
|
targetUserId: targetUserId, |
||||
|
callType: callType, |
||||
|
callStatus: callStatus, |
||||
|
callDuration: callDuration, |
||||
|
chatController: chatController, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
print('❌ [CallManager] 更新通话消息状态失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 从消息中解析通话信息 |
||||
|
Map<String, dynamic>? _parseCallInfo(EMMessage message) { |
||||
|
try { |
||||
|
if (message.body.type == MessageType.TXT) { |
||||
|
final textBody = message.body as EMTextMessageBody; |
||||
|
final content = textBody.content; |
||||
|
if (content != null && content.startsWith('[CALL:]')) { |
||||
|
final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 |
||||
|
return jsonDecode(jsonStr) as Map<String, dynamic>; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
print('解析通话信息失败: $e'); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// 检查是否有正在进行的通话 |
||||
|
bool get isInCall => currentCall.value != null; |
||||
|
|
||||
|
/// 获取当前通话时长(秒) |
||||
|
int get currentCallDuration => callDurationSeconds.value; |
||||
|
|
||||
|
@override |
||||
|
void onClose() { |
||||
|
_stopCallTimer(); |
||||
|
currentCall.value = null; |
||||
|
super.onClose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通话会话信息 |
||||
|
class CallSession { |
||||
|
final String targetUserId; |
||||
|
final CallType callType; |
||||
|
final CallStatus status; |
||||
|
final bool isInitiator; // 是否是发起方 |
||||
|
final DateTime startTime; |
||||
|
DateTime? endTime; |
||||
|
|
||||
|
CallSession({ |
||||
|
required this.targetUserId, |
||||
|
required this.callType, |
||||
|
required this.status, |
||||
|
required this.isInitiator, |
||||
|
required this.startTime, |
||||
|
this.endTime, |
||||
|
}); |
||||
|
|
||||
|
/// 获取通话时长(秒) |
||||
|
int get duration { |
||||
|
final end = endTime ?? DateTime.now(); |
||||
|
return end.difference(startTime).inSeconds; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,314 @@ |
|||||
|
import 'dart:convert'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
||||
|
|
||||
|
import '../../generated/assets.dart'; |
||||
|
|
||||
|
/// 通话类型 |
||||
|
enum CallType { |
||||
|
voice, // 语音通话 |
||||
|
video, // 视频通话 |
||||
|
} |
||||
|
|
||||
|
/// 通话状态 |
||||
|
enum CallStatus { |
||||
|
calling, // 通话中(显示时长) |
||||
|
missed, // 未接听 |
||||
|
cancelled, // 已取消 |
||||
|
rejected, // 已拒绝 |
||||
|
} |
||||
|
|
||||
|
class CallItem extends StatelessWidget { |
||||
|
final EMMessage message; |
||||
|
final bool isSentByMe; |
||||
|
final bool showTime; |
||||
|
final String formattedTime; |
||||
|
final VoidCallback? onResend; |
||||
|
|
||||
|
const CallItem({ |
||||
|
required this.message, |
||||
|
required this.isSentByMe, |
||||
|
required this.showTime, |
||||
|
required this.formattedTime, |
||||
|
this.onResend, |
||||
|
super.key, |
||||
|
}); |
||||
|
|
||||
|
/// 从消息内容中解析通话信息(使用特殊的JSON格式) |
||||
|
Map<String, dynamic>? _parseCallInfo() { |
||||
|
try { |
||||
|
if (message.body.type == MessageType.TXT) { |
||||
|
final textBody = message.body as EMTextMessageBody; |
||||
|
final content = textBody.content; |
||||
|
|
||||
|
// 检查是否是通话消息(以 [CALL:] 开头) |
||||
|
if (content != null && content.startsWith('[CALL:]')) { |
||||
|
final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 |
||||
|
return jsonDecode(jsonStr) as Map<String, dynamic>; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
print('解析通话信息失败: $e'); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// 从消息中解析通话类型 |
||||
|
CallType? _getCallType() { |
||||
|
final callInfo = _parseCallInfo(); |
||||
|
if (callInfo != null) { |
||||
|
final callTypeStr = callInfo['callType'] as String?; |
||||
|
if (callTypeStr == 'voice') { |
||||
|
return CallType.voice; |
||||
|
} else if (callTypeStr == 'video') { |
||||
|
return CallType.video; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// 从消息中解析通话状态 |
||||
|
CallStatus? _getCallStatus() { |
||||
|
final callInfo = _parseCallInfo(); |
||||
|
if (callInfo != null) { |
||||
|
final statusStr = callInfo['callStatus'] as String?; |
||||
|
if (statusStr == 'calling') { |
||||
|
return CallStatus.calling; |
||||
|
} else if (statusStr == 'missed') { |
||||
|
return CallStatus.missed; |
||||
|
} else if (statusStr == 'cancelled') { |
||||
|
return CallStatus.cancelled; |
||||
|
} else if (statusStr == 'rejected') { |
||||
|
return CallStatus.rejected; |
||||
|
} |
||||
|
} |
||||
|
return CallStatus.missed; // 默认未接听 |
||||
|
} |
||||
|
|
||||
|
/// 获取通话时长(秒) |
||||
|
int? _getCallDuration() { |
||||
|
final callInfo = _parseCallInfo(); |
||||
|
if (callInfo != null) { |
||||
|
return callInfo['callDuration'] as int?; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// 格式化通话时长 |
||||
|
String _formatDuration(int seconds) { |
||||
|
final minutes = seconds ~/ 60; |
||||
|
final secs = seconds % 60; |
||||
|
if (minutes > 0) { |
||||
|
return '${minutes}:${secs.toString().padLeft(2, '0')}'; |
||||
|
} else { |
||||
|
return '${secs}秒'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取图标资源 |
||||
|
String _getIconAsset() { |
||||
|
final callType = _getCallType(); |
||||
|
final callStatus = _getCallStatus(); |
||||
|
|
||||
|
if (isSentByMe) { |
||||
|
// 自己发起的通话 |
||||
|
if (callType == CallType.video) { |
||||
|
return Assets.imagesSendVideoCall; |
||||
|
} else { |
||||
|
return Assets.imagesSendCall; |
||||
|
} |
||||
|
} else { |
||||
|
// 别人发起的通话 |
||||
|
if (callStatus == CallStatus.rejected) { |
||||
|
return Assets.imagesRejectCall; |
||||
|
} else { |
||||
|
return Assets.imagesAcceptCall; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取状态文本 |
||||
|
String _getStatusText() { |
||||
|
final callType = _getCallType(); |
||||
|
final callStatus = _getCallStatus(); |
||||
|
final duration = _getCallDuration(); |
||||
|
|
||||
|
if (callStatus == CallStatus.calling && duration != null) { |
||||
|
// 通话中,显示时长 |
||||
|
return _formatDuration(duration); |
||||
|
} else if (callStatus == CallStatus.missed) { |
||||
|
return callType == CallType.video ? '未接听视频通话' : '未接听语音通话'; |
||||
|
} else if (callStatus == CallStatus.cancelled) { |
||||
|
return callType == CallType.video ? '已取消视频通话' : '已取消语音通话'; |
||||
|
} else if (callStatus == CallStatus.rejected) { |
||||
|
return callType == CallType.video ? '已拒绝视频通话' : '已拒绝语音通话'; |
||||
|
} else { |
||||
|
return callType == CallType.video ? '视频通话' : '语音通话'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final callType = _getCallType(); |
||||
|
if (callType == null) { |
||||
|
// 如果解析失败,显示默认文本消息 |
||||
|
return SizedBox.shrink(); |
||||
|
} |
||||
|
|
||||
|
final iconAsset = _getIconAsset(); |
||||
|
final statusText = _getStatusText(); |
||||
|
final callStatus = _getCallStatus(); |
||||
|
final isCalling = callStatus == CallStatus.calling; |
||||
|
|
||||
|
return Column( |
||||
|
children: [ |
||||
|
// 显示时间 |
||||
|
if (showTime) _buildTimeLabel(), |
||||
|
Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: |
||||
|
isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||
|
children: [ |
||||
|
if (!isSentByMe) _buildAvatar(), |
||||
|
if (!isSentByMe) SizedBox(width: 8.w), |
||||
|
// 发送消息时,状态在左侧 |
||||
|
if (isSentByMe) |
||||
|
Align( |
||||
|
alignment: Alignment.center, |
||||
|
child: Container( |
||||
|
margin: EdgeInsets.only(top: 10.h), |
||||
|
child: _buildMessageStatus(), |
||||
|
), |
||||
|
), |
||||
|
if (isSentByMe) SizedBox(width: 10.w), |
||||
|
// 通话消息容器 |
||||
|
Container( |
||||
|
constraints: BoxConstraints(maxWidth: 200.w), |
||||
|
margin: EdgeInsets.only(top: 10.h), |
||||
|
padding: EdgeInsets.symmetric( |
||||
|
horizontal: 16.w, |
||||
|
vertical: 12.h, |
||||
|
), |
||||
|
decoration: BoxDecoration( |
||||
|
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, |
||||
|
borderRadius: BorderRadius.only( |
||||
|
topLeft: |
||||
|
isSentByMe ? Radius.circular(12.w) : Radius.circular(0), |
||||
|
topRight: |
||||
|
isSentByMe ? Radius.circular(0) : Radius.circular(12.w), |
||||
|
bottomLeft: Radius.circular(12.w), |
||||
|
bottomRight: Radius.circular(12.w), |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
// 图标 |
||||
|
Image.asset( |
||||
|
iconAsset, |
||||
|
width: 24.w, |
||||
|
height: 24.w, |
||||
|
fit: BoxFit.contain, |
||||
|
), |
||||
|
SizedBox(width: 8.w), |
||||
|
// 状态文本 |
||||
|
Flexible( |
||||
|
child: Text( |
||||
|
statusText, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isSentByMe ? Colors.white : Colors.black87, |
||||
|
fontWeight: isCalling ? FontWeight.w500 : FontWeight.normal, |
||||
|
), |
||||
|
maxLines: 2, |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
if (isSentByMe) SizedBox(width: 8.w), |
||||
|
if (isSentByMe) _buildAvatar(), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建时间标签 |
||||
|
Widget _buildTimeLabel() { |
||||
|
return Container( |
||||
|
alignment: Alignment.center, |
||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w), |
||||
|
child: Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w), |
||||
|
child: Text( |
||||
|
formattedTime, |
||||
|
style: TextStyle(fontSize: 12.sp, color: Colors.grey), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建头像 |
||||
|
Widget _buildAvatar() { |
||||
|
return Container( |
||||
|
width: 40.w, |
||||
|
height: 40.w, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.circular(20.w), |
||||
|
image: DecorationImage( |
||||
|
image: AssetImage(Assets.imagesAvatarsExample), |
||||
|
fit: BoxFit.cover, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建消息状态(发送中、已发送、失败重发) |
||||
|
Widget _buildMessageStatus() { |
||||
|
if (!isSentByMe) { |
||||
|
return SizedBox.shrink(); |
||||
|
} |
||||
|
|
||||
|
final status = message.status; |
||||
|
|
||||
|
if (status == MessageStatus.FAIL) { |
||||
|
// 发送失败,显示重发按钮 |
||||
|
return GestureDetector( |
||||
|
onTap: onResend, |
||||
|
child: Container( |
||||
|
width: 20.w, |
||||
|
height: 20.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.red.withOpacity(0.1), |
||||
|
shape: BoxShape.circle, |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.refresh, |
||||
|
size: 14.w, |
||||
|
color: Colors.red, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} else if (status == MessageStatus.PROGRESS) { |
||||
|
// 发送中,显示加载动画 |
||||
|
return Container( |
||||
|
width: 16.w, |
||||
|
height: 16.w, |
||||
|
child: CircularProgressIndicator( |
||||
|
strokeWidth: 2, |
||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey), |
||||
|
), |
||||
|
); |
||||
|
} else { |
||||
|
// 发送成功,不显示任何状态 |
||||
|
return SizedBox.shrink(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save