Browse Source

feat(live): 添加礼物产品功能并实现直播间结束页面

- 新增礼物产品模型 GiftProductModel
- 在 RoomController 中集成礼物产品列表加载逻辑
- 实现直播间结束页面 LiveEndPage
- 添加获取礼物产品列表的 API 接口
- 更新网络服务以支持礼物产品相关请求
ios
Jolie 4 months ago
parent
commit
d5fed92784
6 changed files with 343 additions and 0 deletions
  1. 22
      lib/controller/discover/room_controller.dart
  2. 66
      lib/model/live/gift_product_model.dart
  3. 2
      lib/network/api_urls.dart
  4. 5
      lib/network/rtc_api.dart
  5. 41
      lib/network/rtc_api.g.dart
  6. 207
      lib/pages/discover/live_end_page.dart

22
lib/controller/discover/room_controller.dart

@ -1,5 +1,6 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/model/live/gift_product_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';
@ -36,6 +37,9 @@ class RoomController extends GetxController {
///
final RxList<LiveChatMessage> chatMessages = <LiveChatMessage>[].obs;
///
final RxList<GiftProductModel> giftProducts = <GiftProductModel>[].obs;
///
final LiveChatMessageService _messageService =
LiveChatMessageService.instance;
@ -45,6 +49,8 @@ class RoomController extends GetxController {
super.onInit();
//
_registerMessageListener();
//
loadGiftProducts();
}
@override
@ -288,6 +294,22 @@ class RoomController extends GetxController {
await RTCManager.instance.leaveChannel();
}
///
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');
}
}
/// RTC消息
Future<void> receiveRTCMessage(Map<String, dynamic> message) async {
if (message['type'] == 'join_chat') {

66
lib/model/live/gift_product_model.dart

@ -0,0 +1,66 @@
///
class GiftProductModel {
final String productId;
final String productSpecId;
final int mainCategory;
final int subCategory;
final String productTitle;
final String productDesc;
final String? detailDesc;
final double unitOriginalPrice;
final double unitSellingPrice;
final String mainPic;
final String svgaFile;
GiftProductModel({
required this.productId,
required this.productSpecId,
required this.mainCategory,
required this.subCategory,
required this.productTitle,
required this.productDesc,
this.detailDesc,
required this.unitOriginalPrice,
required this.unitSellingPrice,
required this.mainPic,
required this.svgaFile,
});
factory GiftProductModel.fromJson(Map<String, dynamic> json) {
return GiftProductModel(
productId: json['productId']?.toString() ?? '',
productSpecId: json['productSpecId']?.toString() ?? '',
mainCategory: json['mainCategory'] as int? ?? 0,
subCategory: json['subCategory'] as int? ?? 0,
productTitle: json['productTitle']?.toString() ?? '',
productDesc: json['productDesc']?.toString() ?? '',
detailDesc: json['detailDesc']?.toString(),
unitOriginalPrice: (json['unitOriginalPrice'] as num?)?.toDouble() ?? 0.0,
unitSellingPrice: (json['unitSellingPrice'] as num?)?.toDouble() ?? 0.0,
mainPic: json['mainPic']?.toString() ?? '',
svgaFile: json['svgaFile']?.toString() ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'productId': productId,
'productSpecId': productSpecId,
'mainCategory': mainCategory,
'subCategory': subCategory,
'productTitle': productTitle,
'productDesc': productDesc,
'detailDesc': detailDesc,
'unitOriginalPrice': unitOriginalPrice,
'unitSellingPrice': unitSellingPrice,
'mainPic': mainPic,
'svgaFile': svgaFile,
};
}
@override
String toString() {
return 'GiftProductModel(productId: $productId, productTitle: $productTitle, unitSellingPrice: $unitSellingPrice)';
}
}

2
lib/network/api_urls.dart

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

5
lib/network/rtc_api.dart

@ -1,4 +1,5 @@
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/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/network/api_urls.dart';
@ -61,4 +62,8 @@ abstract class RtcApi {
/// RTC
@GET(ApiUrls.getRtcChannelPage)
Future<HttpResponse<BaseResponse<PaginatedResponse<RtcChannelModel>>>> getRtcChannelPage();
///
@GET(ApiUrls.listGiftProduct)
Future<HttpResponse<BaseResponse<List<GiftProductModel>>>> listGiftProduct();
}

41
lib/network/rtc_api.g.dart

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

207
lib/pages/discover/live_end_page.dart

@ -0,0 +1,207 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/main/main_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class LiveEndPage extends StatelessWidget {
const LiveEndPage({super.key});
@override
Widget build(BuildContext context) {
final roomController = Get.find<RoomController>();
final anchorInfo = roomController.rtcChannelDetail.value?.anchorInfo;
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color.fromRGBO(19, 16, 47, 1),
const Color.fromRGBO(19, 16, 47, 1).withOpacity(0.8),
],
),
),
child: SafeArea(
child: Column(
children: [
//
Padding(
padding: EdgeInsets.only(left: 16.w, top: 10.w),
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.white,
size: 20.w,
),
onPressed: () {
Get.back();
},
),
),
),
//
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//
Container(
width: 120.w,
height: 120.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2.w,
),
),
child: ClipOval(
child: anchorInfo?.profilePhoto != null &&
anchorInfo!.profilePhoto.isNotEmpty
? CachedNetworkImage(
imageUrl:
"${anchorInfo.profilePhoto}?x-oss-process=image/format,webp/resize,w_240",
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey.withOpacity(0.3),
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2.w,
color: Colors.white.withOpacity(0.5),
),
),
),
errorWidget: (context, url, error) =>
Image.asset(
Assets.imagesUserAvatar,
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
),
)
: Image.asset(
Assets.imagesUserAvatar,
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
),
),
),
SizedBox(height: 20.w),
//
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
anchorInfo?.nickName ?? '用户',
style: TextStyle(
fontSize: 18.w,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
SizedBox(width: 12.w),
//
Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 6.w,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
border: Border.all(
color: Colors.white,
width: 1.w,
),
),
child: Text(
'关注',
style: TextStyle(
fontSize: 14.w,
color: Colors.white,
),
),
).onTap(() {
// TODO:
print('点击关注');
}),
],
),
SizedBox(height: 60.w),
// 线
Row(
children: [
Expanded(
child: Container(
height: 1.w,
color: Colors.white.withOpacity(0.3),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
'当前相亲已结束',
style: TextStyle(
fontSize: 14.w,
color: Colors.white,
),
),
),
Expanded(
child: Container(
height: 1.w,
color: Colors.white.withOpacity(0.3),
),
),
],
),
],
),
),
//
Padding(
padding: EdgeInsets.only(
left: 20.w,
right: 20.w,
bottom: 40.w + MediaQuery.of(context).padding.bottom,
),
child: Container(
width: double.infinity,
height: 50.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.w),
color: const Color.fromRGBO(108, 105, 244, 1),
),
child: Center(
child: Text(
'返回首页',
style: TextStyle(
fontSize: 16.w,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
).onTap(() {
Get.offAll(() => const MainPage());
}),
),
],
),
),
),
);
}
}
Loading…
Cancel
Save