Browse Source

feat(discover): 实现相亲与聚会脱单直播页面

- 新增相亲页面(DatingPage)和聚会脱单页面(PartyPage)
- 实现RTC频道分页列表接口(getRtcChannelPage)
- 创建发现页控制器(DiscoverController)管理频道数据
- 添加直播项组件(LiveItemWidget)用于展示频道列表
- 更新主页发现页结构,使用PageView切换相亲与聚会页面
- 修改直播间主播展示逻辑,优化RTC用户信息处理
- 完善RTC Manager中的远程用户管理逻辑
- 重构房间控制器中的角色管理和用户信息同步逻辑
ios
Jolie 4 months ago
parent
commit
ae78302a42
13 changed files with 750 additions and 306 deletions
  1. 19
      lib/components/home_appbar.dart
  2. 68
      lib/controller/discover/discover_controller.dart
  3. 124
      lib/controller/discover/room_controller.dart
  4. 34
      lib/model/discover/rtc_channel_model.dart
  5. 2
      lib/network/api_urls.dart
  6. 5
      lib/network/rtc_api.dart
  7. 40
      lib/network/rtc_api.g.dart
  8. 142
      lib/pages/discover/dating_page.dart
  9. 260
      lib/pages/discover/discover_page.dart
  10. 219
      lib/pages/discover/live_item_widget.dart
  11. 130
      lib/pages/discover/party_page.dart
  12. 1
      lib/rtc/rtc_manager.dart
  13. 12
      lib/widget/live/live_room_anchor_showcase.dart

19
lib/components/home_appbar.dart

