From 325a4dcf8cef61fd570bd1bb076d3b0ccd22794b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AD=90=E8=B4=A4?= Date: Mon, 26 Jan 2026 18:21:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/controller/discover/room_controller.dart | 142 ++++++++++ lib/model/discover/audience_list_data.dart | 104 ++++++++ lib/network/api_urls.dart | 7 + lib/network/rtc_api.dart | 21 ++ lib/network/rtc_api.g.dart | 86 ++++++ lib/rtc/rtc_manager.dart | 8 + .../live/live_room_anchor_showcase.dart | 33 ++- .../live/live_room_invitation_list.dart | 251 +++++++++++------- 8 files changed, 555 insertions(+), 97 deletions(-) create mode 100644 lib/model/discover/audience_list_data.dart diff --git a/lib/controller/discover/room_controller.dart b/lib/controller/discover/room_controller.dart index c5d184e..e34f3ad 100644 --- a/lib/controller/discover/room_controller.dart +++ b/lib/controller/discover/room_controller.dart @@ -12,12 +12,14 @@ import 'package:dating_touchme_app/pages/mine/edit_info_page.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:dating_touchme_app/rtc/rtm_manager.dart'; import 'package:dating_touchme_app/service/live_chat_message_service.dart'; +import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; +import '../../model/discover/audience_list_data.dart'; import '../../model/live/live_chat_message.dart'; import '../../pages/discover/live_end_page.dart'; import '../../pages/mine/real_name_page.dart'; @@ -78,6 +80,10 @@ class RoomController extends GetxController with WidgetsBindingObserver { _registerMessageListener(); // 加载礼物产品列表 loadGiftProducts(); + listRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); } @override @@ -97,6 +103,142 @@ class RoomController extends GetxController with WidgetsBindingObserver { chatMessages.clear(); } + final tab = 0.obs; + + changeTab(int i) async { + tab.value = i; + if(tab.value == 0){ + + page.value = 1; + audienceList.clear(); + await getAudienceList(); + } else if(tab.value == 1) { + page.value = 1; + audienceList.clear(); + await getOnMicList(); + } else if(tab.value == 2) { + page.value = 1; + audienceList.clear(); + await getFriendList(); + } + listRefreshController.finishRefresh(IndicatorResult.success); + listRefreshController.finishLoad(IndicatorResult.none); + } + + final page = 1.obs; + + final audienceList = [].obs; + + late final EasyRefreshController listRefreshController; + + getAudienceList() async { + try{ + + final channelId = RTCManager.instance.currentChannelId; + final response = await _networkService.rtcApi.userPageRtcChannelAudience( + pageNum: page.value, + pageSize: 10, + channelId: channelId ?? "" + ); + if (response.data.isSuccess && response.data.data != null) { + final data = response.data.data?.records ?? []; + + audienceList.addAll(data.toList()); + if((data.length ?? 0) == 10){ + + listRefreshController.finishLoad(IndicatorResult.success); + } else { + listRefreshController.finishLoad(IndicatorResult.noMore); + } + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e) { + print('提现记录获取失败: $e'); + SmartDialog.showToast('提现记录获取失败'); + rethrow; + } + } + + getOnMicList() async { + try{ + + final channelId = RTCManager.instance.currentChannelId; + final response = await _networkService.rtcApi.userPageMicJoinRtcChannelUser( + pageNum: page.value, + pageSize: 10, + channelId: channelId ?? "" + ); + if (response.data.isSuccess && response.data.data != null) { + final data = response.data.data?.records ?? []; + + audienceList.addAll(data.toList()); + if((data.length ?? 0) == 10){ + + listRefreshController.finishLoad(IndicatorResult.success); + } else { + listRefreshController.finishLoad(IndicatorResult.noMore); + } + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e) { + print('提现记录获取失败: $e'); + SmartDialog.showToast('提现记录获取失败'); + rethrow; + } + } + + + getFriendList() async { + try{ + final response = await _networkService.userApi.userPageFriendRelation( + pageNum: page.value, + pageSize: 10, + ); + if (response.data.isSuccess && response.data.data != null) { + final data = response.data.data?.records ?? []; + final List b = data.map((e){ + return Records( + id: e.id, + userManagementId: null, + userId: e.userId, + miId: e.miId, + nickName: e.nickName, + profilePhoto: e.profilePhoto, + genderCode: e.genderCode, + birthYear: e.birthYear, + birthDate: e.birthDate, + age: e.age, + provinceCode: e.provinceCode, + provinceName: e.provinceName, + cityCode: e.cityCode, + cityName: e.cityName, + ); + }).toList(); + audienceList.addAll(b); + if((data.length ?? 0) == 10){ + + listRefreshController.finishLoad(IndicatorResult.success); + } else { + listRefreshController.finishLoad(IndicatorResult.noMore); + } + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e) { + print('好友列表获取失败: $e'); + SmartDialog.showToast('好友列表获取失败'); + rethrow; + } + } + void setDialogDismiss(bool flag){ isDialogShowing.value = flag; } diff --git a/lib/model/discover/audience_list_data.dart b/lib/model/discover/audience_list_data.dart new file mode 100644 index 0000000..69a32b6 --- /dev/null +++ b/lib/model/discover/audience_list_data.dart @@ -0,0 +1,104 @@ +class AudienceListData { + List? records; + int? total; + int? size; + int? current; + int? pages; + + AudienceListData( + {this.records, this.total, this.size, this.current, this.pages}); + + AudienceListData.fromJson(Map json) { + if (json['records'] != null) { + records = []; + json['records'].forEach((v) { + records!.add(new Records.fromJson(v)); + }); + } + total = json['total']; + size = json['size']; + current = json['current']; + pages = json['pages']; + } + + Map toJson() { + final Map data = new Map(); + if (this.records != null) { + data['records'] = this.records!.map((v) => v.toJson()).toList(); + } + data['total'] = this.total; + data['size'] = this.size; + data['current'] = this.current; + data['pages'] = this.pages; + return data; + } +} + +class Records { + String? id; + int? userManagementId; + String? userId; + String? miId; + String? nickName; + String? profilePhoto; + int? genderCode; + String? birthYear; + String? birthDate; + int? age; + int? provinceCode; + String? provinceName; + int? cityCode; + String? cityName; + + Records( + {this.id, + this.userManagementId, + this.userId, + this.miId, + this.nickName, + this.profilePhoto, + this.genderCode, + this.birthYear, + this.birthDate, + this.age, + this.provinceCode, + this.provinceName, + this.cityCode, + this.cityName}); + + Records.fromJson(Map json) { + id = json['id']; + userManagementId = json['userManagementId']; + userId = json['userId']; + miId = json['miId']; + nickName = json['nickName']; + profilePhoto = json['profilePhoto']; + genderCode = json['genderCode']; + birthYear = json['birthYear']; + birthDate = json['birthDate']; + age = json['age']; + provinceCode = json['provinceCode']; + provinceName = json['provinceName']; + cityCode = json['cityCode']; + cityName = json['cityName']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['userManagementId'] = this.userManagementId; + data['userId'] = this.userId; + data['miId'] = this.miId; + data['nickName'] = this.nickName; + data['profilePhoto'] = this.profilePhoto; + data['genderCode'] = this.genderCode; + data['birthYear'] = this.birthYear; + data['birthDate'] = this.birthDate; + data['age'] = this.age; + data['provinceCode'] = this.provinceCode; + data['provinceName'] = this.provinceName; + data['cityCode'] = this.cityCode; + data['cityName'] = this.cityName; + return data; + } +} diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index b0f62c6..983a07c 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -152,4 +152,11 @@ class ApiUrls { static const String consumeOneOnOneRtcChannel = 'dating-agency-chat-audio/user/consume/one-on-one/rtc-channel'; + //直播 + + static const String userPageRtcChannelAudience = + 'dating-agency-chat-audio/user/page/rtc-channel-audience'; + + static const String userPageMicJoinRtcChannelUser = + 'dating-agency-chat-audio/user/page/mic-join/rtc-channel-user'; } diff --git a/lib/network/rtc_api.dart b/lib/network/rtc_api.dart index 1a2db80..05070c8 100644 --- a/lib/network/rtc_api.dart +++ b/lib/network/rtc_api.dart @@ -1,3 +1,4 @@ +import 'package:dating_touchme_app/model/discover/audience_list_data.dart'; import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart'; import 'package:dating_touchme_app/model/live/gift_product_model.dart'; import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart'; @@ -137,4 +138,24 @@ abstract class RtcApi { Future>> consumeOneOnOneRtcChannel( @Body() Map data, ); + + + + @GET(ApiUrls.userPageRtcChannelAudience) + Future>> userPageRtcChannelAudience( + { + @Query('pageNum') required int pageNum, + @Query('pageSize') required int pageSize, + @Query('channelId') required String channelId, + } + ); + + @GET(ApiUrls.userPageMicJoinRtcChannelUser) + Future>> userPageMicJoinRtcChannelUser( + { + @Query('pageNum') required int pageNum, + @Query('pageSize') required int pageSize, + @Query('channelId') required String channelId, + } + ); } diff --git a/lib/network/rtc_api.g.dart b/lib/network/rtc_api.g.dart index bb9cad9..88b81b4 100644 --- a/lib/network/rtc_api.g.dart +++ b/lib/network/rtc_api.g.dart @@ -786,6 +786,92 @@ class _RtcApi implements RtcApi { return httpResponse; } + @override + Future>> + userPageRtcChannelAudience({ + required int pageNum, + required int pageSize, + required String channelId, + }) async { + final _extra = {}; + final queryParameters = { + r'pageNum': pageNum, + r'pageSize': pageSize, + r'channelId': channelId, + }; + final _headers = {}; + const Map? _data = null; + final _options = + _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-chat-audio/user/page/rtc-channel-audience', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl), + ), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => AudienceListData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> + userPageMicJoinRtcChannelUser({ + required int pageNum, + required int pageSize, + required String channelId, + }) async { + final _extra = {}; + final queryParameters = { + r'pageNum': pageNum, + r'pageSize': pageSize, + r'channelId': channelId, + }; + final _headers = {}; + const Map? _data = null; + final _options = + _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-chat-audio/user/page/mic-join/rtc-channel-user', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl), + ), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => AudienceListData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart index b837c9d..fb271c4 100644 --- a/lib/rtc/rtc_manager.dart +++ b/lib/rtc/rtc_manager.dart @@ -105,6 +105,7 @@ class RTCManager { )); await _engine?.enableVideo(); // await _engine?.startPreview(); + await _engine?.enableFaceDetection(true); // 注册事件处理器 _registerEventHandlers(); @@ -326,6 +327,13 @@ class RTCManager { ); _engine?.renewToken(rtcToken); }, + onFacePositionChanged: (int imageWidth, + int imageHeight, + List vecRectangle, + List vecDistance, + int numFaces){ + print("当前人脸数:$numFaces"); + }, onError: (ErrorCodeType err, String msg) { print('RTC Engine 错误:$err,消息:$msg'); if (onError != null) { diff --git a/lib/widget/live/live_room_anchor_showcase.dart b/lib/widget/live/live_room_anchor_showcase.dart index 01cbf5c..59fa0ac 100644 --- a/lib/widget/live/live_room_anchor_showcase.dart +++ b/lib/widget/live/live_room_anchor_showcase.dart @@ -130,6 +130,35 @@ class _LiveRoomAnchorShowcaseState extends State { ), ); }), + 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( + (_roomController + .rtcChannelDetail + .value?.anchorInfo?.isMicrophoneOn ?? false) + ? Assets.imagesMicOpen + : Assets.imagesMicClose, + width: 10.w, + height: 11.w, + ), + ), + ), + ], + ), + ), ], ), SizedBox(height: 5.w), @@ -379,7 +408,9 @@ class _LiveRoomAnchorShowcaseState extends State { ); } - void _showGuestListDialog(BuildContext context, bool isMaleSeat) { + void _showGuestListDialog(BuildContext context, bool isMaleSeat) async { + _roomController.audienceList.clear(); + await _roomController.getAudienceList(); _roomController.setDialogDismiss(true); SmartDialog.show( alignment: Alignment.bottomCenter, diff --git a/lib/widget/live/live_room_invitation_list.dart b/lib/widget/live/live_room_invitation_list.dart index e807b00..934cc2e 100644 --- a/lib/widget/live/live_room_invitation_list.dart +++ b/lib/widget/live/live_room_invitation_list.dart @@ -1,6 +1,9 @@ +import 'package:cached_network_image/cached_network_image.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/discover/audience_list_data.dart'; +import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -51,7 +54,7 @@ class _LiveRoomInvitationListState extends State with Ti controller: _tabController, showIndicator: true, onTap: (int i) async { - + _roomController.changeTab(i); }, ), ), @@ -61,102 +64,85 @@ class _LiveRoomInvitationListState extends State with Ti child: Column( children: [ Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - Container( - margin: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Image.asset( - Assets.imagesUserAvatar, - width: 40.w, - height: 40.w, - ), - SizedBox(width: 5.w,), - Column( - children: [ - Text( - "开心", - style: TextStyle( - fontSize: 12.w - ), - ), - Text( - "22岁", - style: TextStyle( - fontSize: 12.w, - color: const Color.fromRGBO(121, 121, 121, 1) - ), - ), - ], - ) - ], - ), - - Checkbox( - value: false, - onChanged: (value) { - - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - Container( - margin: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Image.asset( - Assets.imagesUserAvatar, - width: 40.w, - height: 40.w, - ), - SizedBox(width: 5.w,), - Column( - children: [ - Text( - "开心", - style: TextStyle( - fontSize: 12.w - ), - ), - Text( - "22岁", - style: TextStyle( - fontSize: 12.w, - color: const Color.fromRGBO(121, 121, 121, 1) - ), - ), - ], - ) - ], - ), - - Checkbox( - value: false, - onChanged: (value) { + child: EasyRefresh( - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - ], + controller: _roomController.listRefreshController, + header: const ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '刷新中...', + processingText: '刷新中...', + processedText: '刷新完成', + failedText: '刷新失败', + noMoreText: '没有更多数据', + showMessage: false + ), + footer: ClassicFooter( + dragText: '上拉加载', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败', + noMoreText: '没有更多数据', + showMessage: false + ), + // 下拉刷新 + onRefresh: () async { + print('推荐列表下拉刷新被触发'); + _roomController.page.value = 1; + _roomController.audienceList.clear(); + if(_roomController.tab.value == 0){ + await _roomController.getAudienceList(); + } else if(_roomController.tab.value == 1){ + await _roomController.getOnMicList(); + } else if(_roomController.tab.value == 2){ + await _roomController.getFriendList(); + } + _roomController.listRefreshController.finishRefresh(IndicatorResult.success); + _roomController.listRefreshController.finishLoad(IndicatorResult.none); + }, + // 上拉加载更多 + onLoad: () async { + print('推荐列表上拉加载被触发, hasMore: '); + _roomController.page.value += 1; + if(_roomController.tab.value == 0){ + await _roomController.getAudienceList(); + } else if(_roomController.tab.value == 1){ + await _roomController.getOnMicList(); + } else if(_roomController.tab.value == 2){ + await _roomController.getFriendList(); + } + }, + child: ListView.separated( + // 关键:始终允许滚动,即使内容不足 + // 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 + padding: EdgeInsets.only(left: 12, right: 12), + itemBuilder: (context, index) { + // 空数据状态 + if (_roomController.audienceList.isEmpty && index == 0) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('暂无数据'), + ], + ), + ); + } + // 数据项 + final item = _roomController.audienceList[index]; + return AudienceItem(item: item); + }, + separatorBuilder: (context, index) { + // 空状态或加载状态时不显示分隔符 + if (_roomController.audienceList.isEmpty) { + return const SizedBox.shrink(); + } + return const SizedBox(height: 12); + }, + // 至少显示一个 item(用于显示加载或空状态) + itemCount: _roomController.audienceList.isEmpty ? 1 : _roomController.audienceList.length, ), ), ) @@ -347,3 +333,76 @@ class _LiveRoomInvitationListState extends State with Ti } } +class AudienceItem extends StatefulWidget { + final Records item; + const AudienceItem({super.key, required this.item}); + + @override + State createState() => _AudienceItemState(); +} + +class _AudienceItemState extends State { + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(40.w)), + child: CachedNetworkImage( + imageUrl: "${widget.item.profilePhoto ?? ""}?x-oss-process=image/format,webp/resize,w_120", + + width: 40.w, + height: 40.w, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + SizedBox(width: 5.w,), + Column( + children: [ + Text( + widget.item.nickName ?? "", + style: TextStyle( + fontSize: 12.w + ), + ), + Text( + "${widget.item.age ?? ""}岁", + style: TextStyle( + fontSize: 12.w, + color: const Color.fromRGBO(121, 121, 121, 1) + ), + ), + ], + ) + ], + ), + + Checkbox( + value: false, + onChanged: (value) { + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], + ), + ); + } +} + +