Browse Source
feat(discover): 实现相亲与聚会脱单直播页面
feat(discover): 实现相亲与聚会脱单直播页面
- 新增相亲页面(DatingPage)和聚会脱单页面(PartyPage) - 实现RTC频道分页列表接口(getRtcChannelPage) - 创建发现页控制器(DiscoverController)管理频道数据 - 添加直播项组件(LiveItemWidget)用于展示频道列表 - 更新主页发现页结构,使用PageView切换相亲与聚会页面 - 修改直播间主播展示逻辑,优化RTC用户信息处理 - 完善RTC Manager中的远程用户管理逻辑 - 重构房间控制器中的角色管理和用户信息同步逻辑ios
13 changed files with 750 additions and 306 deletions
Split View
Diff Options
-
19lib/components/home_appbar.dart
-
68lib/controller/discover/discover_controller.dart
-
124lib/controller/discover/room_controller.dart
-
34lib/model/discover/rtc_channel_model.dart
-
2lib/network/api_urls.dart
-
5lib/network/rtc_api.dart
-
40lib/network/rtc_api.g.dart
-
142lib/pages/discover/dating_page.dart
-
260lib/pages/discover/discover_page.dart
-
219lib/pages/discover/live_item_widget.dart
-
130lib/pages/discover/party_page.dart
-
1lib/rtc/rtc_manager.dart
-
12lib/widget/live/live_room_anchor_showcase.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(); |
|||
} |
|||
} |
|||
@ -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)'; |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
|
|||
@ -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, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
|
|||
Write
Preview
Loading…
Cancel
Save