@ -7,7 +7,8 @@ class HomeAppbar extends StatefulWidget {
final List<String> topNav;
final void Function(int) changeNav;
final Widget right;
const HomeAppbar({super.key, required this.topNav, required this.changeNav, this.right = const SizedBox()});
final int? activeIndex;
const HomeAppbar({super.key, required this.topNav, required this.changeNav, this.right = const SizedBox(), this.activeIndex});
@override
State<HomeAppbar> createState() => _HomeAppbarState();
@ -15,10 +16,12 @@ class HomeAppbar extends StatefulWidget {
class _HomeAppbarState extends State<HomeAppbar> {
int active = 0;
int get active => widget.activeIndex ?? _internalActive;
int _internalActive = 0;
@override
Widget build(BuildContext context) {
final currentActive = widget.activeIndex ?? _internalActive;
return Container(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
color: Colors.transparent,
@ -37,8 +40,10 @@ class _HomeAppbarState extends State<HomeAppbar> {
...widget.topNav.asMap().entries.map((entry){
return InkWell(
onTap: (){
active = entry.key;
widget.changeNav(active);
if (widget.activeIndex == null) {
_internalActive = entry.key;
}
widget.changeNav(entry.key);
setState(() {
});
@ -52,13 +57,13 @@ class _HomeAppbarState extends State<HomeAppbar> {
Text(
entry.value,
style: TextStyle(
fontSize: active == entry.key ? 19 : 17,
fontSize: currentActive == entry.key ? 19 : 17,
color: const Color.fromRGBO(51, 51, 51, 1),
fontWeight: active == entry.key ? FontWeight.w600 : FontWeight.w400
fontWeight: currentActive == entry.key ? FontWeight.w600 : FontWeight.w400
),
),
SizedBox(height: ScreenUtil().setWidth(4),),
if(active == entry.key) Image.asset(
if(currentActive == entry.key) Image.asset(
Assets.imagesTabChangeIcon,
width: 20,
)

68
lib/controller/discover/discover_controller.dart

@ -0,0 +1,68 @@
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart';
import 'package:dating_touchme_app/network/network_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
///
class DiscoverController extends GetxController {
DiscoverController({NetworkService? networkService})
: _networkService = networkService ?? Get.find<NetworkService>();
final NetworkService _networkService;
/// RTC
final rtcChannelList = <RtcChannelModel>[].obs;
///
final isLoading = false.obs;
@override
void onInit() {
super.onInit();
//
loadRtcChannelPage();
}
/// RTC
Future<void> loadRtcChannelPage() async {
try {
isLoading.value = true;
final response = await _networkService.rtcApi.getRtcChannelPage();
final base = response.data;
print('API 响应: isSuccess=${base.isSuccess}, data=${base.data}');
if (base.isSuccess) {
if (base.data != null) {
// base.data PaginatedResponse<RtcChannelModel>
final paginatedData = base.data!;
print(
'分页数据: total=${paginatedData.total}, records长度=${paginatedData.records.length}',
);
// PaginatedResponse records
rtcChannelList.assignAll(paginatedData.records);
print('更新后的列表长度: ${rtcChannelList.length}');
} else {
print('base.data 为 null');
rtcChannelList.clear();
}
} else {
final message = base.message.isNotEmpty ? base.message : '获取频道列表失败';
print('API 请求失败: $message');
SmartDialog.showToast(message);
rtcChannelList.clear();
}
} catch (e, stackTrace) {
print('获取频道列表异常: $e');
print('堆栈: $stackTrace');
SmartDialog.showToast('获取频道列表异常:$e');
rtcChannelList.clear();
} finally {
isLoading.value = false;
}
}
/// RTC
Future<void> refreshRtcChannelPage() async {
await loadRtcChannelPage();
}
}

124
lib/controller/discover/room_controller.dart

@ -180,15 +180,18 @@ class RoomController extends GetxController {
}
RtcSeatUserInfo userInfo = RtcSeatUserInfo(
uid: rtcChannel.value?.uid,
miId: GlobalData().userData?.id ?? '',
userId: GlobalData().userData?.id ?? '',
nickName: GlobalData().userData?.nickName ?? '',
profilePhoto: GlobalData().userData?.profilePhoto ?? '',
seatNumber: role == CurrentRole.maleAudience ? 1 : 2,
isFriend: false,
isMicrophoneOn: true,
isVideoOn: role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience ? true : false,
genderCode: GlobalData().userData?.genderCode ?? 0,
miId: GlobalData().userData?.id ?? '',
userId: GlobalData().userData?.id ?? '',
nickName: GlobalData().userData?.nickName ?? '',
profilePhoto: GlobalData().userData?.profilePhoto ?? '',
seatNumber: role == CurrentRole.maleAudience ? 1 : 2,
isFriend: false,
isMicrophoneOn: true,
isVideoOn:
role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience
? true
: false,
genderCode: GlobalData().userData?.genderCode ?? 0,
);
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
@ -277,20 +280,23 @@ class RoomController extends GetxController {
Future<void> leaveChannel() async {
isLive = false;
if (currentRole == CurrentRole.maleAudience || currentRole == CurrentRole.femaleAudience) {
if (currentRole == CurrentRole.maleAudience ||
currentRole == CurrentRole.femaleAudience) {
await RTCManager.instance.unpublish(currentRole);
}
currentRole = CurrentRole.normalUser;;
currentRole = CurrentRole.normalUser;
;
await RTCManager.instance.leaveChannel();
}
/// RTC消息
Future<void> receiveRTCMessage(Map<String, dynamic> message) async {
if (message['type'] == 'join_chat') {
final response = await _networkService.rtcApi.getDatingRtcChannelUserDetail(
rtcChannel.value!.channelId,
message['uid'],
);
final response = await _networkService.rtcApi
.getDatingRtcChannelUserDetail(
rtcChannel.value!.channelId,
message['uid'],
);
if (!response.data.isSuccess) {
return;
}
@ -301,55 +307,55 @@ class RoomController extends GetxController {
if (message['role'] == 'male_audience') {
final userData = response.data.data;
// if (userData != null) {
final maleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: maleInfo,
femaleInfo: currentDetail.femaleInfo,
);
rtcChannelDetail.value = newDetail;
final maleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: maleInfo,
femaleInfo: currentDetail.femaleInfo,
);
rtcChannelDetail.value = newDetail;
// }
} else if (message['role'] == 'female_audience') {
final userData = response.data.data;
// if (userData != null) {
final femaleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: currentDetail.maleInfo,
femaleInfo: femaleInfo,
);
rtcChannelDetail.value = newDetail;
}
final femaleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: currentDetail.maleInfo,
femaleInfo: femaleInfo,
);
rtcChannelDetail.value = newDetail;
}
// }
}else if (message['type'] == 'leave_chat') {
} else if (message['type'] == 'leave_chat') {
if (message['role'] == 'male_audience') {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,

34
lib/model/discover/rtc_channel_model.dart

@ -0,0 +1,34 @@
/// RTC
class RtcChannelModel {
final String channelId;
final String channelPic;
final String channelName;
RtcChannelModel({
required this.channelId,
required this.channelPic,
required this.channelName,
});
factory RtcChannelModel.fromJson(Map<String, dynamic> json) {
return RtcChannelModel(
channelId: json['channelId']?.toString() ?? '',
channelPic: json['channelPic']?.toString() ?? '',
channelName: json['channelName']?.toString() ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'channelId': channelId,
'channelPic': channelPic,
'channelName': channelName,
};
}
@override
String toString() {
return 'RtcChannelModel(channelId: $channelId, channelName: $channelName)';
}
}

2
lib/network/api_urls.dart

@ -56,6 +56,8 @@ class ApiUrls {
'dating-agency-chat-audio/user/enable/rtc-channel-user/audio';
static const String disconnectRtcChannel =
'dating-agency-chat-audio/user/disconnect/rtc-channel';
static const String getRtcChannelPage =
'dating-agency-chat-audio/user/page/rtc-channel';
static const String listBankCardByIndividual =
'dating-agency-mall/user/list/bank-card/by-individual';
static const String createBankCardByIndividual =

5
lib/network/rtc_api.dart

@ -1,3 +1,4 @@
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/network/api_urls.dart';
@ -56,4 +57,8 @@ abstract class RtcApi {
Future<HttpResponse<BaseResponse<dynamic>>> disconnectRtcChannel(
@Body() Map<String, dynamic> data,
);
/// RTC
@GET(ApiUrls.getRtcChannelPage)
Future<HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>> getRtcChannelPage();
}

40
lib/network/rtc_api.g.dart

@ -287,6 +287,46 @@ class _RtcApi implements RtcApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>>
getRtcChannelPage() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options =
_setStreamType<
HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>
>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/page/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<PaginatedResponse<RtcChannelModel>> _value;
try {
_value = BaseResponse<PaginatedResponse<RtcChannelModel>>.fromJson(
_result.data!,
(json) => PaginatedResponse<RtcChannelModel>.fromJson(
json as Map<String, dynamic>,
(json) => RtcChannelModel.fromJson(json as Map<String, dynamic>),
),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

142
lib/pages/discover/dating_page.dart

@ -0,0 +1,142 @@
import 'package:dating_touchme_app/controller/discover/discover_controller.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/pages/discover/live_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import '../../controller/discover/room_controller.dart';
///
class DatingPage extends StatefulWidget {
const DatingPage({super.key});
@override
State<DatingPage> createState() => _DatingPageState();
}
class _DatingPageState extends State<DatingPage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
late final DiscoverController discoverController;
late final TabController _tabController;
late final RoomController roomController;
@override
void initState() {
super.initState();
if (Get.isRegistered<DiscoverController>()) {
discoverController = Get.find<DiscoverController>();
} else {
discoverController = Get.put(DiscoverController());
}
if (Get.isRegistered<RoomController>()) {
roomController = Get.find<RoomController>();
} else {
roomController = Get.put(RoomController());
}
_tabController = TabController(length: 4, vsync: this);
discoverController.loadRtcChannelPage();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Column(
children: [
TDTabBar(
tabs: [
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 16, left: 16),
child: Text('全部'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 16, left: 16),
child: Text('同城'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 12, left: 12),
child: Text('相亲视频'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 12, left: 12),
child: Text('相亲语音'),
),
),
],
backgroundColor: Colors.transparent,
labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4),
selectedBgColor: const Color.fromRGBO(108, 105, 244, 1),
unSelectedBgColor: Colors.transparent,
labelColor: Colors.white,
dividerHeight: 0,
tabAlignment: TabAlignment.start,
outlineType: TDTabBarOutlineType.capsule,
controller: _tabController,
showIndicator: false,
isScrollable: true,
onTap: (index) {
print('相亲页面 Tab: $index');
},
),
Expanded(
child: Obx(() {
print('DatingPage Obx 触发,列表长度: ${discoverController.rtcChannelList.length}');
print('isLoading: ${discoverController.isLoading.value}');
if (discoverController.isLoading.value &&
discoverController.rtcChannelList.isEmpty) {
return Center(
child: CircularProgressIndicator(
color: const Color.fromRGBO(108, 105, 244, 1),
),
);
}
if (discoverController.rtcChannelList.isEmpty) {
return Center(
child: Text(
'暂无直播频道',
style: TextStyle(
fontSize: 14.w,
color: Colors.white.withOpacity(0.7),
),
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Wrap(
spacing: 7.w,
runSpacing: 7.w,
children: discoverController.rtcChannelList.map((channel) {
print('渲染频道: ${channel.channelId}, ${channel.channelName}');
return LiveItemWidget(
channel: channel,
channelId: channel.channelId,
).onTap(() async{
await roomController.joinChannel(channel.channelId);
});
}).toList(),
),
);
}),
),
],
);
}
@override
bool get wantKeepAlive => true;
}

260
lib/pages/discover/discover_page.dart

@ -1,11 +1,11 @@
import 'package:dating_touchme_app/components/home_appbar.dart';
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/discover/live_room_page.dart';
import 'package:dating_touchme_app/pages/discover/dating_page.dart';
import 'package:dating_touchme_app/pages/discover/party_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class DiscoverPage extends StatefulWidget {
const DiscoverPage({super.key});
@ -15,28 +15,19 @@ class DiscoverPage extends StatefulWidget {
}
class _DiscoverPageState extends State<DiscoverPage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
with AutomaticKeepAliveClientMixin {
late final RoomController roomController;
late final PageController _pageController;
List<String> topNav = ["相亲", "聚会脱单"];
List<Map> liveList = [
{"isNew": true},
{"isNew": true},
{"isNew": false},
{"isNew": false},
{"isNew": false},
{"isNew": false},
{"isNew": false},
{"isNew": false},
{"isNew": false},
{"isNew": false},
];
int active = 0;
void changeNav(int active) {
print("当前项: $active");
setState(() {
this.active = active;
_pageController.jumpToPage(active);
});
}
@override
@ -47,6 +38,13 @@ class _DiscoverPageState extends State<DiscoverPage>
} else {
roomController = Get.put(RoomController());
}
_pageController = PageController(initialPage: 0);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
@ -68,6 +66,7 @@ class _DiscoverPageState extends State<DiscoverPage>
HomeAppbar(
topNav: topNav,
changeNav: changeNav,
activeIndex: active,
right: InkWell(
onTap: () async {
await roomController.createRtcChannel();
@ -92,40 +91,19 @@ class _DiscoverPageState extends State<DiscoverPage>
),
),
),
TDTabBar(
tabs: [
TDTab(child: Padding(padding: EdgeInsets.only(right: 16, left: 16), child: Text('全部'))),
TDTab(child: Padding(padding: EdgeInsets.only(right: 16, left: 16), child: Text('同城'))),
TDTab(child: Padding(padding: EdgeInsets.only(right: 12, left: 12), child: Text('相亲视频'))),
TDTab(child: Padding(padding: EdgeInsets.only(right: 12, left: 12), child: Text('相亲语音')))
],
// width: MediaQuery.of(context).size.width - 64,
backgroundColor: Colors.transparent,
labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4),
selectedBgColor: const Color.fromRGBO(108, 105, 244, 1),
unSelectedBgColor: Colors.transparent,
labelColor: Colors.white,
dividerHeight: 0,
tabAlignment: TabAlignment.start,
outlineType: TDTabBarOutlineType.capsule,
controller: TabController(length: 4, vsync: this),
showIndicator: false,
isScrollable: true,
onTap: (index){
print(index);
},
),
Expanded(
child: SingleChildScrollView(
child: Wrap(
spacing: 7.w,
runSpacing: 7.w,
children: [
...liveList.map((e) {
return LiveItem(item: e);
}),
],
),
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (index) {
setState(() {
active = index;
});
},
children: const [
DatingPage(),
PartyPage(),
],
),
),
],
@ -138,185 +116,3 @@ class _DiscoverPageState extends State<DiscoverPage>
@override
bool get wantKeepAlive => true;
}
class LiveItem extends StatefulWidget {
final Map item;
const LiveItem({super.key, required this.item});
@override
State<LiveItem> createState() => _LiveItemState();
}
class _LiveItemState extends State<LiveItem> {
late final RoomController roomController;
@override
void initState() {
super.initState();
if (Get.isRegistered<RoomController>()) {
roomController = Get.find<RoomController>();
} else {
roomController = Get.put(RoomController());
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async{
// Get.to(() => LiveRoomPage(id: 0));
await roomController.joinChannel('1190140590348701696');
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10.w)),
child: Stack(
children: [
Container(
width: 171.w,
height: 171.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.w)),
),
child: Image.network(
"https://picsum.photos/400",
width: 171.w,
height: 171.w,
),
),
Positioned(
top: 0,
left: 0,
child: Stack(
children: [
Image.asset(
Assets.imagesSubscript,
width: 56.w,
height: 16.w,
),
SizedBox(
height: 16.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 5.w),
Image.asset(
Assets.imagesLocationIcon,
width: 6.w,
height: 7.w,
),
SizedBox(width: 3.w),
Text(
"49.9km",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
if (widget.item["isNew"])
Positioned(
top: 9.w,
right: 8.w,
child: Container(
width: 39.w,
height: 13.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(13.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 5.w,
height: 5.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.w)),
color: const Color.fromRGBO(255, 209, 43, 1),
),
),
Text(
"等待",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Positioned(
left: 9.w,
bottom: 6.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64.w,
child: Text(
"一直一直在等你一直一直在等你......",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 2.w),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"福州 | 28岁",
style: TextStyle(
fontSize: 11.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 5.w),
if (widget.item["isNew"])
Container(
width: 32.w,
height: 10.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(10.w),
),
color: const Color.fromRGBO(255, 206, 28, .8),
),
child: Center(
child: Text(
"新人",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
],
),
),
],
),
),
);
}
}

219
lib/pages/discover/live_item_widget.dart

@ -0,0 +1,219 @@
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
///
class LiveItemWidget extends StatefulWidget {
final dynamic item;
final String? channelId;
final RtcChannelModel? channel;
const LiveItemWidget({
super.key,
this.item,
this.channelId,
this.channel,
});
@override
State<LiveItemWidget> createState() => _LiveItemWidgetState();
}
class _LiveItemWidgetState extends State<LiveItemWidget> {
late final RoomController roomController;
@override
void initState() {
super.initState();
if (Get.isRegistered<RoomController>()) {
roomController = Get.find<RoomController>();
} else {
roomController = Get.put(RoomController());
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async {
if (widget.channelId != null && widget.channelId!.isNotEmpty) {
await roomController.joinChannel(widget.channelId!);
} else if (widget.item is Map && widget.item['channelId'] != null) {
await roomController.joinChannel(widget.item['channelId'].toString());
}
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10.w)),
child: Stack(
children: [
Container(
width: 171.w,
height: 171.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.w)),
),
child: widget.channel != null &&
widget.channel!.channelPic.isNotEmpty
? Image.network(
widget.channel!.channelPic,
width: 171.w,
height: 171.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('图片加载失败: ${widget.channel!.channelPic}');
return Image.network(
"https://picsum.photos/400",
width: 171.w,
height: 171.w,
fit: BoxFit.cover,
);
},
)
: Image.network(
"https://picsum.photos/400",
width: 171.w,
height: 171.w,
fit: BoxFit.cover,
),
),
Positioned(
top: 0,
left: 0,
child: Stack(
children: [
Image.asset(
Assets.imagesSubscript,
width: 56.w,
height: 16.w,
),
SizedBox(
height: 16.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 5.w),
Image.asset(
Assets.imagesLocationIcon,
width: 6.w,
height: 7.w,
),
SizedBox(width: 3.w),
Text(
"49.9km",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
if (widget.item != null && widget.item is Map && widget.item["isNew"] == true)
Positioned(
top: 9.w,
right: 8.w,
child: Container(
width: 39.w,
height: 13.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(13.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 5.w,
height: 5.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.w)),
color: const Color.fromRGBO(255, 209, 43, 1),
),
),
Text(
"等待",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Positioned(
left: 9.w,
bottom: 6.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 64.w,
child: Text(
widget.channel != null && widget.channel!.channelName.isNotEmpty
? widget.channel!.channelName
: "一直一直在等你一直一直在等你......",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 2.w),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"福州 | 28岁",
style: TextStyle(
fontSize: 11.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 5.w),
if (widget.item != null && widget.item is Map && widget.item["isNew"] == true)
Container(
width: 32.w,
height: 10.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(10.w),
),
color: const Color.fromRGBO(255, 206, 28, .8),
),
child: Center(
child: Text(
"新人",
style: TextStyle(
fontSize: 8.w,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
],
),
),
],
),
),
);
}
}

130
lib/pages/discover/party_page.dart

@ -0,0 +1,130 @@
import 'package:dating_touchme_app/controller/discover/discover_controller.dart';
import 'package:dating_touchme_app/pages/discover/live_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
///
class PartyPage extends StatefulWidget {
const PartyPage({super.key});
@override
State<PartyPage> createState() => _PartyPageState();
}
class _PartyPageState extends State<PartyPage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
late final DiscoverController discoverController;
late final TabController _tabController;
@override
void initState() {
super.initState();
if (Get.isRegistered<DiscoverController>()) {
discoverController = Get.find<DiscoverController>();
} else {
discoverController = Get.put(DiscoverController());
}
_tabController = TabController(length: 4, vsync: this);
discoverController.loadRtcChannelPage();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Column(
children: [
TDTabBar(
tabs: [
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 16, left: 16),
child: Text('全部'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 16, left: 16),
child: Text('同城'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 12, left: 12),
child: Text('相亲视频'),
),
),
TDTab(
child: Padding(
padding: EdgeInsets.only(right: 12, left: 12),
child: Text('相亲语音'),
),
),
],
backgroundColor: Colors.transparent,
labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4),
selectedBgColor: const Color.fromRGBO(108, 105, 244, 1),
unSelectedBgColor: Colors.transparent,
labelColor: Colors.white,
dividerHeight: 0,
tabAlignment: TabAlignment.start,
outlineType: TDTabBarOutlineType.capsule,
controller: _tabController,
showIndicator: false,
isScrollable: true,
onTap: (index) {
print('聚会脱单页面 Tab: $index');
},
),
Expanded(
child: Obx(() {
if (discoverController.isLoading.value &&
discoverController.rtcChannelList.isEmpty) {
return Center(
child: CircularProgressIndicator(
color: const Color.fromRGBO(108, 105, 244, 1),
),
);
}
if (discoverController.rtcChannelList.isEmpty) {
return Center(
child: Text(
'暂无直播频道',
style: TextStyle(
fontSize: 14.w,
color: Colors.white.withOpacity(0.7),
),
),
);
}
return SingleChildScrollView(
child: Wrap(
spacing: 7.w,
runSpacing: 7.w,
children: [
...discoverController.rtcChannelList.map((channel) {
return LiveItemWidget(
channel: channel,
channelId: channel.channelId,
);
}),
],
),
);
}),
),
],
);
}
@override
bool get wantKeepAlive => true;
}

1
lib/rtc/rtc_manager.dart

@ -16,7 +16,6 @@ class RTCManager {
final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>(
<int>[],
);
NetworkService get _networkService => NetworkService();
RtcEngine? get engine => _engine;
bool get isInChannel => _isInChannel;
int? get currentUid => _currentUid;

12
lib/widget/live/live_room_anchor_showcase.dart

@ -128,7 +128,7 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
Widget _buildAnchorVideo(bool joined, int? remoteUid) {
final engine = _rtcManager.engine;
if (!joined || engine == null) {
if (_roomController.rtcChannelDetail.value?.anchorInfo == null || engine == null) {
return _buildWaitingPlaceholder();
}
print('joined: $joined');
@ -138,26 +138,24 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
child: SizedBox(
width: 177.w,
height: 175.w,
child: role == ClientRoleType.clientRoleBroadcaster
child: _roomController.currentRole == CurrentRole.broadcaster
? AgoraVideoView(
controller: VideoViewController(
rtcEngine: engine,
canvas: const VideoCanvas(uid: 0),
),
)
: (_rtcManager.currentChannelId == null && _rtcManager.remoteUserIds.isEmpty
? _buildWaitingPlaceholder()
: AgoraVideoView(
: AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: engine,
canvas: VideoCanvas(
uid: _rtcManager.remoteUserIds.first,
uid: _roomController.rtcChannelDetail.value?.anchorInfo?.uid,
),
connection: RtcConnection(
channelId: _rtcManager.currentChannelId!,
),
),
)),
),
),
);
}

Loading…
Cancel
Save