Browse Source
feat(live): 添加直播间嘉宾列表对话框
feat(live): 添加直播间嘉宾列表对话框
- 实现了可切换标签的嘉宾列表展示界面 - 支持按性别筛选显示男嘉宾或女嘉宾 - 集成了IM管理器获取会话和联系人信息 - 调用网络服务获取用户基础资料和扩展信息 - 展示用户头像、昵称、年龄、地区和VIP等级 - 提供邀请按钮和已有嘉宾状态提示 - 使用CachedNetworkImage优化图片加载体验 - 添加了加载状态指示器和空数据提示 - 实现了下拉刷新和上拉加载更多功能 - 支持根据用户ID获取并解析环信用户扩展信息ios
1 changed files with 380 additions and 0 deletions
Split View
Diff Options
@ -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