You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1607 lines
57 KiB
1607 lines
57 KiB
import 'dart:convert';
|
|
|
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
import 'package:dating_touchme_app/controller/global.dart';
|
|
import 'package:dating_touchme_app/controller/overlay_controller.dart';
|
|
import 'package:dating_touchme_app/extension/ex_widget.dart';
|
|
import 'package:dating_touchme_app/generated/assets.dart';
|
|
import 'package:dating_touchme_app/im/im_manager.dart';
|
|
import 'package:dating_touchme_app/model/discover/task_data.dart';
|
|
import 'package:dating_touchme_app/model/discover/task_template_data.dart';
|
|
import 'package:dating_touchme_app/model/live/gift_product_model.dart';
|
|
import 'package:dating_touchme_app/model/mine/chat_static_data.dart';
|
|
import 'package:dating_touchme_app/model/rtc/link_mic_card_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/network_service.dart';
|
|
import 'package:dating_touchme_app/pages/mine/edit_info_page.dart';
|
|
import 'package:dating_touchme_app/pages/setting/match_league_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_screenutil/flutter_screenutil.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';
|
|
import '../../pages/setting/match_spread_page.dart';
|
|
import '../../widget/live/live_recharge_popup.dart';
|
|
import '../message/call_controller.dart';
|
|
import 'discover_controller.dart';
|
|
import 'svga_player_manager.dart';
|
|
|
|
// 当前角色
|
|
enum CurrentRole {
|
|
broadcaster, //主持
|
|
maleAudience, //男嘉宾
|
|
femaleAudience, //女嘉宾
|
|
audience, //观众
|
|
normalUser, //普通用户
|
|
}
|
|
|
|
/// 直播房间相关控制器
|
|
class RoomController extends GetxController with WidgetsBindingObserver {
|
|
RoomController({NetworkService? networkService})
|
|
: _networkService = networkService ?? Get.find<NetworkService>();
|
|
|
|
final NetworkService _networkService;
|
|
CurrentRole currentRole = CurrentRole.normalUser;
|
|
var isLive = false.obs;
|
|
var matchmakerFlag = false.obs;
|
|
|
|
/// 当前频道信息
|
|
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
|
|
final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>();
|
|
|
|
/// 聊天消息列表
|
|
final RxList<LiveChatMessage> chatMessages = <LiveChatMessage>[].obs;
|
|
|
|
/// 礼物产品列表
|
|
final RxList<GiftProductModel> giftProducts = <GiftProductModel>[].obs;
|
|
|
|
/// 连麦卡片信息
|
|
final Rxn<LinkMicCardModel> linkMicCard = Rxn<LinkMicCardModel>();
|
|
|
|
/// 玫瑰数量
|
|
final RxInt roseCount = 0.obs;
|
|
var isDialogShowing = false.obs;
|
|
|
|
/// 消息服务实例
|
|
final LiveChatMessageService _messageService =
|
|
LiveChatMessageService.instance;
|
|
|
|
// matchmakerFlag
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
matchmakerFlag.value = GlobalData().userData!.matchmakerFlag!;
|
|
// 注册消息监听
|
|
_registerMessageListener();
|
|
// 加载礼物产品列表
|
|
loadGiftProducts();
|
|
listRefreshController = EasyRefreshController(
|
|
controlFinishRefresh: true,
|
|
controlFinishLoad: true,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
// print('_handleAppResumed');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
super.onClose();
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
// 移除消息监听
|
|
_messageService.unregisterMessageListener();
|
|
// 清空RTM消息列表
|
|
chatMessages.clear();
|
|
}
|
|
|
|
final durationMins = 0.obs;
|
|
|
|
getDurationMins() async {
|
|
try {
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
final response = await _networkService.rtcApi.userGetRtcChannelAbstract(channelId: channelId ?? "");
|
|
if (response.data.isSuccess && response.data.data != null) {
|
|
durationMins.value = response.data.data?.durationMins ?? 0;
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e){
|
|
print('钱包数据获取失败: $e');
|
|
SmartDialog.showToast('钱包数据获取失败');
|
|
rethrow;
|
|
|
|
}
|
|
}
|
|
|
|
final todayLiveIncomeStr = "0.00".obs;
|
|
|
|
getLiveIncome() async {
|
|
try {
|
|
final response = await _networkService.rtcApi.userGetTodayLiveIncome();
|
|
if (response.data.isSuccess && response.data.data != null) {
|
|
todayLiveIncomeStr.value = response.data.data?.todayLiveIncomeStr ?? "0.00";
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e){
|
|
print('钱包数据获取失败: $e');
|
|
SmartDialog.showToast('钱包数据获取失败');
|
|
rethrow;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
final consumption = ChatStaticData(todayLiveDurationMins: 0).obs;
|
|
|
|
getLiveData() async {
|
|
try {
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
final response = await _networkService.userApi.getChatStaticsInfo();
|
|
if (response.data.isSuccess && response.data.data != null) {
|
|
consumption.value = response.data.data!;
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e){
|
|
print('钱包数据获取失败: $e');
|
|
SmartDialog.showToast('钱包数据获取失败');
|
|
rethrow;
|
|
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
update();
|
|
listRefreshController.finishRefresh(IndicatorResult.success);
|
|
listRefreshController.finishLoad(IndicatorResult.none);
|
|
}
|
|
|
|
final page = 1.obs;
|
|
|
|
final audienceList = <Records>[].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);
|
|
}
|
|
update();
|
|
} 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);
|
|
}
|
|
update();
|
|
} 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<Records> 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);
|
|
}
|
|
update();
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e) {
|
|
print('好友列表获取失败: $e');
|
|
SmartDialog.showToast('好友列表获取失败');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
final taskData = TaskData().obs;
|
|
|
|
getTaskData() async {
|
|
try{
|
|
final response = await _networkService.userApi.userGetUserTaskComplete(
|
|
taskType: 1,
|
|
);
|
|
if (response.data.isSuccess) {
|
|
if(response.data.data == null){
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
final data = response.data.data ?? TaskData();
|
|
taskData.value = data;
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e) {
|
|
print('任务列表获取失败: $e');
|
|
// SmartDialog.showToast('任务列表获取失败');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
final taskTemplateData = TaskTemplateData().obs;
|
|
|
|
getTaskTemplateData() async {
|
|
try{
|
|
final response = await _networkService.rtcApi.userGetTaskTemplateDetail(
|
|
id: taskData.value.taskTemplateId ?? "",
|
|
);
|
|
if (response.data.isSuccess) {
|
|
final data = response.data.data ?? TaskTemplateData();
|
|
taskTemplateData.value = data;
|
|
} else {
|
|
|
|
// 响应失败,抛出异常
|
|
throw Exception(response.data.message ?? '获取数据失败');
|
|
}
|
|
} catch(e) {
|
|
print('任务列表获取失败: $e');
|
|
// SmartDialog.showToast('任务列表获取失败');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void setDialogDismiss(bool flag){
|
|
isDialogShowing.value = flag;
|
|
}
|
|
|
|
/// 注册消息监听
|
|
void _registerMessageListener() {
|
|
_messageService.registerMessageListener(
|
|
onMessageReceived: (message) {
|
|
_addMessage(message);
|
|
},
|
|
onMessageError: (error) {
|
|
print('❌ 消息处理错误: $error');
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 添加消息到列表(带去重和数量限制)
|
|
void _addMessage(LiveChatMessage message) {
|
|
// 去重:检查是否已存在相同的消息(基于 userId + content + timestamp)
|
|
final exists = chatMessages.any(
|
|
(m) =>
|
|
m.userId == message.userId &&
|
|
m.content == message.content &&
|
|
(m.timestamp - message.timestamp).abs() < 1000,
|
|
); // 1秒内的相同消息视为重复
|
|
|
|
if (exists) {
|
|
print('⚠️ 消息已存在,跳过添加');
|
|
return;
|
|
}
|
|
|
|
chatMessages.add(message);
|
|
print('✅ 消息已添加到列表,当前消息数: ${chatMessages.length}');
|
|
|
|
// 限制消息数量,最多保留100条
|
|
if (chatMessages.length > 300) {
|
|
chatMessages.removeAt(0);
|
|
print('📝 消息列表已满,移除最旧的消息');
|
|
}
|
|
}
|
|
|
|
/// 调用接口创建 RTC 频道
|
|
Future<void> createRtcChannel() async {
|
|
if (isLive.value) {
|
|
return;
|
|
}
|
|
// 检查是否正在通话中(包括呼叫中)
|
|
try {
|
|
// 先尝试使用 Get.find 获取已注册的实例,如果不存在再使用 instance
|
|
CallController callController;
|
|
if (Get.isRegistered<CallController>()) {
|
|
callController = Get.find<CallController>();
|
|
} else {
|
|
callController = CallController.instance;
|
|
}
|
|
print('⚠️ [RoomController] 检查通话状态: currentCall=${callController.currentCall.value}, hashCode=${callController.hashCode}');
|
|
if (callController.currentCall.value != null) {
|
|
SmartDialog.showToast('请先结束通话');
|
|
print('⚠️ [RoomController] 当前正在通话中,无法开始直播');
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// 如果获取失败,说明没有正在进行的通话,继续执行
|
|
print('⚠️ [RoomController] 获取CallController失败(可能没有通话): $e');
|
|
}
|
|
|
|
final granted = await _ensureRtcPermissions();
|
|
if (!granted) return;
|
|
|
|
try {
|
|
final response = await _networkService.rtcApi.createRtcChannel();
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
rtcChannel.value = base.data;
|
|
currentRole = CurrentRole.broadcaster;
|
|
isLive.value = true;
|
|
await _joinRtcChannel(
|
|
base.data!.token,
|
|
base.data!.channelId,
|
|
base.data!.uid,
|
|
ClientRoleType.clientRoleBroadcaster,
|
|
);
|
|
} else {
|
|
final message = base.message.isNotEmpty ? base.message : '创建频道失败';
|
|
SmartDialog.showToast(message);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('创建频道异常:$e');
|
|
}
|
|
}
|
|
|
|
Future<void> joinChannel(String channelName) async {
|
|
// 检查是否正在通话中(包括呼叫中)
|
|
try {
|
|
// 直接使用 CallController.instance,因为它使用单例模式会自动注册
|
|
final callController = CallController.instance;
|
|
if (callController.currentCall.value != null) {
|
|
SmartDialog.showToast('请先结束通话');
|
|
print('⚠️ [RoomController] 当前正在通话中,无法加入直播间');
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// 忽略错误,继续处理加入频道逻辑
|
|
print('⚠️ [RoomController] 检查通话状态失败: $e');
|
|
}
|
|
|
|
final response = await _networkService.rtcApi.getSwRtcToken(channelName);
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
rtcChannel.value = base.data;
|
|
// 加入他人直播间时,当前用户默认作为普通观众,不在麦上
|
|
currentRole = CurrentRole.normalUser;
|
|
isLive.value = false;
|
|
await _joinRtcChannel(
|
|
base.data!.token,
|
|
channelName,
|
|
base.data!.uid,
|
|
ClientRoleType.clientRoleAudience,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _joinRtcChannel(
|
|
String token,
|
|
String channelName,
|
|
int uid,
|
|
ClientRoleType roleType,
|
|
) async {
|
|
try {
|
|
await _fetchRtcChannelDetail(channelName);
|
|
await RTCManager.instance.joinChannel(
|
|
token: token,
|
|
channelId: channelName,
|
|
uid: uid,
|
|
role: roleType,
|
|
);
|
|
} catch (e) {
|
|
SmartDialog.showToast('加入频道失败:$e');
|
|
}
|
|
}
|
|
|
|
Future<void> joinChat(CurrentRole role) async {
|
|
// 根据角色检查权限:男/女嘉宾需要摄像头和麦克风权限,普通观众只需要麦克风权限
|
|
bool granted = false;
|
|
if (role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience) {
|
|
// 男/女嘉宾上麦:需要摄像头和麦克风权限
|
|
granted = await _ensureRtcPermissions();
|
|
} else {
|
|
// 普通观众上麦:只需要麦克风权限
|
|
granted = await _ensureMicrophonePermission();
|
|
}
|
|
if (!granted) {
|
|
print('❌ [RoomController] 权限检查失败,无法上麦');
|
|
return;
|
|
}
|
|
final data = {
|
|
'channelId': RTCManager.instance.currentChannelId,
|
|
'seatNumber': role == CurrentRole.maleAudience ? 1 : 2,
|
|
'isMicrophoneOn': role != CurrentRole.normalUser ? true : false,
|
|
'isVideoOn':
|
|
role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience
|
|
? true
|
|
: false,
|
|
};
|
|
final response = await _networkService.rtcApi.connectRtcChannel(data);
|
|
if (!response.data.isSuccess) {
|
|
// 刷新 DiscoverController.loadRtcChannelPage
|
|
if (Get.isRegistered<DiscoverController>()) {
|
|
final discoverController = Get.find<DiscoverController>();
|
|
discoverController.loadRtcChannelPage();
|
|
}
|
|
SmartDialog.showToast(response.data.message);
|
|
return;
|
|
}
|
|
if (!response.data.data['success']) {
|
|
return;
|
|
}
|
|
currentRole = role;
|
|
if (role == CurrentRole.maleAudience ||
|
|
role == CurrentRole.femaleAudience) {
|
|
await RTCManager.instance.publishVideo(role);
|
|
} else {
|
|
await RTCManager.instance.publishAudio();
|
|
}
|
|
// 上麦成功后刷新连麦卡片数据
|
|
await getUserPropLinkMicCard();
|
|
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,
|
|
);
|
|
final newDetail = RtcChannelDetail(
|
|
channelId: rtcChannelDetail.value!.channelId,
|
|
anchorInfo: rtcChannelDetail.value!.anchorInfo,
|
|
maleInfo: role == CurrentRole.maleAudience
|
|
? userInfo
|
|
: rtcChannelDetail.value?.maleInfo,
|
|
femaleInfo: role == CurrentRole.femaleAudience
|
|
? userInfo
|
|
: rtcChannelDetail.value?.femaleInfo,
|
|
);
|
|
rtcChannelDetail.value = newDetail;
|
|
isLive.value = true;
|
|
}
|
|
|
|
Future<void> leaveChat() async {
|
|
final data = {'channelId': RTCManager.instance.currentChannelId};
|
|
final response = await _networkService.rtcApi.disconnectRtcChannel(data);
|
|
if (response.data.isSuccess) {
|
|
isLive.value = false;
|
|
await RTCManager.instance.unpublish(currentRole);
|
|
if (currentRole == CurrentRole.maleAudience) {
|
|
final newDetail = RtcChannelDetail(
|
|
channelId: rtcChannelDetail.value!.channelId,
|
|
anchorInfo: rtcChannelDetail.value!.anchorInfo,
|
|
maleInfo: null,
|
|
femaleInfo: rtcChannelDetail.value!.femaleInfo,
|
|
);
|
|
rtcChannelDetail.value = newDetail;
|
|
} else if (currentRole == CurrentRole.femaleAudience) {
|
|
final newDetail = RtcChannelDetail(
|
|
channelId: rtcChannelDetail.value!.channelId,
|
|
anchorInfo: rtcChannelDetail.value!.anchorInfo,
|
|
maleInfo: rtcChannelDetail.value!.maleInfo,
|
|
femaleInfo: null,
|
|
);
|
|
rtcChannelDetail.value = newDetail;
|
|
}
|
|
currentRole = CurrentRole.normalUser;
|
|
}
|
|
}
|
|
|
|
/// 获取 RTC 频道详情(公有方法,供外部调用)
|
|
Future<void> fetchRtcChannelDetail(String channelName) async {
|
|
try {
|
|
final response = await _networkService.rtcApi.getRtcChannelDetail(
|
|
channelName,
|
|
);
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
rtcChannelDetail.value = base.data;
|
|
}
|
|
} catch (e) {
|
|
print('获取 RTC 频道详情失败:$e');
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchRtcChannelDetail(String channelName) async {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
|
|
/// 发送公屏消息
|
|
Future<void> sendChatMessage(String content) async {
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
|
|
final result = await _messageService.sendMessage(
|
|
content: content,
|
|
channelName: channelName,
|
|
);
|
|
|
|
// 如果发送成功,立即添加到本地列表(优化体验,避免等待 RTM 回调)
|
|
if (result.success && result.message != null) {
|
|
_addMessage(result.message!);
|
|
}
|
|
}
|
|
|
|
Future<bool> _ensureRtcPermissions() async {
|
|
final statuses = await [Permission.camera, Permission.microphone].request();
|
|
final allGranted = statuses.values.every((status) => status.isGranted);
|
|
if (allGranted) {
|
|
return true;
|
|
}
|
|
|
|
final permanentlyDenied = statuses.values.any(
|
|
(status) => status.isPermanentlyDenied,
|
|
);
|
|
if (permanentlyDenied) {
|
|
SmartDialog.showToast('请在系统设置中开启摄像头和麦克风权限');
|
|
await openAppSettings();
|
|
} else {
|
|
SmartDialog.showToast('请允许摄像头和麦克风权限以进入房间');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// 检查麦克风权限(用于普通观众上麦)
|
|
Future<bool> _ensureMicrophonePermission() async {
|
|
final micStatus = await Permission.microphone.request();
|
|
if (micStatus.isGranted) {
|
|
return true;
|
|
}
|
|
|
|
if (micStatus.isPermanentlyDenied) {
|
|
SmartDialog.showToast('请在系统设置中开启麦克风权限');
|
|
await openAppSettings();
|
|
} else {
|
|
SmartDialog.showToast('请允许麦克风权限以进入房间');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Future<void> leaveChannel() async {
|
|
if (currentRole == CurrentRole.broadcaster) {
|
|
try {
|
|
// 先调用销毁 RTC 频道 API
|
|
final destroyResponse = await _networkService.rtcApi.destroyRtcChannel();
|
|
if (destroyResponse.data.isSuccess) {
|
|
// 然后发送结束直播消息
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
if (channelId != null && channelId.isNotEmpty) {
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelId,
|
|
message: json.encode({'type': 'end_live'}),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ 销毁 RTC 频道异常: $e');
|
|
}
|
|
} else if (currentRole == CurrentRole.maleAudience || currentRole == CurrentRole.femaleAudience) {
|
|
try {
|
|
// 嘉宾离开时调用断开 RTC 频道连接接口
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
if (channelId != null && channelId.isNotEmpty) {
|
|
final data = {'channelId': channelId};
|
|
final response = await _networkService.rtcApi.disconnectRtcChannel(data);
|
|
if (response.data.isSuccess) {
|
|
print('✅ [RoomController] 嘉宾已断开 RTC 频道连接,channelId: $channelId');
|
|
} else {
|
|
print('⚠️ [RoomController] 断开 RTC 频道连接失败: ${response.data.message}');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ [RoomController] 断开 RTC 频道连接异常: $e');
|
|
}
|
|
}
|
|
|
|
isLive.value = false;
|
|
if (currentRole == CurrentRole.maleAudience ||
|
|
currentRole == CurrentRole.femaleAudience) {
|
|
await RTCManager.instance.unpublish(currentRole);
|
|
}
|
|
currentRole = CurrentRole.normalUser;
|
|
await RTCManager.instance.leaveChannel();
|
|
// 清空RTM消息列表
|
|
chatMessages.clear();
|
|
}
|
|
|
|
/// 加载礼物产品列表
|
|
Future<void> loadGiftProducts() async {
|
|
try {
|
|
final response = await _networkService.rtcApi.listGiftProduct();
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
giftProducts.assignAll(base.data!);
|
|
print('✅ 礼物产品列表加载成功,共 ${giftProducts.length} 个');
|
|
} else {
|
|
print('❌ 加载礼物产品列表失败: ${base.message}');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 加载礼物产品列表异常: $e');
|
|
}
|
|
}
|
|
|
|
/// 赠送礼物
|
|
Future<void> sendGift({
|
|
required GiftProductModel gift,
|
|
required int targetUserId,
|
|
required int type,
|
|
}) async {
|
|
try {
|
|
// 先调用消费接口
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
if (channelId == null || channelId.isEmpty) {
|
|
SmartDialog.showToast('频道ID不存在');
|
|
return;
|
|
}
|
|
|
|
// 准备请求参数
|
|
final requestData = {
|
|
'channelId': int.tryParse(channelId) ?? 0,
|
|
'type': type, // 1.送礼 2.添加好友
|
|
'toUId': targetUserId,
|
|
'productSpecId': int.tryParse(gift.productSpecId) ?? 0,
|
|
'quantity': 1,
|
|
};
|
|
|
|
// 调用消费接口
|
|
final response = await _networkService.rtcApi.costChannelGift(
|
|
requestData,
|
|
);
|
|
|
|
if (!response.data.isSuccess) {
|
|
SmartDialog.showToast(
|
|
response.data.message.isNotEmpty ? response.data.message : '发送礼物失败',
|
|
);
|
|
return;
|
|
}
|
|
if (response.data.isSuccess && !response.data.data['success']) {
|
|
final code = response.data.data['code'];
|
|
if (code == 'E0002') {
|
|
// 玫瑰不足,显示 toast 并弹出充值弹框
|
|
SmartDialog.showToast('玫瑰不足请充值');
|
|
Get.log('❌ 送礼失败: ${response.data.data}');
|
|
// 使用 addPostFrameCallback 确保在下一帧显示弹框,避免与 toast 冲突
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
setDialogDismiss(true);
|
|
SmartDialog.show(
|
|
alignment: Alignment.bottomCenter,
|
|
onDismiss: (){
|
|
setDialogDismiss(false);
|
|
},
|
|
maskColor: Colors.black.withOpacity(0.5),
|
|
builder: (_) => const LiveRechargePopup(),
|
|
);
|
|
});
|
|
} else {
|
|
SmartDialog.showToast('发送礼物失败');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 消费成功后刷新玫瑰数量
|
|
await getVirtualAccount();
|
|
|
|
// 消费成功后再添加到本地播放队列
|
|
final svgaManager = SvgaPlayerManager.instance;
|
|
svgaManager.addToQueue(
|
|
SvgaAnimationItem(
|
|
svgaFile: gift.svgaFile,
|
|
targetUserId: targetUserId,
|
|
senderUserId: rtcChannel.value?.uid,
|
|
giftProductId: gift.productId,
|
|
),
|
|
);
|
|
print('✅ 礼物已添加到播放队列: ${gift.productTitle}');
|
|
|
|
// 在公屏显示赠送礼物消息
|
|
final senderNickName = GlobalData().userData?.nickName ?? '用户';
|
|
String targetNickName = '用户';
|
|
|
|
// 从频道详情中查找目标用户昵称
|
|
final channelDetail = rtcChannelDetail.value;
|
|
if (channelDetail != null) {
|
|
// 检查是否是主持人
|
|
final anchorInfo = channelDetail.anchorInfo;
|
|
if (anchorInfo != null && anchorInfo.uid == targetUserId) {
|
|
targetNickName = anchorInfo.nickName.isNotEmpty
|
|
? anchorInfo.nickName
|
|
: '用户';
|
|
}
|
|
// 检查是否是男嘉宾
|
|
else {
|
|
final maleInfo = channelDetail.maleInfo;
|
|
if (maleInfo != null && maleInfo.uid == targetUserId) {
|
|
targetNickName = maleInfo.nickName.isNotEmpty
|
|
? maleInfo.nickName
|
|
: '用户';
|
|
}
|
|
// 检查是否是女嘉宾
|
|
else {
|
|
final femaleInfo = channelDetail.femaleInfo;
|
|
if (femaleInfo != null && femaleInfo.uid == targetUserId) {
|
|
targetNickName = femaleInfo.nickName.isNotEmpty
|
|
? femaleInfo.nickName
|
|
: '用户';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 发送 RTM 消息通知其他用户(包含目标用户昵称)
|
|
final messageData = {
|
|
'type': 'gift',
|
|
'svgaFile': gift.svgaFile,
|
|
'giftProductId': gift.productId,
|
|
'targetUserId': targetUserId,
|
|
'targetNickName': targetNickName, // 包含目标用户昵称
|
|
'senderUserId': rtcChannel.value?.uid,
|
|
'senderNickName': senderNickName,
|
|
'senderAvatar': GlobalData().userData?.profilePhoto ?? '',
|
|
};
|
|
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelId,
|
|
message: json.encode(messageData),
|
|
);
|
|
print('✅ 礼物消息已发送: ${gift.productTitle}');
|
|
|
|
// 创建公屏消息
|
|
final giftMessage = LiveChatMessage(
|
|
userId: GlobalData().userId ?? GlobalData().userData?.id ?? '',
|
|
userName: senderNickName,
|
|
avatar: GlobalData().userData?.profilePhoto,
|
|
content: '向$targetNickName赠送了【${gift.productTitle}】',
|
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
|
|
// 添加到消息列表
|
|
_addMessage(giftMessage);
|
|
} catch (e) {
|
|
print('❌ 发送礼物失败: $e');
|
|
SmartDialog.showToast('发送礼物失败');
|
|
}
|
|
}
|
|
|
|
/// 接收RTC消息
|
|
Future<void> receiveRTCMessage(Map<String, dynamic> message) async {
|
|
if (message['type'] == 'gift') {
|
|
// 处理礼物消息
|
|
try {
|
|
final svgaFile = message['svgaFile']?.toString() ?? '';
|
|
final giftProductId = message['giftProductId']?.toString();
|
|
final targetUserId = message['targetUserId'];
|
|
final targetNickNameFromMessage = message['targetNickName']?.toString();
|
|
final senderUserId = message['senderUserId'];
|
|
final senderNickName = message['senderNickName']?.toString() ?? '用户';
|
|
final senderAvatar = message['senderAvatar']?.toString();
|
|
|
|
// 从礼物产品列表中查找礼物名称
|
|
String giftTitle = '礼物';
|
|
if (giftProductId != null && giftProductId.isNotEmpty) {
|
|
try {
|
|
final gift = giftProducts.firstWhere(
|
|
(g) => g.productId == giftProductId,
|
|
);
|
|
giftTitle = gift.productTitle;
|
|
} catch (e) {
|
|
// 如果找不到对应的礼物,使用默认名称
|
|
print('⚠️ 未找到礼物ID: $giftProductId');
|
|
}
|
|
}
|
|
|
|
// 获取目标用户昵称(优先使用消息中的昵称,如果不存在则从频道详情中查找)
|
|
String targetNickName = targetNickNameFromMessage ?? '用户';
|
|
if (targetNickName == '用户' || targetNickName.isEmpty) {
|
|
final channelDetail = rtcChannelDetail.value;
|
|
if (channelDetail != null) {
|
|
final targetUserIdStr = targetUserId?.toString() ?? '';
|
|
final targetUid = targetUserId is int
|
|
? targetUserId
|
|
: (int.tryParse(targetUserIdStr) ?? 0);
|
|
|
|
// 检查是否是主持人(同时检查 uid 和 userId)
|
|
final anchorInfo = channelDetail.anchorInfo;
|
|
if (anchorInfo != null &&
|
|
(anchorInfo.uid == targetUid || anchorInfo.userId == targetUserIdStr)) {
|
|
targetNickName = anchorInfo.nickName.isNotEmpty
|
|
? anchorInfo.nickName
|
|
: '用户';
|
|
}
|
|
// 检查是否是男嘉宾(同时检查 uid 和 userId)
|
|
else {
|
|
final maleInfo = channelDetail.maleInfo;
|
|
if (maleInfo != null &&
|
|
(maleInfo.uid == targetUid || maleInfo.userId == targetUserIdStr)) {
|
|
targetNickName = maleInfo.nickName.isNotEmpty
|
|
? maleInfo.nickName
|
|
: '用户';
|
|
}
|
|
// 检查是否是女嘉宾(同时检查 uid 和 userId)
|
|
else {
|
|
final femaleInfo = channelDetail.femaleInfo;
|
|
if (femaleInfo != null &&
|
|
(femaleInfo.uid == targetUid || femaleInfo.userId == targetUserIdStr)) {
|
|
targetNickName = femaleInfo.nickName.isNotEmpty
|
|
? femaleInfo.nickName
|
|
: '用户';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 在公屏显示赠送礼物消息
|
|
final giftMessage = LiveChatMessage(
|
|
userId: senderUserId?.toString() ?? '',
|
|
userName: senderNickName,
|
|
avatar: senderAvatar?.isNotEmpty == true ? senderAvatar : null,
|
|
content: '向$targetNickName赠送了【$giftTitle】',
|
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
_addMessage(giftMessage);
|
|
|
|
if (svgaFile.isNotEmpty) {
|
|
// 添加到播放队列
|
|
final svgaManager = SvgaPlayerManager.instance;
|
|
svgaManager.addToQueue(
|
|
SvgaAnimationItem(
|
|
svgaFile: svgaFile,
|
|
targetUserId: targetUserId,
|
|
senderUserId: senderUserId is int
|
|
? senderUserId
|
|
: (int.tryParse(senderUserId?.toString() ?? '') ?? 0),
|
|
giftProductId: giftProductId,
|
|
),
|
|
);
|
|
print('✅ 收到礼物消息,已添加到播放队列: $senderNickName 赠送了礼物');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 处理礼物消息失败: $e');
|
|
}
|
|
} else if (message['type'] == 'kick_user') {
|
|
// 处理踢人消息
|
|
try {
|
|
final kickingUId = message['kickingUId'];
|
|
final operatorName = message['operatorName']?.toString() ?? '主持人';
|
|
|
|
print('✅ 收到踢人消息: 被踢用户ID $kickingUId');
|
|
|
|
// 判断当前用户是否是被踢的用户
|
|
if (rtcChannel.value?.uid == kickingUId) {
|
|
// 被踢用户:关闭小窗口
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
try {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.hide(); // 隐藏直播房间小窗
|
|
} catch (e) {
|
|
print('关闭小窗口失败: $e');
|
|
}
|
|
}
|
|
// 被踢用户:离开房间
|
|
await leaveChannel();
|
|
// 跳转到结束直播页面,并传入被踢出标识
|
|
Get.off(
|
|
() => LiveEndPage(isKickedOut: true, operatorName: operatorName),
|
|
);
|
|
} else {
|
|
// 其他用户:刷新房间详情
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
print('✅ 其他用户收到踢人消息,已刷新房间详情');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 处理踢人消息失败: $e');
|
|
}
|
|
} else if(message['type'] == 'invite_to_mic'){
|
|
// 处理踢人消息+
|
|
try {
|
|
final userId = message['userId'];
|
|
final operatorName = message['operatorName']?.toString() ?? '主持人';
|
|
|
|
print('✅ 收到邀请消息: 被邀请用户ID $userId');
|
|
|
|
// 判断当前用户是否是被踢的用户
|
|
if (userId.contains(GlobalData().userId ?? '')) {
|
|
if(rtcChannelDetail.value?.maleInfo == null && GlobalData().userData?.genderCode == 0 && currentRole != CurrentRole.broadcaster ||
|
|
rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 1 && currentRole != CurrentRole.broadcaster ||
|
|
isLive.value && currentRole != CurrentRole.broadcaster && !isLive.value){
|
|
setDialogDismiss(true);
|
|
SmartDialog.show(
|
|
alignment: Alignment.center,
|
|
maskColor: Colors.black.withOpacity(0.5),
|
|
onDismiss: () {
|
|
setDialogDismiss(false);
|
|
},
|
|
builder: (context) {
|
|
// return LiveRoomGuestListDialog(
|
|
// initialTab: isMaleSeat ? 1 : 0, // 0: 女嘉宾, 1: 男嘉宾
|
|
// );
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.all(Radius.circular(16.w)),
|
|
child: Material(
|
|
child: Stack(
|
|
|
|
children: [
|
|
Container(
|
|
width: 311.w,
|
|
height: 210.w,
|
|
color: Colors.white,
|
|
padding: EdgeInsets.only(
|
|
top: 53.w,
|
|
left: 23.w,
|
|
right: 23.w,
|
|
bottom: 20.w
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
"主持人邀请您视频连麦",
|
|
style: TextStyle(
|
|
fontSize: 21.w,
|
|
color: const Color.fromRGBO(117, 98, 249, 1),
|
|
fontWeight: FontWeight.w500
|
|
),
|
|
),
|
|
SizedBox(height: 15.w,),
|
|
if(GlobalData().userData?.genderCode == 0) Text(
|
|
"有相亲卡的用户免费",
|
|
style: TextStyle(
|
|
fontSize: 12.w,
|
|
color: const Color.fromRGBO(87, 87, 87, 1),
|
|
fontWeight: FontWeight.w500
|
|
),
|
|
),
|
|
SizedBox(height: 28.w,),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
width: 128.w,
|
|
height: 40.w,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
|
color: const Color.fromRGBO(237, 237, 237, 1)
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Image.asset(
|
|
Assets.imagesHangUpIcon,
|
|
width: 26.w,
|
|
height: 26.w,
|
|
),
|
|
SizedBox(width: 5.w,),
|
|
Text(
|
|
"拒绝",
|
|
style: TextStyle(
|
|
fontSize: 15.w
|
|
),
|
|
)
|
|
],
|
|
),
|
|
).onTap((){
|
|
// 隐藏键盘
|
|
FocusScope.of(context).unfocus();
|
|
// 隐藏 overlay
|
|
SmartDialog.dismiss();
|
|
setDialogDismiss(false);
|
|
}),
|
|
Container(
|
|
width: 128.w,
|
|
height: 40.w,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
|
color: const Color.fromRGBO(117, 98, 249, 1)
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Image.asset(
|
|
Assets.imagesAnswerIcon,
|
|
width: 18.w,
|
|
height: 13.w,
|
|
),
|
|
SizedBox(width: 5.w,),
|
|
Text(
|
|
"接受",
|
|
style: TextStyle(
|
|
fontSize: 15.w,
|
|
color: Colors.white
|
|
),
|
|
)
|
|
],
|
|
),
|
|
).onTap(() async {
|
|
// 隐藏键盘
|
|
FocusScope.of(context).unfocus();
|
|
// 隐藏 overlay
|
|
SmartDialog.dismiss();
|
|
setDialogDismiss(false);
|
|
|
|
// 检查是否需要弹出充值弹框
|
|
if (!isLive.value) {
|
|
final userData = GlobalData().userData;
|
|
final isMale = userData?.genderCode == 0;
|
|
if (isMale) {
|
|
final cardNum = linkMicCard.value?.num ?? 0;
|
|
// 如果显示"上麦20玫瑰"且玫瑰数量小于20,弹出充值弹框
|
|
if (cardNum == 0 && roseCount.value < 20) {
|
|
setDialogDismiss(true);
|
|
SmartDialog.show(
|
|
alignment: Alignment.bottomCenter,
|
|
maskColor: Colors.black.withOpacity(0.5),
|
|
onDismiss: (){
|
|
setDialogDismiss(false);
|
|
},
|
|
builder: (_) => const LiveRechargePopup(),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(isLive.value){
|
|
await leaveChat();
|
|
}else{
|
|
await joinChat(GlobalData().userData?.genderCode == 0 ? CurrentRole.maleAudience : CurrentRole.femaleAudience);
|
|
}
|
|
}),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 15.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(16.w),
|
|
bottomRight: Radius.circular(16.w),
|
|
),
|
|
color: const Color.fromRGBO(117, 98, 249, 1)
|
|
),
|
|
child: Text(
|
|
GlobalData().userData?.genderCode == 0 ? "20玫瑰" : "免费",
|
|
style: TextStyle(
|
|
fontSize: 14.w,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w500
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
} else {
|
|
// 其他用户:刷新房间详情
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
print('✅ 其他用户收到邀请消息,已刷新房间详情');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 处理邀请消息失败: $e');
|
|
}
|
|
|
|
} else if(message['type'] == 'kick_more_user'){
|
|
// 处理踢人消息+
|
|
try {
|
|
final userId = message['userId'].map((e) => int.parse(e)).toList();
|
|
final operatorName = message['operatorName']?.toString() ?? '主持人';
|
|
|
|
print('✅ 收到踢人消息: 被踢人用户ID $userId');
|
|
|
|
// 判断当前用户是否是被踢的用户
|
|
if (userId.contains(rtcChannel.value?.uid ?? '')) {
|
|
|
|
// 被踢用户:关闭小窗口
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
try {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.hide(); // 隐藏直播房间小窗
|
|
} catch (e) {
|
|
print('关闭小窗口失败: $e');
|
|
}
|
|
}
|
|
// 被踢用户:离开房间
|
|
await leaveChannel();
|
|
// 跳转到结束直播页面,并传入被踢出标识
|
|
Get.off(
|
|
() => LiveEndPage(isKickedOut: true, operatorName: operatorName),
|
|
);
|
|
|
|
} else {
|
|
// 其他用户:刷新房间详情
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
print('✅ 其他用户收到踢人消息,已刷新房间详情');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 处理踢人消息失败: $e');
|
|
}
|
|
|
|
} else if (message['type'] == 'end_mic') {
|
|
// 处理踢人消息
|
|
try {
|
|
final kickingUId = message['kickingUId'];
|
|
final operatorName = message['operatorName']?.toString() ?? '主持人';
|
|
|
|
print('✅ 收到下麦消息: 被下麦用户ID $kickingUId');
|
|
|
|
// 判断当前用户是否是被踢的用户
|
|
if (rtcChannel.value?.uid == kickingUId) {
|
|
|
|
await leaveChat();
|
|
} else {
|
|
// 其他用户:刷新房间详情
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
print('✅ 其他用户收到下麦消息,已刷新房间详情');
|
|
}
|
|
} catch (e) {
|
|
print('❌ 处理下麦消息失败: $e');
|
|
}
|
|
} else if (message['type'] == 'close_mic') {
|
|
// 处理踢人消息
|
|
try {
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
print('✅ 其他用户收到下麦消息,已刷新房间详情');
|
|
} catch (e) {
|
|
print('❌ 处理下麦消息失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void registerMatch(int type) async {
|
|
if (GlobalData().userData!.profilePhoto == null || GlobalData().userData!.profilePhoto!.isEmpty) {
|
|
SmartDialog.showToast('请先上传头像');
|
|
await Get.to(() => EditInfoPage());
|
|
return;
|
|
}
|
|
if (GlobalData().userData!.auditProfilePhoto != null) {
|
|
showGeneralDialog(
|
|
context: Get.context!,
|
|
pageBuilder: (BuildContext buildContext, Animation<double> animation,
|
|
Animation<double> secondaryAnimation) {
|
|
return TDConfirmDialog(
|
|
title: '温馨提示',
|
|
content: '当前头像正在审核中,请稍候,如果长时间没有审核结果,请联系客服。',
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
if(GlobalData().userData!.identityCard == null || GlobalData().userData!.identityCard!.isEmpty){
|
|
SmartDialog.showToast('请先进行实名认证');
|
|
await Get.to(() => RealNamePage(type: 1));
|
|
return;
|
|
}
|
|
if(type == 1){
|
|
Get.to(() => MatchLeaguePage());
|
|
} else {
|
|
await createRtcChannel();
|
|
}
|
|
}
|
|
|
|
|
|
int calculateAge(String birthdayStr) {
|
|
final birthday = DateTime.parse(birthdayStr); // 自动识别 1996-1-20
|
|
final today = DateTime.now();
|
|
|
|
int age = today.year - birthday.year;
|
|
|
|
// 如果今年生日还没过,年龄要减 1
|
|
if (today.month < birthday.month ||
|
|
(today.month == birthday.month && today.day < birthday.day)) {
|
|
age--;
|
|
}
|
|
|
|
return age;
|
|
}
|
|
|
|
changeMic(bool mic) async {
|
|
try {
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
var res = await _networkService.rtcApi.enableRtcChannelUserAudio(
|
|
{
|
|
"channelId": channelId,
|
|
"isMicrophoneOn": mic,
|
|
"isVideoOn": true
|
|
}
|
|
);
|
|
if(res.data.isSuccess){
|
|
final messageData = {
|
|
'type': 'close_mic',
|
|
'operatorId': GlobalData().userData?.id ?? '',
|
|
'operatorName': GlobalData().userData?.nickName ?? '',
|
|
};
|
|
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelName,
|
|
message: json.encode(messageData),
|
|
);
|
|
print("关闭麦克风成功");
|
|
}
|
|
|
|
}
|
|
} catch(e){
|
|
|
|
print("关闭麦克风失败");
|
|
}
|
|
}
|
|
|
|
inviteMic(List<Map<String, dynamic>> selectUserId) async {
|
|
try {
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
|
|
final idIsNull = selectUserId
|
|
.where((e) => e["uid"] == 0)
|
|
.toList();
|
|
final idNotNull = selectUserId
|
|
.where((e) => e["uid"] != 0)
|
|
.toList();
|
|
final userIds = idNotNull.map((e) => e["userId"]).toList();
|
|
final messageData = {
|
|
'type': 'invite_to_mic',
|
|
'userId': userIds,
|
|
'operatorId': GlobalData().userData?.id ?? '',
|
|
'operatorName': GlobalData().userData?.nickName ?? '',
|
|
};
|
|
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelName,
|
|
message: json.encode(messageData),
|
|
);
|
|
|
|
final others = idIsNull.map((e) => e["userId"]).toList();
|
|
|
|
final channelId = RTCManager.instance.currentChannelId;
|
|
if (channelId == null || channelId.isEmpty) {
|
|
SmartDialog.showToast('频道ID不存在');
|
|
return;
|
|
}
|
|
|
|
// 获取房间详情
|
|
final channelDetail = rtcChannelDetail.value;
|
|
final anchorName = channelDetail?.anchorInfo?.nickName ?? '主持人';
|
|
final anchorAvatar = channelDetail?.anchorInfo?.profilePhoto ?? '';
|
|
|
|
// 确保头像和封面URL不为空且格式正确
|
|
final cleanedAvatar = anchorAvatar.trim().replaceAll('`', '');
|
|
final cleanedCover = cleanedAvatar; // 封面使用主持人头像
|
|
|
|
// 构建消息参数,包含房间信息(不需要 type 字段,因为 event 已经是 'live_room_invite')
|
|
final messageParams = <String, String>{
|
|
'channelId': channelId,
|
|
'anchorAvatar': cleanedAvatar,
|
|
'anchorName': anchorName,
|
|
'coverImage': cleanedCover, // 封面使用主持人头像
|
|
'genderCode': GlobalData().userData?.genderCode.toString() ?? "",
|
|
"age": calculateAge(
|
|
(GlobalData().userData?.birthDate != null && GlobalData().userData!.birthDate!.isNotEmpty) ?
|
|
(GlobalData().userData?.birthDate ?? "") : GlobalData().userData?.birthYear != null && GlobalData().userData!.birthYear!.isNotEmpty ?
|
|
"${GlobalData().userData?.birthYear}-01-01" : "").toString(),
|
|
"provinceName": GlobalData().userData?.provinceName ?? "",
|
|
};
|
|
|
|
print('📤 [LiveRoomGuestListDialog] 发送房间邀请消息: anchorAvatar=$cleanedAvatar, coverImage=$cleanedCover');
|
|
|
|
others.forEach((e) async {
|
|
// 发送自定义消息
|
|
final result = await IMManager.instance.sendCustomMessage(
|
|
e,
|
|
'live_room_invite',
|
|
messageParams,
|
|
);
|
|
|
|
if (result != null) {
|
|
SmartDialog.showToast('邀请已发送');
|
|
SmartDialog.dismiss();
|
|
} else {
|
|
SmartDialog.showToast('邀请发送失败,id$e');
|
|
}
|
|
});
|
|
|
|
|
|
|
|
print('✅ 邀请消息已发送: 邀请用户ID $selectUserId');
|
|
|
|
// 刷新频道详情
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
} catch (e){
|
|
print('❌ 邀请用户异常: $e');
|
|
SmartDialog.showToast('邀请用户失败');
|
|
}
|
|
}
|
|
|
|
kickUser(List<String> selectUserId) async {
|
|
try {
|
|
final futures = selectUserId.map((e){
|
|
|
|
final requestData = {'channelId': rtcChannelDetail.value!.channelId, 'kickingUId': e};
|
|
return _networkService.rtcApi.kickingRtcChannelUser(
|
|
requestData,
|
|
);
|
|
});
|
|
Future.wait(futures).then((e) async {
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
final messageData = {
|
|
'type': 'kick_more_user',
|
|
'userId': selectUserId,
|
|
'operatorId': GlobalData().userData?.id ?? '',
|
|
'operatorName': GlobalData().userData?.nickName ?? '',
|
|
};
|
|
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelName,
|
|
message: json.encode(messageData),
|
|
);
|
|
print('✅ 邀请消息已发送: 邀请用户ID $selectUserId');
|
|
|
|
// 刷新频道详情
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
});
|
|
|
|
|
|
} catch (e){
|
|
print('❌ 邀请用户异常: $e');
|
|
SmartDialog.showToast('邀请用户失败');
|
|
}
|
|
}
|
|
|
|
/// 踢出 RTC 频道用户
|
|
Future<void> kickingRtcChannelUser({
|
|
required String channelId,
|
|
required int kickingUId,
|
|
}) async {
|
|
try {
|
|
final requestData = {'channelId': channelId, 'kickingUId': kickingUId};
|
|
|
|
final response = await _networkService.rtcApi.kickingRtcChannelUser(
|
|
requestData,
|
|
);
|
|
|
|
if (response.data.isSuccess) {
|
|
SmartDialog.showToast('已踢出用户');
|
|
|
|
// 发送 RTM 消息通知所有用户
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
final messageData = {
|
|
'type': 'kick_user',
|
|
'kickingUId': kickingUId,
|
|
'operatorId': GlobalData().userData?.id ?? '',
|
|
'operatorName': GlobalData().userData?.nickName ?? '',
|
|
};
|
|
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelName,
|
|
message: json.encode(messageData),
|
|
);
|
|
print('✅ 踢人消息已发送: 踢出用户ID $kickingUId');
|
|
|
|
// 刷新频道详情
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
} else {
|
|
final message = response.data.message.isNotEmpty
|
|
? response.data.message
|
|
: '踢出用户失败';
|
|
SmartDialog.showToast(message);
|
|
}
|
|
} catch (e) {
|
|
print('❌ 踢出用户异常: $e');
|
|
SmartDialog.showToast('踢出用户失败');
|
|
}
|
|
}
|
|
|
|
/// 踢出 RTC 频道用户
|
|
Future<void> endMic({
|
|
required String channelId,
|
|
required int kickingUId,
|
|
}) async {
|
|
try {
|
|
|
|
// 发送 RTM 消息通知所有用户
|
|
final channelName = RTCManager.instance.currentChannelId;
|
|
if (channelName != null && channelName.isNotEmpty) {
|
|
final messageData = {
|
|
'type': 'end_mic',
|
|
'kickingUId': kickingUId,
|
|
'operatorId': GlobalData().userData?.id ?? '',
|
|
'operatorName': GlobalData().userData?.nickName ?? '',
|
|
};
|
|
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: channelName,
|
|
message: json.encode(messageData),
|
|
);
|
|
print('✅ 下麦消息已发送: 下麦用户ID $kickingUId');
|
|
|
|
// 刷新频道详情
|
|
await fetchRtcChannelDetail(channelName);
|
|
}
|
|
} catch (e) {
|
|
print('❌ 下麦用户异常: $e');
|
|
SmartDialog.showToast('下麦用户失败');
|
|
}
|
|
}
|
|
|
|
/// 获取用户道具连麦卡片
|
|
Future<void> getUserPropLinkMicCard() async {
|
|
try {
|
|
final response = await _networkService.rtcApi.getUserPropLinkMicCard();
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
linkMicCard.value = base.data;
|
|
print(
|
|
'✅ 获取连麦卡片成功: type=${base.data!.type}, num=${base.data!.num}, unitSellingPrice=${base.data!.unitSellingPrice}',
|
|
);
|
|
} else {
|
|
linkMicCard.value = null;
|
|
print('❌ 获取连麦卡片失败: ${base.message}');
|
|
}
|
|
} catch (e) {
|
|
linkMicCard.value = null;
|
|
print('❌ 获取连麦卡片异常: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取虚拟账户(玫瑰数量)
|
|
Future<void> getVirtualAccount() async {
|
|
try {
|
|
final response = await _networkService.userApi.getVirtualAccount({});
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
roseCount.value = base.data!.balance ?? 0;
|
|
print('✅ 获取玫瑰数量成功: ${roseCount.value}');
|
|
} else {
|
|
roseCount.value = 0;
|
|
print('❌ 获取玫瑰数量失败: ${base.message}');
|
|
}
|
|
} catch (e) {
|
|
roseCount.value = 0;
|
|
print('❌ 获取玫瑰数量异常: $e');
|
|
}
|
|
}
|
|
}
|