Browse Source

feat(call): 添加聊天音频产品功能

- 新增聊天音频产品API接口和模型定义
- 实现获取聊天音频产品列表功能
- 在通话类型选择对话框中显示动态价格信息
- 优化通话时长显示逻辑
- 更新视频通话回调为异步处理方式
master
Jolie 2 months ago
parent
commit
b57a3757fc
8 changed files with 126 additions and 22 deletions
  1. 24
      lib/controller/message/call_controller.dart
  2. 3
      lib/network/api_urls.dart
  3. 7
      lib/network/rtc_api.dart
  4. 43
      lib/network/rtc_api.g.dart
  5. 24
      lib/pages/message/chat_page.dart
  6. 9
      lib/widget/message/call_item.dart
  7. 32
      lib/widget/message/call_type_selection_dialog.dart
  8. 6
      lib/widget/message/chat_input_bar.dart

24
lib/controller/message/call_controller.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/network/network_service.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
@ -173,6 +174,29 @@ class CallController extends GetxController {
return createOneOnOneRtcChannel(type: 3);
}
///
/// [toUserId] ID
Future<List<ChatAudioProductModel>?> listChatAudioProduct(String toUserId) async {
try {
final response = await _networkService.rtcApi.listChatAudioProduct(toUserId);
if (response.data.isSuccess) {
print('✅ [CallController] 获取聊天音频产品列表成功');
return response.data.data;
} else {
final message = response.data.message.isNotEmpty
? response.data.message
: '获取聊天音频产品列表失败';
SmartDialog.showToast(message);
print('❌ [CallController] 获取聊天音频产品列表失败: $message');
return null;
}
} catch (e) {
SmartDialog.showToast('网络请求失败,请重试');
print('❌ [CallController] 获取聊天音频产品列表异常: $e');
return null;
}
}
///
/// [targetUserId] ID
/// [callType]

3
lib/network/api_urls.dart

@ -146,4 +146,7 @@ class ApiUrls {
static const String getAppVersion =
'dating-agency-uec/user/get/app-version/update';
static const String listChatAudioProduct =
'dating-agency-chat-audio/user/list/chat-audio-product';
}

7
lib/network/rtc_api.dart

@ -1,5 +1,6 @@
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart';
import 'package:dating_touchme_app/model/live/gift_product_model.dart';
import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart';
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';
@ -123,4 +124,10 @@ abstract class RtcApi {
Future<HttpResponse<BaseResponse<dynamic>>> terminateOneOnOneRtcChannel(
@Body() Map<String, dynamic> data,
);
///
@GET(ApiUrls.listChatAudioProduct)
Future<HttpResponse<BaseResponse<List<ChatAudioProductModel>>>> listChatAudioProduct(
@Query('toUserId') String toUserId,
);
}

43
lib/network/rtc_api.g.dart

@ -706,6 +706,49 @@ class _RtcApi implements RtcApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<List<ChatAudioProductModel>>>>
listChatAudioProduct(String toUserId) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'toUserId': toUserId};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options =
_setStreamType<HttpResponse<BaseResponse<List<ChatAudioProductModel>>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/list/chat-audio-product',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<List<ChatAudioProductModel>> _value;
try {
_value = BaseResponse<List<ChatAudioProductModel>>.fromJson(
_result.data!,
(json) => json is List<dynamic>
? json
.map<ChatAudioProductModel>(
(i) => ChatAudioProductModel.fromJson(
i as Map<String, dynamic>,
),
)
.toList()
: List.empty(),
);
} 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 ||

24
lib/pages/message/chat_page.dart

