6 changed files with 708 additions and 139 deletions
Unified 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:dating_touchme_app/generated/assets.dart'; |
||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
class LiveRoomActiveSpeaker extends StatelessWidget { |
class LiveRoomActiveSpeaker extends StatelessWidget { |
||||
const LiveRoomActiveSpeaker({super.key}); |
const LiveRoomActiveSpeaker({super.key}); |
||||
|
|
||||
@override |
@override |
||||
Widget build(BuildContext context) { |
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