From a6896411547877951e61baa3bb0dd7488d13d3ff Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Thu, 27 Nov 2025 23:55:19 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(live):=20=E6=9B=B4=E6=96=B0=E7=9B=B4?= =?UTF-8?q?=E6=92=AD=E9=97=B4UI=E5=B9=B6=E5=A2=9E=E5=BC=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BA=A4=E4=BA=92=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增房间用户添加图标资源 - 优化主播展示区域,支持点击添加嘉宾功能 - 改进活跃说话人组件,使用网络图片并添加加载状态 - 实现用户上麦状态判断逻辑 - 添加嘉宾列表弹窗功能 - 优化视频占位图显示逻辑 - 调整UI布局和样式细节 --- assets/images/room_user_add.png | Bin 0 -> 689 bytes lib/generated/assets.dart | 1 + lib/widget/live/live_room_active_speaker.dart | 148 ++++++++--- .../live/live_room_anchor_showcase.dart | 234 +++++++++++------- 4 files changed, 252 insertions(+), 131 deletions(-) create mode 100644 assets/images/room_user_add.png diff --git a/assets/images/room_user_add.png b/assets/images/room_user_add.png new file mode 100644 index 0000000000000000000000000000000000000000..913aa75ff851692cd6225d7208e7608ed4316dc4 GIT binary patch literal 689 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VJUX<4B-HR z8jh3>1_q{RPZ!6KiaBp@Z_H#d6mSSUDPwW##NX}Zwic7LZ^(15eRxkhK|;b#>a4D=I(liUAGxOzht#L8ueN#IcE2>vzGUFrxl(T=5BIO;9wc1 b#3A`hNBCE=m3MOhlO2PntDnm{r-UW|tJRj9 literal 0 HcmV?d00001 diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 28561ce..5ebfadd 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -152,6 +152,7 @@ class Assets { static const String imagesRocket1 = 'assets/images/rocket1.svga'; static const String imagesRocket2 = 'assets/images/rocket2.svga'; static const String imagesRocket3 = 'assets/images/rocket3.svga'; + static const String imagesRoomUserAdd = 'assets/images/room_user_add.png'; static const String imagesRoomVideo = 'assets/images/room_video.png'; static const String imagesRose = 'assets/images/rose.png'; static const String imagesRoseBanner = 'assets/images/rose_banner.png'; diff --git a/lib/widget/live/live_room_active_speaker.dart b/lib/widget/live/live_room_active_speaker.dart index bf55b82..2dfe78f 100644 --- a/lib/widget/live/live_room_active_speaker.dart +++ b/lib/widget/live/live_room_active_speaker.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() + ? Get.find() + : 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), ), ), ), - ), - ], - ), - ], - ); + ], + ), + ], + ); + }); } } - diff --git a/lib/widget/live/live_room_anchor_showcase.dart b/lib/widget/live/live_room_anchor_showcase.dart index b02883a..22af563 100644 --- a/lib/widget/live/live_room_anchor_showcase.dart +++ b/lib/widget/live/live_room_anchor_showcase.dart @@ -1,10 +1,13 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:dating_touchme_app/controller/discover/room_controller.dart'; +import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart'; +import 'package:dating_touchme_app/widget/live/live_room_guest_list_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class LiveRoomAnchorShowcase extends StatefulWidget { @@ -127,7 +130,8 @@ class _LiveRoomAnchorShowcaseState extends State { Widget _buildAnchorVideo(bool joined, int? remoteUid) { final engine = _rtcManager.engine; - if (_roomController.rtcChannelDetail.value?.anchorInfo == null || engine == null) { + if (_roomController.rtcChannelDetail.value?.anchorInfo == null || + engine == null) { return _buildWaitingPlaceholder(); } return ClipRRect( @@ -143,16 +147,17 @@ class _LiveRoomAnchorShowcaseState extends State { ), ) : AgoraVideoView( - controller: VideoViewController.remote( - rtcEngine: engine, - canvas: VideoCanvas( - uid: _roomController.rtcChannelDetail.value?.anchorInfo?.uid, - ), - connection: RtcConnection( - channelId: _rtcManager.currentChannelId!, - ), - ), - ), + controller: VideoViewController.remote( + rtcEngine: engine, + canvas: VideoCanvas( + uid: + _roomController.rtcChannelDetail.value?.anchorInfo?.uid, + ), + connection: RtcConnection( + channelId: _rtcManager.currentChannelId!, + ), + ), + ), ), ); } @@ -170,9 +175,9 @@ class _LiveRoomAnchorShowcaseState extends State { Column( children: [ Image.asset(Assets.imagesWaitting), - Text('暂时离开', style: TextStyle(color: Colors.white,)) + Text('暂时离开', style: TextStyle(color: Colors.white)), ], - ) + ), ], ), ); @@ -197,100 +202,143 @@ class _LiveRoomAnchorShowcaseState extends State { child: SizedBox( width: 177.w, height: 175.w, - child: userInfo != null && userInfo.uid != null && joined && engine != null ? Stack( - children: [ - Positioned( - top: 5.w, - right: 5.w, - child: Container( - width: 20.w, - height: 20.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(20.w)), - ), - child: Center( - child: Image.asset( - Assets.imagesGiftIcon, - width: 19.w, - height: 19.w, - ), - ), - ), - ), - Positioned( - bottom: 5.w, - right: 5.w, - child: Container( - width: 47.w, - height: 20.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(20.w)), - color: Colors.white, - ), - child: Center( - child: Text( - "加好友", - style: TextStyle( - fontSize: 11.w, - color: const Color.fromRGBO(117, 98, 249, 1), - ), - ), - ), - ), - ), - Positioned( - left: 5.w, - bottom: 5.w, - child: Row( + child: + userInfo != null && + userInfo.uid != null && + joined && + engine != null + ? Stack( children: [ - Container( - width: 20.w, - height: 20.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(4.w)), - color: const Color.fromRGBO(0, 0, 0, .65), + Positioned( + top: 5.w, + right: 5.w, + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(20.w), + ), + ), + child: Center( + child: Image.asset( + Assets.imagesGiftIcon, + width: 19.w, + height: 19.w, + ), + ), ), - child: Center( - child: Image.asset( - userInfo.isMicrophoneOn - ? Assets.imagesMicOpen - : Assets.imagesMicClose, - width: 10.w, - height: 11.w, + ), + Positioned( + bottom: 5.w, + right: 5.w, + child: Container( + width: 47.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(20.w), + ), + color: Colors.white, + ), + child: Center( + child: Text( + "加好友", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(117, 98, 249, 1), + ), + ), ), ), ), - SizedBox(width: 5.w), - Text( - userInfo.nickName, - style: TextStyle( - fontSize: 11.w, - color: Colors.white, - fontWeight: FontWeight.w500, + Positioned( + left: 5.w, + bottom: 5.w, + child: Row( + children: [ + 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( + userInfo.isMicrophoneOn + ? Assets.imagesMicOpen + : Assets.imagesMicClose, + width: 10.w, + height: 11.w, + ), + ), + ), + SizedBox(width: 5.w), + Text( + userInfo.nickName, + style: TextStyle( + fontSize: 11.w, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), + AgoraVideoView( + controller: isCurrentUser + ? VideoViewController( + rtcEngine: engine, + canvas: const VideoCanvas(uid: 0), + ) + : VideoViewController.remote( + rtcEngine: engine, + canvas: VideoCanvas(uid: userInfo.uid!), + connection: RtcConnection( + channelId: _rtcManager.currentChannelId ?? '', + ), + ), + ), ], - ), - ), - AgoraVideoView( - controller: isCurrentUser - ? VideoViewController( - rtcEngine: engine, - canvas: const VideoCanvas(uid: 0), ) - : VideoViewController.remote( - rtcEngine: engine, - canvas: VideoCanvas(uid: userInfo.uid!), - connection: RtcConnection( - channelId: _rtcManager.currentChannelId ?? '', + : _roomController.currentRole == CurrentRole.broadcaster + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isLeft ? 10.w : 0.w), + topRight: Radius.circular(!isLeft ? 10.w : 0.w), + bottomLeft: Radius.circular(isLeft ? 10.w : 0.w), + bottomRight: Radius.circular(!isLeft ? 10.w : 0.w), + ), + color: const Color(0xff15143C), + ), + child: Center( + child: Image.asset(Assets.imagesRoomUserAdd, width: 50.w), ), + ).onTap(() { + _showGuestListDialog(context, isLeft); + }) + : Image.asset( + isLeft ? Assets.imagesMaleEmpty : Assets.imagesFemaleEmpty, ), - ) - ], - ) : Image.asset(isLeft ? Assets.imagesMaleEmpty : Assets.imagesFemaleEmpty), ), ), ], ); } + + void _showGuestListDialog(BuildContext context, bool isMaleSeat) { + SmartDialog.show( + alignment: Alignment.bottomCenter, + maskColor: Colors.black.withOpacity(0.5), + builder: (context) { + return LiveRoomGuestListDialog( + initialTab: isMaleSeat ? 1 : 0, // 0: 女嘉宾, 1: 男嘉宾 + ); + }, + ); + } } From 4d7e2a1d9352613328569331faa4a26b79cb3b02 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Fri, 28 Nov 2025 00:08:59 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(live):=20=E6=B7=BB=E5=8A=A0=E7=9B=B4?= =?UTF-8?q?=E6=92=AD=E9=97=B4=E5=98=89=E5=AE=BE=E5=88=97=E8=A1=A8=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了可切换标签的嘉宾列表展示界面 - 支持按性别筛选显示男嘉宾或女嘉宾 - 集成了IM管理器获取会话和联系人信息 - 调用网络服务获取用户基础资料和扩展信息 - 展示用户头像、昵称、年龄、地区和VIP等级 - 提供邀请按钮和已有嘉宾状态提示 - 使用CachedNetworkImage优化图片加载体验 - 添加了加载状态指示器和空数据提示 - 实现了下拉刷新和上拉加载更多功能 - 支持根据用户ID获取并解析环信用户扩展信息 --- .../live/live_room_guest_list_dialog.dart | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 lib/widget/live/live_room_guest_list_dialog.dart diff --git a/lib/widget/live/live_room_guest_list_dialog.dart b/lib/widget/live/live_room_guest_list_dialog.dart new file mode 100644 index 0000000..8ba417d --- /dev/null +++ b/lib/widget/live/live_room_guest_list_dialog.dart @@ -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 createState() => + _LiveRoomGuestListDialogState(); +} + +class _LiveRoomGuestListDialogState extends State { + late int _selectedTab; // 0: 女嘉宾, 1: 男嘉宾 + List> _guestList = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _selectedTab = widget.initialTab; + _loadContacts(); + } + + Future _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> 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) { + 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 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 = []; + if (age != null) { + parts.add('${age}岁'); + } + if (location != null && location.isNotEmpty) { + parts.add(location); + } + return parts.isEmpty ? '' : parts.join('·'); + } +} From b49415550e2b7efd47ec916ecd81c25a4eda4209 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Fri, 28 Nov 2025 00:21:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(live):=20=E4=BC=98=E5=8C=96=E7=A4=BC?= =?UTF-8?q?=E7=89=A9=E5=BC=B9=E7=AA=97=E6=94=AF=E6=8C=81=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=B5=A0=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 showHeader 参数控制是否显示用户选择头部 - 新增 targetUserId 参数用于预设礼物接收用户 - 优化礼物弹窗逻辑,支持直接向指定用户赠送礼物 - 在主播展示页面增加向主播或观众赠送礼物的功能入口 - 修复未选择用户时的提示逻辑,避免重复提醒 - 重构 _showGiftPopupForUser 方法统一处理礼物弹窗展示逻辑 --- lib/widget/live/live_gift_popup.dart | 16 +++- .../live/live_room_anchor_showcase.dart | 78 ++++++++++++++++--- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/lib/widget/live/live_gift_popup.dart b/lib/widget/live/live_gift_popup.dart index d53a361..b14cd8a 100644 --- a/lib/widget/live/live_gift_popup.dart +++ b/lib/widget/live/live_gift_popup.dart @@ -17,12 +17,16 @@ class LiveGiftPopup extends StatefulWidget { required this.giftNum, required this.giftList, required this.changeActive, + this.showHeader = true, + this.targetUserId, }); final ValueNotifier activeGift; final ValueNotifier giftNum; final List giftList; // 支持 List 或 List final void Function(int) changeActive; + final bool showHeader; // 是否显示头部(用户选择部分) + final String? targetUserId; // 预设的目标用户ID @override State createState() => _LiveGiftPopupState(); @@ -35,6 +39,10 @@ class _LiveGiftPopupState extends State { @override void initState() { super.initState(); + // 如果传入了目标用户ID,直接设置 + if (widget.targetUserId != null) { + _selectedUserId = widget.targetUserId; + } // 默认选择第一个礼物 if (widget.giftList.isNotEmpty && widget.activeGift.value == null) { widget.activeGift.value = 0; @@ -65,9 +73,11 @@ class _LiveGiftPopupState extends State { return; } - // 检查是否选中了接收用户 + // 检查是否选中了接收用户(如果showHeader为false,则必须有targetUserId) if (_selectedUserId == null || _selectedUserId!.isEmpty) { - SmartDialog.showToast('请先选择接收礼物的用户'); + if (widget.showHeader) { + SmartDialog.showToast('请先选择接收礼物的用户'); + } return; } @@ -110,7 +120,7 @@ class _LiveGiftPopupState extends State { height: 363.w, child: Column( children: [ - _buildHeader(), + if (widget.showHeader) _buildHeader(), Expanded( child: Container( decoration: BoxDecoration( diff --git a/lib/widget/live/live_room_anchor_showcase.dart b/lib/widget/live/live_room_anchor_showcase.dart index 22af563..b5744f5 100644 --- a/lib/widget/live/live_room_anchor_showcase.dart +++ b/lib/widget/live/live_room_anchor_showcase.dart @@ -4,6 +4,7 @@ import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart'; +import 'package:dating_touchme_app/widget/live/live_gift_popup.dart'; import 'package:dating_touchme_app/widget/live/live_room_guest_list_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -69,11 +70,20 @@ class _LiveRoomAnchorShowcaseState extends State { color: const Color.fromRGBO(0, 0, 0, .3), ), child: Center( - child: Image.asset( - Assets.imagesGiftIcon, - width: 19.w, - height: 19.w, - ), + child: + Image.asset( + Assets.imagesGiftIcon, + width: 19.w, + height: 19.w, + ).onTap(() { + final anchorInfo = _roomController + .rtcChannelDetail + .value + ?.anchorInfo; + if (anchorInfo != null) { + _showGiftPopupForUser(anchorInfo); + } + }), ), ), ), @@ -221,11 +231,14 @@ class _LiveRoomAnchorShowcaseState extends State { ), ), child: Center( - child: Image.asset( - Assets.imagesGiftIcon, - width: 19.w, - height: 19.w, - ), + child: + Image.asset( + Assets.imagesGiftIcon, + width: 19.w, + height: 19.w, + ).onTap(() { + _showGiftPopupForUser(userInfo); + }), ), ), ), @@ -341,4 +354,49 @@ class _LiveRoomAnchorShowcaseState extends State { }, ); } + + void _showGiftPopupForUser(RtcSeatUserInfo? userInfo) { + if (userInfo == null) { + return; + } + + // 获取目标用户ID(优先使用userId,如果没有则使用miId) + final targetUserId = userInfo.userId.isNotEmpty + ? userInfo.userId + : userInfo.miId.isNotEmpty + ? userInfo.miId + : null; + + if (targetUserId == null) { + SmartDialog.showToast('用户ID不存在'); + return; + } + + // 创建必要的 ValueNotifier + final activeGift = ValueNotifier(null); + final giftNum = ValueNotifier(1); + + SmartDialog.show( + alignment: Alignment.bottomCenter, + maskColor: Colors.black.withOpacity(0.5), + builder: (context) { + return Obx(() { + // 获取礼物列表 + final giftProducts = _roomController.giftProducts; + final giftList = giftProducts.toList(); + + return LiveGiftPopup( + activeGift: activeGift, + giftNum: giftNum, + giftList: giftList, + changeActive: (index) { + activeGift.value = index; + }, + showHeader: false, // 不显示头部 + targetUserId: targetUserId, // 设置目标用户ID + ); + }); + }, + ); + } }