@ -9,6 +9,7 @@ import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
import '../../generated/assets.dart';
import '../../model/home/marriage_data.dart';
import '../../model/rtc/chat_audio_product_model.dart';
import '../../../widget/message/chat_input_bar.dart';
import '../../../widget/message/message_item.dart';
import '../../../widget/message/chat_gift_popup.dart';
@ -136,12 +137,16 @@ class _ChatPageState extends State<ChatPage> {
}
//
void _showCallTypeSelectionDialog(ChatController controller) {
void _showCallTypeSelectionDialog(
ChatController controller, {
List<ChatAudioProductModel>? products,
}) {
//
FocusScope.of(context).unfocus();
SmartDialog.show(
builder: (context) {
return CallTypeSelectionDialog(
products: products,
onVoiceCall: () async {
//
final success = await CallController.instance.initiateCall(
@ -351,19 +356,12 @@ class _ChatPageState extends State<ChatPage> {
//
_showGiftPopup();
},
//
// onVoiceCall: () async {
// //
// await CallController.instance.initiateCall(
// targetUserId: widget.userId,
// callType: CallType.voice,
// chatController: controller,
// );
// },
//
onVideoCall: () {
//
_showCallTypeSelectionDialog(controller);
onVideoCall: () async {
//
final products = await CallController.instance.listChatAudioProduct(widget.userId);
//
_showCallTypeSelectionDialog(controller, products: products);
},
),
],

9
lib/widget/message/call_item.dart

@ -102,7 +102,8 @@ class CallItem extends StatelessWidget {
int? _getCallDuration() {
final callInfo = _parseCallInfo();
if (callInfo != null) {
return callInfo['callDuration'] as int?;
Get.log('通话信息: ${callInfo['callDuration']}');
return int.tryParse(callInfo['callDuration'].toString());
}
return null;
}
@ -148,13 +149,13 @@ class CallItem extends StatelessWidget {
if (callStatus == CallStatus.calling && duration != null) {
//
return _formatDuration(duration);
return '通话中';
} else if (callStatus == CallStatus.cancelled) {
return '已取消';
} else if (callStatus == CallStatus.rejected) {
return '已拒绝';
} else if(callStatus == CallStatus.terminated){
return '已结束';
} else if(callStatus == CallStatus.terminated && duration != null){
return _formatDuration(duration);
}else {
return callType == CallType.video ? '我刚刚邀请你视频通话' : '我刚刚邀请你语音通话';
}

32
lib/widget/message/call_type_selection_dialog.dart

@ -1,18 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../model/rtc/chat_audio_product_model.dart';
///
class CallTypeSelectionDialog extends StatelessWidget {
final VoidCallback? onVoiceCall;
final VoidCallback? onVideoCall;
final List<ChatAudioProductModel>? products;
const CallTypeSelectionDialog({
super.key,
this.onVoiceCall,
this.onVideoCall,
this.products,
});
/// 2102=2103=
ChatAudioProductModel? _getProductBySubCategory(int subCategory) {
if (products == null || products!.isEmpty) return null;
try {
return products!.firstWhere(
(product) => product.subCategory == subCategory,
);
} catch (e) {
// null
return null;
}
}
///
ChatAudioProductModel? get voiceProduct => _getProductBySubCategory(2102);
///
ChatAudioProductModel? get videoProduct => _getProductBySubCategory(2103);
///
String _formatPrice(ChatAudioProductModel? product) {
if (product == null) return '35玫瑰/分钟';
return '${product.unitSellingPrice.toInt()}玫瑰/分钟';
}
@override
Widget build(BuildContext context) {
return Container(
@ -37,7 +65,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center(
child: Text(
'语音通话 (35玫瑰/分钟)',
'语音通话 (${_formatPrice(voiceProduct)})',
style: TextStyle(
fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1),
@ -63,7 +91,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center(
child: Text(
'视频通话 (35玫瑰/分钟)',
'视频通话 (${_formatPrice(videoProduct)})',
style: TextStyle(
fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1),

6
lib/widget/message/chat_input_bar.dart

@ -13,7 +13,7 @@ class ChatInputBar extends StatefulWidget {
final ValueChanged<List<String>>? onImageSelected;
final Function(String filePath, int seconds)? onVoiceRecorded;
final VoidCallback? onVoiceCall; //
final VoidCallback? onVideoCall; //
final Future<void> Function()? onVideoCall; //
final VoidCallback? onGiftTap; //
const ChatInputBar({
@ -304,8 +304,8 @@ class _ChatInputBarState extends State<ChatInputBar> {
Assets.imagesVideoCall,
width: 24.w,
height: 24.w,
).onTap(() {
widget.onVideoCall?.call();
).onTap(() async {
await widget.onVideoCall?.call();
}),
//
Image.asset(Assets.imagesGift, width: 24.w, height: 24.w).onTap(() {

Loading…
Cancel
Save