20 changed files with 2705 additions and 411 deletions
Split View
Diff Options
-
65.vscode/launch.json
-
924lib/controller/message/chat_controller.dart
-
360lib/controller/message/conversation_controller.dart
-
2lib/controller/mine/edit_info_controller.dart
-
13lib/controller/setting/setting_controller.dart
-
325lib/im/im_manager.dart
-
2lib/network/home_api.g.dart
-
24lib/network/rtc_api.g.dart
-
64lib/network/user_api.g.dart
-
1lib/pages/home/content_card.dart
-
39lib/pages/home/user_information_page.dart
-
44lib/pages/message/chat_page.dart
-
8lib/pages/message/conversation_tab.dart
-
115lib/widget/message/chat_gift_item.dart
-
184lib/widget/message/chat_gift_popup.dart
-
6lib/widget/message/chat_input_bar.dart
-
293lib/widget/message/gift_item.dart
-
39lib/widget/message/message_item.dart
-
68location_plugin/example/pubspec.lock
-
540pubspec.lock
@ -0,0 +1,65 @@ |
|||
{ |
|||
// 使用 IntelliSense 了解相关属性。 |
|||
// 悬停以查看现有属性的描述。 |
|||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"name": "dating_touchme_app", |
|||
"request": "launch", |
|||
"type": "dart" |
|||
}, |
|||
{ |
|||
"name": "dating_touchme_app (profile mode)", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "profile" |
|||
}, |
|||
{ |
|||
"name": "dating_touchme_app (release mode)", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "release" |
|||
}, |
|||
{ |
|||
"name": "location_plugin", |
|||
"cwd": "location_plugin", |
|||
"request": "launch", |
|||
"type": "dart" |
|||
}, |
|||
{ |
|||
"name": "location_plugin (profile mode)", |
|||
"cwd": "location_plugin", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "profile" |
|||
}, |
|||
{ |
|||
"name": "location_plugin (release mode)", |
|||
"cwd": "location_plugin", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "release" |
|||
}, |
|||
{ |
|||
"name": "example", |
|||
"cwd": "location_plugin/example", |
|||
"request": "launch", |
|||
"type": "dart" |
|||
}, |
|||
{ |
|||
"name": "example (profile mode)", |
|||
"cwd": "location_plugin/example", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "profile" |
|||
}, |
|||
{ |
|||
"name": "example (release mode)", |
|||
"cwd": "location_plugin/example", |
|||
"request": "launch", |
|||
"type": "dart", |
|||
"flutterMode": "release" |
|||
} |
|||
] |
|||
} |
|||
924
lib/controller/message/chat_controller.dart
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,115 @@ |
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:dating_touchme_app/model/live/gift_product_model.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
|
|||
class ChatGiftItem extends StatelessWidget { |
|||
final GiftProductModel item; |
|||
final int active; |
|||
final int index; |
|||
final void Function(int) changeActive; |
|||
|
|||
const ChatGiftItem({ |
|||
super.key, |
|||
required this.item, |
|||
required this.active, |
|||
required this.index, |
|||
required this.changeActive, |
|||
}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final isActive = active == index; |
|||
|
|||
return InkWell( |
|||
onTap: () { |
|||
changeActive(index); |
|||
}, |
|||
child: Container( |
|||
width: 83.w, |
|||
height: 94.w, |
|||
padding: EdgeInsets.only(top: 10.w), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.all(Radius.circular(9.w)), |
|||
color: Color.fromRGBO( |
|||
117, |
|||
98, |
|||
249, |
|||
isActive ? .2 : 0, |
|||
), |
|||
border: Border.all( |
|||
width: 1, |
|||
color: Color.fromRGBO( |
|||
117, |
|||
98, |
|||
249, |
|||
isActive ? 1 : 0, |
|||
), |
|||
), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
_buildImage(), |
|||
SizedBox(height: 7.w), |
|||
Text( |
|||
item.productTitle, |
|||
style: TextStyle( |
|||
fontSize: 11.w, |
|||
color: const Color.fromRGBO(51, 51, 51, 1), |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
SizedBox(height: 1.w), |
|||
Text( |
|||
"${item.unitSellingPrice.toInt()}支", |
|||
style: TextStyle( |
|||
fontSize: 7.w, |
|||
color: const Color.fromRGBO(144, 144, 144, 1), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildImage() { |
|||
if (item.mainPic.isNotEmpty) { |
|||
return CachedNetworkImage( |
|||
imageUrl: item.mainPic, |
|||
width: 41.w, |
|||
height: 41.w, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => Container( |
|||
width: 41.w, |
|||
height: 41.w, |
|||
color: Colors.grey[300], |
|||
child: Center( |
|||
child: SizedBox( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => Container( |
|||
width: 41.w, |
|||
height: 41.w, |
|||
color: Colors.grey[300], |
|||
child: Icon(Icons.error_outline, size: 20.w, color: Colors.grey), |
|||
), |
|||
); |
|||
} else { |
|||
return Container( |
|||
width: 41.w, |
|||
height: 41.w, |
|||
color: Colors.grey[300], |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,184 @@ |
|||
import 'package:dating_touchme_app/model/live/gift_product_model.dart'; |
|||
import 'package:dating_touchme_app/widget/message/chat_gift_item.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|||
|
|||
class ChatGiftPopup extends StatefulWidget { |
|||
const ChatGiftPopup({ |
|||
super.key, |
|||
required this.activeGift, |
|||
required this.giftNum, |
|||
required this.giftList, |
|||
required this.changeActive, |
|||
required this.onSendGift, |
|||
}); |
|||
|
|||
final ValueNotifier<int?> activeGift; |
|||
final ValueNotifier<int> giftNum; |
|||
final List<GiftProductModel> giftList; |
|||
final void Function(int) changeActive; |
|||
final Future<void> Function(GiftProductModel, int) onSendGift; |
|||
|
|||
@override |
|||
State<ChatGiftPopup> createState() => _ChatGiftPopupState(); |
|||
} |
|||
|
|||
class _ChatGiftPopupState extends State<ChatGiftPopup> { |
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
// 默认选择第一个礼物 |
|||
if (widget.giftList.isNotEmpty && widget.activeGift.value == null) { |
|||
widget.activeGift.value = 0; |
|||
} |
|||
} |
|||
|
|||
// 处理赠送礼物 |
|||
Future<void> _handleSendGift() async { |
|||
// 检查是否选中了礼物 |
|||
final activeIndex = widget.activeGift.value; |
|||
if (activeIndex == null || |
|||
activeIndex < 0 || |
|||
activeIndex >= widget.giftList.length) { |
|||
SmartDialog.showToast('请先选择礼物'); |
|||
return; |
|||
} |
|||
|
|||
// 获取选中的礼物 |
|||
final gift = widget.giftList[activeIndex]; |
|||
final quantity = widget.giftNum.value; |
|||
|
|||
// 发送礼物 |
|||
await widget.onSendGift(gift, quantity); |
|||
SmartDialog.dismiss(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Material( |
|||
color: Colors.transparent, |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.vertical( |
|||
top: Radius.circular(9.w), |
|||
), |
|||
color: Colors.white, |
|||
), |
|||
height: 363.w, |
|||
child: Column( |
|||
children: [ |
|||
_buildTab(), |
|||
_buildGiftSwiper(), |
|||
_buildBottomBar(), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTab() { |
|||
return Container( |
|||
height: 47.w, |
|||
padding: EdgeInsets.only(left: 29.w), |
|||
child: Row( |
|||
children: [ |
|||
Text( |
|||
"礼物", |
|||
style: TextStyle( |
|||
fontSize: 13.w, |
|||
color: const Color.fromRGBO(117, 98, 249, 1), |
|||
fontWeight: FontWeight.w700, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildGiftSwiper() { |
|||
if (widget.giftList.isEmpty) { |
|||
return Expanded( |
|||
child: Center( |
|||
child: Text( |
|||
'暂无礼物', |
|||
style: TextStyle(fontSize: 14.w, color: Colors.grey), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return Expanded( |
|||
child: ValueListenableBuilder<int?>( |
|||
valueListenable: widget.activeGift, |
|||
builder: (context, active, _) { |
|||
return GridView.builder( |
|||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), |
|||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( |
|||
crossAxisCount: 4, // 每行4个 |
|||
crossAxisSpacing: 7.w, |
|||
mainAxisSpacing: 7.w, |
|||
childAspectRatio: 0.85, // 调整宽高比 |
|||
), |
|||
itemCount: widget.giftList.length, |
|||
itemBuilder: (context, index) { |
|||
return ChatGiftItem( |
|||
item: widget.giftList[index], |
|||
active: active ?? 0, |
|||
index: index, |
|||
changeActive: widget.changeActive, |
|||
); |
|||
}, |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildBottomBar() { |
|||
return Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 10.w), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
// 数量选择(暂时不实现,固定为1) |
|||
SizedBox(width: 1.w), |
|||
ValueListenableBuilder<int>( |
|||
valueListenable: widget.giftNum, |
|||
builder: (context, num, _) { |
|||
return Row( |
|||
children: [ |
|||
GestureDetector( |
|||
onTap: () => _handleSendGift(), |
|||
child: Container( |
|||
width: 63.w, |
|||
height: 30.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.all(Radius.circular(30.w)), |
|||
gradient: const LinearGradient( |
|||
begin: Alignment.centerLeft, |
|||
end: Alignment.centerRight, |
|||
colors: [ |
|||
Color.fromRGBO(61, 138, 224, 1), |
|||
Color.fromRGBO(131, 89, 255, 1), |
|||
], |
|||
), |
|||
), |
|||
child: Center( |
|||
child: Text( |
|||
"赠送", |
|||
style: TextStyle(fontSize: 13.w, color: Colors.white), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,293 @@ |
|||
import 'dart:convert'; |
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|||
|
|||
import '../../generated/assets.dart'; |
|||
|
|||
class GiftItem extends StatelessWidget { |
|||
final EMMessage message; |
|||
final bool isSentByMe; |
|||
final bool showTime; |
|||
final String formattedTime; |
|||
final VoidCallback? onResend; |
|||
|
|||
const GiftItem({ |
|||
required this.message, |
|||
required this.isSentByMe, |
|||
required this.showTime, |
|||
required this.formattedTime, |
|||
this.onResend, |
|||
super.key, |
|||
}); |
|||
|
|||
/// 从消息内容中解析礼物信息(使用特殊的JSON格式) |
|||
Map<String, dynamic>? _parseGiftInfo() { |
|||
try { |
|||
if (message.body.type == MessageType.TXT) { |
|||
final textBody = message.body as EMTextMessageBody; |
|||
final content = textBody.content; |
|||
|
|||
// 检查是否是礼物消息(以 [GIFT:] 开头) |
|||
if (content.startsWith('[GIFT:]')) { |
|||
final jsonStr = content.substring(7); // 移除 '[GIFT:]' 前缀 |
|||
return jsonDecode(jsonStr) as Map<String, dynamic>; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
print('解析礼物信息失败: $e'); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 获取礼物标题 |
|||
String _getGiftTitle() { |
|||
final giftInfo = _parseGiftInfo(); |
|||
if (giftInfo != null) { |
|||
return giftInfo['giftProductTitle']?.toString() ?? '礼物'; |
|||
} |
|||
return '礼物'; |
|||
} |
|||
|
|||
/// 获取礼物图片 |
|||
String _getGiftImage() { |
|||
final giftInfo = _parseGiftInfo(); |
|||
if (giftInfo != null) { |
|||
return giftInfo['giftMainPic']?.toString() ?? ''; |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
/// 获取礼物数量 |
|||
int _getGiftQuantity() { |
|||
final giftInfo = _parseGiftInfo(); |
|||
if (giftInfo != null) { |
|||
return giftInfo['quantity'] as int? ?? 1; |
|||
} |
|||
return 1; |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final giftInfo = _parseGiftInfo(); |
|||
if (giftInfo == null) { |
|||
// 如果解析失败,不显示 |
|||
return SizedBox.shrink(); |
|||
} |
|||
|
|||
final giftTitle = _getGiftTitle(); |
|||
final giftImage = _getGiftImage(); |
|||
final quantity = _getGiftQuantity(); |
|||
|
|||
return Column( |
|||
children: [ |
|||
// 显示时间 |
|||
if (showTime) _buildTimeLabel(), |
|||
Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
|||
child: Row( |
|||
mainAxisAlignment: |
|||
isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: [ |
|||
if (!isSentByMe) _buildAvatar(), |
|||
if (!isSentByMe) SizedBox(width: 8.w), |
|||
// 发送消息时,状态在左侧 |
|||
if (isSentByMe) |
|||
Align( |
|||
alignment: Alignment.center, |
|||
child: Container( |
|||
margin: EdgeInsets.only(top: 10.h), |
|||
child: _buildMessageStatus(), |
|||
), |
|||
), |
|||
if (isSentByMe) SizedBox(width: 10.w), |
|||
// 礼物消息容器 |
|||
Container( |
|||
constraints: BoxConstraints(maxWidth: 200.w), |
|||
margin: EdgeInsets.only(top: 10.h), |
|||
padding: EdgeInsets.all(12.w), |
|||
decoration: BoxDecoration( |
|||
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, |
|||
borderRadius: BorderRadius.only( |
|||
topLeft: |
|||
isSentByMe ? Radius.circular(12.w) : Radius.circular(0), |
|||
topRight: |
|||
isSentByMe ? Radius.circular(0) : Radius.circular(12.w), |
|||
bottomLeft: Radius.circular(12.w), |
|||
bottomRight: Radius.circular(12.w), |
|||
), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
// 礼物图片 |
|||
if (giftImage.isNotEmpty) |
|||
Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(8.w), |
|||
color: Colors.grey[200], |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(8.w), |
|||
child: CachedNetworkImage( |
|||
imageUrl: giftImage, |
|||
fit: BoxFit.cover, |
|||
placeholder: (context, url) => Container( |
|||
color: Colors.grey[200], |
|||
child: Center( |
|||
child: SizedBox( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => Container( |
|||
color: Colors.grey[200], |
|||
child: Icon( |
|||
Icons.card_giftcard, |
|||
size: 20.w, |
|||
color: Colors.grey[400], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
) |
|||
else |
|||
Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(8.w), |
|||
color: Colors.grey[200], |
|||
), |
|||
child: Icon( |
|||
Icons.card_giftcard, |
|||
size: 20.w, |
|||
color: Colors.grey[400], |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
// 礼物信息 |
|||
Flexible( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Text( |
|||
giftTitle, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isSentByMe ? Colors.white : Colors.black87, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
if (quantity > 1) ...[ |
|||
SizedBox(height: 2.h), |
|||
Text( |
|||
'x$quantity', |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: isSentByMe |
|||
? Colors.white70 |
|||
: Colors.grey[600], |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
if (isSentByMe) SizedBox(width: 8.w), |
|||
if (isSentByMe) _buildAvatar(), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
// 构建时间标签 |
|||
Widget _buildTimeLabel() { |
|||
return Container( |
|||
alignment: Alignment.center, |
|||
padding: EdgeInsets.symmetric(horizontal: 16.w), |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 12.w), |
|||
child: Text( |
|||
formattedTime, |
|||
style: TextStyle(fontSize: 12.sp, color: Colors.grey), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建头像 |
|||
Widget _buildAvatar() { |
|||
return Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(20.w), |
|||
image: DecorationImage( |
|||
image: AssetImage(Assets.imagesAvatarsExample), |
|||
fit: BoxFit.cover, |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建消息状态(发送中、已发送、失败重发) |
|||
Widget _buildMessageStatus() { |
|||
if (!isSentByMe) { |
|||
return SizedBox.shrink(); |
|||
} |
|||
|
|||
final status = message.status; |
|||
|
|||
if (status == MessageStatus.FAIL) { |
|||
// 发送失败,显示重发按钮 |
|||
return GestureDetector( |
|||
onTap: onResend, |
|||
child: Container( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
decoration: BoxDecoration( |
|||
color: Colors.red.withOpacity(0.1), |
|||
shape: BoxShape.circle, |
|||
), |
|||
child: Icon( |
|||
Icons.refresh, |
|||
size: 14.w, |
|||
color: Colors.red, |
|||
), |
|||
), |
|||
); |
|||
} else if (status == MessageStatus.PROGRESS) { |
|||
// 发送中,显示加载动画 |
|||
return Container( |
|||
width: 16.w, |
|||
height: 16.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey), |
|||
), |
|||
); |
|||
} else { |
|||
// 发送成功,不显示任何状态 |
|||
return SizedBox.shrink(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
540
pubspec.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save