Browse Source
feat(message): 新增直播间邀请消息展示功能
feat(message): 新增直播间邀请消息展示功能
- 在 MessageItem 中引入 RoomItem 组件 - 添加 `_isRoomInviteMessage` 方法用于识别直播间邀请消息 - 新增对自定义消息类型为 'live_room_invite' 的处理逻辑 - 创建 RoomItem 组件用于展示直播间邀请卡片 - 实现点击直播间卡片跳转至 LiveRoomPage 页面的功能 - 支持从自定义消息中解析房间信息(频道ID、主播昵称、头像) - 添加消息重发机制,提升消息发送可靠性 - 使用 CachedNetworkImage 优化图片加载体验 - 增加时间标签与消息状态显示(发送中、失败重试)ios
2 changed files with 391 additions and 0 deletions
Split View
Diff Options
@ -0,0 +1,358 @@ |
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
import '../../generated/assets.dart'; |
|||
import '../../pages/discover/live_room_page.dart'; |
|||
import '../../controller/discover/room_controller.dart'; |
|||
|
|||
class RoomItem extends StatelessWidget { |
|||
final EMMessage message; |
|||
final bool isSentByMe; |
|||
final bool showTime; |
|||
final String formattedTime; |
|||
final VoidCallback? onResend; |
|||
|
|||
const RoomItem({ |
|||
required this.message, |
|||
required this.isSentByMe, |
|||
required this.showTime, |
|||
required this.formattedTime, |
|||
this.onResend, |
|||
super.key, |
|||
}); |
|||
|
|||
/// 从自定义消息中解析房间信息 |
|||
Map<String, String>? _parseRoomInfo() { |
|||
try { |
|||
if (message.body.type == MessageType.CUSTOM) { |
|||
final customBody = message.body as EMCustomMessageBody; |
|||
// 检查是否是直播间邀请消息 |
|||
if (customBody.event == 'live_room_invite') { |
|||
return customBody.params; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
print('解析房间信息失败: $e'); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 获取频道ID |
|||
String _getChannelId() { |
|||
final roomInfo = _parseRoomInfo(); |
|||
return roomInfo?['channelId'] ?? ''; |
|||
} |
|||
|
|||
/// 获取主持人昵称 |
|||
String _getAnchorName() { |
|||
final roomInfo = _parseRoomInfo(); |
|||
return roomInfo?['anchorName'] ?? '主持人'; |
|||
} |
|||
|
|||
/// 获取主持人头像 |
|||
String _getAnchorAvatar() { |
|||
final roomInfo = _parseRoomInfo(); |
|||
return roomInfo?['anchorAvatar'] ?? ''; |
|||
} |
|||
|
|||
/// 处理点击事件 |
|||
void _handleTap() { |
|||
final channelId = _getChannelId(); |
|||
if (channelId.isNotEmpty) { |
|||
// 获取 RoomController 并加入频道 |
|||
final roomController = Get.isRegistered<RoomController>() |
|||
? Get.find<RoomController>() |
|||
: Get.put(RoomController()); |
|||
|
|||
// 加入频道并跳转 |
|||
roomController.joinChannel(channelId).then((_) { |
|||
Get.to(() => const LiveRoomPage(id: 0)); |
|||
}).catchError((e) { |
|||
print('❌ 加入直播间失败: $e'); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final roomInfo = _parseRoomInfo(); |
|||
if (roomInfo == null) { |
|||
return SizedBox.shrink(); |
|||
} |
|||
|
|||
final anchorName = _getAnchorName(); |
|||
final anchorAvatar = _getAnchorAvatar(); |
|||
|
|||
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), |
|||
// 直播间卡片 |
|||
GestureDetector( |
|||
onTap: _handleTap, |
|||
child: Container( |
|||
constraints: BoxConstraints(maxWidth: 250.w), |
|||
margin: EdgeInsets.only(top: 10.h), |
|||
decoration: BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.circular(12.w), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.1), |
|||
blurRadius: 8, |
|||
offset: Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(12.w), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 封面图片 |
|||
Stack( |
|||
children: [ |
|||
Container( |
|||
width: 250.w, |
|||
height: 250.w, |
|||
color: Colors.grey[200], |
|||
child: anchorAvatar.isNotEmpty |
|||
? CachedNetworkImage( |
|||
imageUrl: anchorAvatar, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => Container( |
|||
color: Colors.grey[200], |
|||
child: Center( |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => |
|||
Container( |
|||
color: Colors.grey[200], |
|||
child: Icon( |
|||
Icons.live_tv, |
|||
size: 40.w, |
|||
color: Colors.grey[400], |
|||
), |
|||
), |
|||
) |
|||
: Container( |
|||
color: Colors.grey[200], |
|||
child: Icon( |
|||
Icons.live_tv, |
|||
size: 40.w, |
|||
color: Colors.grey[400], |
|||
), |
|||
), |
|||
), |
|||
// 直播中标签 |
|||
Positioned( |
|||
top: 8.w, |
|||
left: 8.w, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 8.w, |
|||
vertical: 4.w, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: const Color.fromRGBO(117, 98, 249, 1), |
|||
borderRadius: BorderRadius.circular(4.w), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon( |
|||
Icons.play_circle_filled, |
|||
size: 12.w, |
|||
color: Colors.white, |
|||
), |
|||
SizedBox(width: 4.w), |
|||
Text( |
|||
'直播中', |
|||
style: TextStyle( |
|||
fontSize: 11.sp, |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
// 底部信息 |
|||
Container( |
|||
padding: EdgeInsets.all(12.w), |
|||
child: Row( |
|||
children: [ |
|||
// 头像 |
|||
ClipRRect( |
|||
borderRadius: BorderRadius.circular(12.w), |
|||
child: Container( |
|||
width: 24.w, |
|||
height: 24.w, |
|||
color: Colors.grey[300], |
|||
child: anchorAvatar.isNotEmpty |
|||
? CachedNetworkImage( |
|||
imageUrl: anchorAvatar, |
|||
width: 24.w, |
|||
height: 24.w, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => |
|||
Container( |
|||
color: Colors.grey[300], |
|||
child: Center( |
|||
child: SizedBox( |
|||
width: 12.w, |
|||
height: 12.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => |
|||
Icon( |
|||
Icons.person, |
|||
size: 16.w, |
|||
color: Colors.grey[600], |
|||
), |
|||
) |
|||
: Icon( |
|||
Icons.person, |
|||
size: 16.w, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
// 标题 |
|||
Expanded( |
|||
child: Text( |
|||
anchorName, |
|||
style: TextStyle( |
|||
fontSize: 13.sp, |
|||
color: Colors.black87, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
maxLines: 1, |
|||
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