6 changed files with 708 additions and 139 deletions
Split View
Diff Options
-
BINassets/images/room_user_add.png
-
1lib/generated/assets.dart
-
16lib/widget/live/live_gift_popup.dart
-
148lib/widget/live/live_room_active_speaker.dart
-
302lib/widget/live/live_room_anchor_showcase.dart
-
380lib/widget/live/live_room_guest_list_dialog.dart
@ -1,54 +1,126 @@ |
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:dating_touchme_app/controller/discover/room_controller.dart'; |
|||
import 'package:dating_touchme_app/controller/global.dart'; |
|||
import 'package:dating_touchme_app/generated/assets.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
class LiveRoomActiveSpeaker extends StatelessWidget { |
|||
const LiveRoomActiveSpeaker({super.key}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Row( |
|||
mainAxisAlignment: MainAxisAlignment.start, |
|||
children: [ |
|||
Stack( |
|||
clipBehavior: Clip.none, |
|||
children: [ |
|||
Container( |
|||
width: 34.w, |
|||
height: 34.w, |
|||
margin: EdgeInsets.only(left: 13.w), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.all(Radius.circular(34.w)), |
|||
child: Image.asset( |
|||
Assets.imagesUserAvatar, |
|||
width: 34.w, |
|||
height: 34.w, |
|||
// 获取 RoomController(在 Obx 外部获取,避免重复查找) |
|||
final roomController = Get.isRegistered<RoomController>() |
|||
? Get.find<RoomController>() |
|||
: null; |
|||
|
|||
if (roomController == null) { |
|||
return const SizedBox.shrink(); |
|||
} |
|||
|
|||
// 获取当前用户信息 |
|||
final currentUserId = GlobalData().userId ?? GlobalData().userData?.id; |
|||
final currentUserPhoto = GlobalData().userData?.profilePhoto ?? ''; |
|||
|
|||
return Obx(() { |
|||
// 访问响应式变量以触发更新 |
|||
final rtcChannelDetail = roomController.rtcChannelDetail.value; |
|||
final isLive = roomController.isLive.value; |
|||
|
|||
// 判断当前用户是否上麦 |
|||
bool isOnSeat = false; |
|||
if (currentUserId != null) { |
|||
// 方式1:检查当前用户是否在 maleInfo 或 femaleInfo 中(maleAudience/femaleAudience 角色) |
|||
if (rtcChannelDetail != null) { |
|||
final maleInfo = rtcChannelDetail.maleInfo; |
|||
final femaleInfo = rtcChannelDetail.femaleInfo; |
|||
|
|||
isOnSeat = |
|||
(maleInfo != null && |
|||
(maleInfo.userId == currentUserId || |
|||
maleInfo.miId == currentUserId)) || |
|||
(femaleInfo != null && |
|||
(femaleInfo.userId == currentUserId || |
|||
femaleInfo.miId == currentUserId)); |
|||
} |
|||
|
|||
// 方式2:如果 isLive 为 true,说明用户已连接(可能是 audience 角色) |
|||
// 如果已经在 maleInfo/femaleInfo 中,就不需要再检查 isLive |
|||
if (!isOnSeat && isLive) { |
|||
isOnSeat = true; |
|||
} |
|||
} |
|||
|
|||
// 确定显示的图标 |
|||
final micIcon = isOnSeat ? Assets.imagesMicOpen : Assets.imagesMicClose; |
|||
|
|||
return Row( |
|||
mainAxisAlignment: MainAxisAlignment.start, |
|||
children: [ |
|||
Stack( |
|||
clipBehavior: Clip.none, |
|||
children: [ |
|||
Container( |
|||
width: 34.w, |
|||
height: 34.w, |
|||
margin: EdgeInsets.only(left: 13.w), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.all(Radius.circular(34.w)), |
|||
child: currentUserPhoto.isNotEmpty |
|||
? CachedNetworkImage( |
|||
imageUrl: currentUserPhoto, |
|||
width: 34.w, |
|||
height: 34.w, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => Container( |
|||
width: 34.w, |
|||
height: 34.w, |
|||
color: Colors.grey[300], |
|||
child: Center( |
|||
child: SizedBox( |
|||
width: 16.w, |
|||
height: 16.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => Image.asset( |
|||
Assets.imagesUserAvatar, |
|||
width: 34.w, |
|||
height: 34.w, |
|||
), |
|||
) |
|||
: Image.asset( |
|||
Assets.imagesUserAvatar, |
|||
width: 34.w, |
|||
height: 34.w, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
Positioned( |
|||
bottom: -3.w, |
|||
left: 20.w, |
|||
child: Container( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.all(Radius.circular(4.w)), |
|||
color: const Color.fromRGBO(0, 0, 0, .65), |
|||
), |
|||
child: Center( |
|||
child: Image.asset( |
|||
Assets.imagesMicClose, |
|||
width: 10.w, |
|||
height: 11.w, |
|||
Positioned( |
|||
bottom: -3.w, |
|||
left: 20.w, |
|||
child: Container( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.all(Radius.circular(4.w)), |
|||
color: const Color.fromRGBO(0, 0, 0, .65), |
|||
), |
|||
child: Center( |
|||
child: Image.asset(micIcon, width: 10.w, height: 11.w), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
); |
|||
], |
|||
), |
|||
], |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,380 @@ |
|||
import 'dart:convert'; |
|||
|
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:dating_touchme_app/im/im_manager.dart'; |
|||
import 'package:dating_touchme_app/network/network_service.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|||
|
|||
/// 嘉宾列表对话框 |
|||
class LiveRoomGuestListDialog extends StatefulWidget { |
|||
final int initialTab; // 0: 女嘉宾, 1: 男嘉宾 |
|||
|
|||
const LiveRoomGuestListDialog({super.key, required this.initialTab}); |
|||
|
|||
@override |
|||
State<LiveRoomGuestListDialog> createState() => |
|||
_LiveRoomGuestListDialogState(); |
|||
} |
|||
|
|||
class _LiveRoomGuestListDialogState extends State<LiveRoomGuestListDialog> { |
|||
late int _selectedTab; // 0: 女嘉宾, 1: 男嘉宾 |
|||
List<Map<String, dynamic>> _guestList = []; |
|||
bool _isLoading = true; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_selectedTab = widget.initialTab; |
|||
_loadContacts(); |
|||
} |
|||
|
|||
Future<void> _loadContacts() async { |
|||
setState(() { |
|||
_isLoading = true; |
|||
}); |
|||
|
|||
try { |
|||
// 获取会话列表 |
|||
final conversations = await IMManager.instance.getConversations(); |
|||
|
|||
if (conversations.isEmpty) { |
|||
setState(() { |
|||
_guestList = []; |
|||
_isLoading = false; |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
final networkService = NetworkService(); |
|||
final List<Map<String, dynamic>> guestList = []; |
|||
|
|||
// 遍历每个会话,获取联系人信息 |
|||
for (final conversation in conversations) { |
|||
try { |
|||
final userId = conversation.id; |
|||
if (userId.isEmpty) continue; |
|||
|
|||
// 获取环信用户信息 |
|||
final contactsMap = await IMManager.instance.getContacts(userId); |
|||
final emUserInfo = contactsMap[userId]; |
|||
|
|||
// 获取用户详细信息(包含性别) |
|||
final response = await networkService.userApi.getBaseUserInfo(userId); |
|||
|
|||
if (response.data.isSuccess && response.data.data != null) { |
|||
final userBaseData = response.data.data!; |
|||
|
|||
// 尝试从环信用户信息的扩展信息中获取性别等信息 |
|||
int? genderCode; |
|||
int? age; |
|||
String? cityName; |
|||
int? vipLevel; |
|||
|
|||
if (emUserInfo?.ext != null) { |
|||
try { |
|||
final extJson = json.decode(emUserInfo!.ext!); |
|||
if (extJson is Map<String, dynamic>) { |
|||
genderCode = extJson['genderCode'] is int |
|||
? extJson['genderCode'] as int |
|||
: int.tryParse(extJson['genderCode']?.toString() ?? '0'); |
|||
age = extJson['age'] is int |
|||
? extJson['age'] as int |
|||
: int.tryParse(extJson['age']?.toString() ?? ''); |
|||
cityName = extJson['cityName']?.toString(); |
|||
vipLevel = extJson['vipLevel'] is int |
|||
? extJson['vipLevel'] as int |
|||
: int.tryParse(extJson['vipLevel']?.toString() ?? ''); |
|||
} |
|||
} catch (e) { |
|||
print('解析扩展信息失败: $e'); |
|||
} |
|||
} |
|||
|
|||
guestList.add({ |
|||
'userId': userId, |
|||
'avatar': emUserInfo?.avatarUrl ?? '', |
|||
'name': emUserInfo?.nickName ?? userBaseData.nickName, |
|||
'age': age, |
|||
'location': cityName ?? '', |
|||
'vipLevel': vipLevel, |
|||
'genderCode': genderCode ?? 0, // 0-男, 1-女 |
|||
'hasMaleGuest': false, // TODO: 根据实际业务逻辑判断 |
|||
'hasFemaleGuest': false, // TODO: 根据实际业务逻辑判断 |
|||
}); |
|||
} |
|||
} catch (e) { |
|||
print('获取用户信息失败: ${conversation.id}, $e'); |
|||
} |
|||
} |
|||
|
|||
setState(() { |
|||
_guestList = guestList; |
|||
_isLoading = false; |
|||
}); |
|||
} catch (e) { |
|||
print('获取联系人列表失败: $e'); |
|||
setState(() { |
|||
_guestList = []; |
|||
_isLoading = false; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
height: MediaQuery.of(context).size.height * 0.7, |
|||
decoration: BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
// 标签页头部 |
|||
Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 20.w), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
SizedBox(width: 60.w), |
|||
_buildTabButton('女嘉宾', 0), |
|||
SizedBox(width: 40.w), |
|||
_buildTabButton('男嘉宾', 1), |
|||
SizedBox(width: 60.w), |
|||
], |
|||
), |
|||
), |
|||
Divider(height: 1, color: Colors.grey[200]), |
|||
// 列表内容 |
|||
Expanded(child: _buildGuestList()), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTabButton(String title, int index) { |
|||
final isSelected = _selectedTab == index; |
|||
return GestureDetector( |
|||
onTap: () { |
|||
setState(() { |
|||
_selectedTab = index; |
|||
}); |
|||
}, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(vertical: 10.w), |
|||
decoration: BoxDecoration( |
|||
border: Border( |
|||
bottom: BorderSide( |
|||
color: isSelected |
|||
? const Color.fromRGBO(117, 98, 249, 1) |
|||
: Colors.transparent, |
|||
width: 1.5, |
|||
), |
|||
), |
|||
), |
|||
child: Text( |
|||
title, |
|||
textAlign: TextAlign.center, |
|||
style: TextStyle( |
|||
fontSize: 16.w, |
|||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, |
|||
color: isSelected |
|||
? const Color.fromRGBO(117, 98, 249, 1) |
|||
: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildGuestList() { |
|||
if (_isLoading) { |
|||
return Center(child: CircularProgressIndicator()); |
|||
} |
|||
|
|||
// 根据性别筛选联系人 |
|||
// genderCode: 0-男, 1-女 |
|||
final filteredList = _guestList.where((guest) { |
|||
final genderCode = guest['genderCode'] as int? ?? 0; |
|||
if (_selectedTab == 0) { |
|||
// 女嘉宾标签页,显示女性(genderCode == 1) |
|||
return genderCode == 1; |
|||
} else { |
|||
// 男嘉宾标签页,显示男性(genderCode == 0) |
|||
return genderCode == 0; |
|||
} |
|||
}).toList(); |
|||
|
|||
if (filteredList.isEmpty) { |
|||
return Center( |
|||
child: Text( |
|||
'暂无${_selectedTab == 0 ? "女" : "男"}嘉宾', |
|||
style: TextStyle(fontSize: 14.w, color: Colors.grey[500]), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return ListView.builder( |
|||
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), |
|||
itemCount: filteredList.length, |
|||
itemBuilder: (context, index) { |
|||
return _buildGuestItem(filteredList[index]); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
Widget _buildGuestItem(Map<String, dynamic> guest) { |
|||
// 根据当前标签页判断是否已有对应性别的嘉宾 |
|||
final hasGuest = _selectedTab == 0 |
|||
? (guest['hasMaleGuest'] as bool? ?? false) |
|||
: (guest['hasFemaleGuest'] as bool? ?? false); |
|||
final vipLevel = guest['vipLevel'] as int?; |
|||
|
|||
return Container( |
|||
margin: EdgeInsets.only(bottom: 15.w), |
|||
child: Row( |
|||
children: [ |
|||
// 头像 |
|||
ClipRRect( |
|||
borderRadius: BorderRadius.circular(20.w), |
|||
child: CachedNetworkImage( |
|||
imageUrl: guest['avatar'] as String? ?? '', |
|||
width: 40.w, |
|||
height: 40.w, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
color: Colors.grey[300], |
|||
child: Center( |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
color: Colors.grey[300], |
|||
child: Icon(Icons.person, size: 24.w), |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 12.w), |
|||
// 用户信息 |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Text( |
|||
guest['name'] as String? ?? '', |
|||
style: TextStyle( |
|||
fontSize: 15.w, |
|||
fontWeight: FontWeight.w500, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
if (vipLevel != null && vipLevel > 0) ...[ |
|||
SizedBox(width: 6.w), |
|||
Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 6.w, |
|||
vertical: 2.w, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: const Color(0xFFFF6B35), |
|||
borderRadius: BorderRadius.circular(10.w), |
|||
), |
|||
child: Text( |
|||
'VIP $vipLevel', |
|||
style: TextStyle( |
|||
fontSize: 10.w, |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
SizedBox(height: 4.w), |
|||
Text( |
|||
_buildUserInfoText(guest['age'], guest['location']), |
|||
style: TextStyle(fontSize: 12.w, color: Colors.grey[600]), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
// 操作按钮 |
|||
_buildActionButton(hasGuest), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildActionButton(bool hasGuest) { |
|||
if (hasGuest) { |
|||
return Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w), |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey[200], |
|||
borderRadius: BorderRadius.circular(15.w), |
|||
), |
|||
child: Text( |
|||
'已有${_selectedTab == 0 ? "男" : "女"}嘉宾', |
|||
style: TextStyle(fontSize: 12.w, color: Colors.grey[600]), |
|||
), |
|||
); |
|||
} else { |
|||
return GestureDetector( |
|||
onTap: () { |
|||
// TODO: 实现邀请功能 |
|||
SmartDialog.showToast('邀请${_selectedTab == 0 ? "女" : "男"}嘉宾'); |
|||
}, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w), |
|||
decoration: BoxDecoration( |
|||
gradient: const LinearGradient( |
|||
colors: [ |
|||
Color.fromRGBO(117, 98, 249, 1), |
|||
Color.fromRGBO(131, 89, 255, 1), |
|||
], |
|||
), |
|||
borderRadius: BorderRadius.circular(15.w), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.videocam, size: 14.w, color: Colors.white), |
|||
SizedBox(width: 4.w), |
|||
Text( |
|||
'邀请', |
|||
style: TextStyle( |
|||
fontSize: 12.w, |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
String _buildUserInfoText(int? age, String? location) { |
|||
final parts = <String>[]; |
|||
if (age != null) { |
|||
parts.add('${age}岁'); |
|||
} |
|||
if (location != null && location.isNotEmpty) { |
|||
parts.add(location); |
|||
} |
|||
return parts.isEmpty ? '' : parts.join('·'); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save