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
Unified 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