flutter/lib/pages/chat/chat_no_friend.dart

2139 lines
85 KiB
Dart
Raw Normal View History

2025-08-21 10:50:38 +08:00
/// 聊天模板
library;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
2025-09-03 11:25:31 +08:00
import 'package:loopin/IM/controller/chat_controller.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/IM/controller/chat_detail_controller.dart';
import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_result.dart';
import 'package:loopin/IM/im_service.dart';
2025-09-03 11:25:31 +08:00
import 'package:loopin/components/network_or_asset_image.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/components/preview_video.dart';
import 'package:loopin/models/conversation_type.dart';
2025-08-27 23:26:29 +08:00
import 'package:loopin/pages/chat/notify_controller/notify_no_friend_controller.dart';
2025-08-21 10:50:38 +08:00
import 'package:loopin/utils/snapshot.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import '../../styles/index.dart';
import '../../utils/index.dart';
import './components/redpacket.dart';
import './components/richtext.dart';
// import 'mock/chat_json.dart';
import 'mock/emoj_json.dart';
class ChatNoFriend extends StatefulWidget {
const ChatNoFriend({super.key});
@override
State<ChatNoFriend> createState() => _ChatNoFriendState();
}
class _ChatNoFriendState extends State<ChatNoFriend> with SingleTickerProviderStateMixin {
late final ChatDetailController controller;
// 接收参数
late final Rx<V2TimConversation> arguments;
late String selfUserId;
// 聊天消息模块
final bool isNeedScrollBottom = true;
2025-09-03 11:25:31 +08:00
final String _tips = '回复或关注对方之前,对方只能发送一条消息';
2025-08-21 10:50:38 +08:00
bool isLoading = false; // 是否在加载中
bool hasMore = true; // 是否还有更多数据
final RxBool _throttleFlag = false.obs; // 滚动节流锁
// 表情json
List emoJson = emotionData;
// 底部操作栏模块
TextEditingController editorController = TextEditingController();
FocusNode editorFocusNode = FocusNode();
bool voiceBtnEnable = false; // 语音按钮
bool voicePanelEnable = false; // 语音操作面板
bool voiceToTransfer = false; // 语音转文字中
int voiceType = 0; // 语音操作类型
Map voiceTypeMap = {
0: '按住 说话', // 按住说话
1: '松开 发送', // 松开发送
2: '松开 取消', // 松开取消(左滑)
3: '语音转文字', // 语音转文字(右滑)
};
bool toolbarEnable = false; // 显示表情/选择区域
int toolbarIndex = 0; // 0 表情 1 选择
double keyboardHeight = 157.6; // 键盘高度
List chooseOptions = [
{'key': 'photo', 'name': '相册', 'icon': 'assets/images/icon_photo.webp'},
{'key': 'camera', 'name': '拍摄', 'icon': 'assets/images/icon_camera.webp'},
{'key': 'location', 'name': '位置', 'icon': 'assets/images/icon_location.webp'},
{'key': 'redpacket', 'name': '红包', 'icon': 'assets/images/icon_hb.webp'},
];
// controller监听
// ScrollController chatController = ScrollController();
late ScrollController chatController;
ScrollController emojController = ScrollController();
// 模拟开红包按钮动画
late AnimationController animController;
// 创建一个从 0 到 π 的旋转动画
late Animation<double> animTurns;
// 初始化状态
@override
void initState() {
super.initState();
final arg = Get.arguments as V2TimConversation;
arguments = arg.obs;
controller = Get.find<ChatDetailController>();
chatController = controller.chatController;
animController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
animTurns = Tween<double>(begin: 0, end: 3.1415926).animate(animController);
cleanUnRead();
getUserId();
isMyFriend();
getMsgData(initFlag: true);
// 编辑框获取焦点
editorFocusNode.addListener(() {
if (editorFocusNode.hasFocus) {
setState(() {
toolbarEnable = false;
});
controller.scrollToBottom();
}
});
// 滚动监听
// Future.delayed(Duration(milliseconds: 1000), () {
// });
chatController.addListener(() {
if (_throttleFlag.value) return;
if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) {
// if (chatController.position.pixels <= 50) {
_throttleFlag.value = true;
getMsgData().then((_) {
// 解锁
Future.delayed(Duration(milliseconds: 1000), () {
_throttleFlag.value = false;
});
});
}
});
}
@override
void dispose() {
if (Get.isRegistered<ChatDetailController>()) {
Get.delete<ChatDetailController>();
}
emojController.dispose();
editorFocusNode.dispose();
animController.dispose();
super.dispose();
}
// 设置好友备注
void setRemark() async {
print('不支持');
}
void isMyFriend() async {
final isFriendId = arguments.value.userID;
2025-09-03 11:25:31 +08:00
// final isfd = await ImService.instance.isMyFriend(isFriendId!, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH);
// if (isfd.success) {
// controller.isFriend.value = isfd.data;
// print(isfd.data);
// } else {
// controller.isFriend.value = false;
// print(isfd.desc);
// }
/// 0不是好友也没有关注
/// 1你关注了对方单向
/// 2对方关注了你单向
/// 3互相关注双向好友
final isfd = await ImService.instance.checkFollowType(userIDList: [isFriendId!]);
2025-08-21 10:50:38 +08:00
if (isfd.success) {
2025-09-03 11:25:31 +08:00
controller.followType.value = isfd.data?.first.followType ?? 0;
if ([3].contains(controller.followType.value)) {
controller.isFriend.value = true;
} else {
controller.isFriend.value = false;
}
logger.i('当前聊天的关系为:${controller.followType.value}');
2025-08-21 10:50:38 +08:00
}
}
bool checkSend() {
if (controller.isFriend.value) {
// 是好友直接发
return true;
} else {
// 不是好友
if (arguments.value.lastMessage != null) {
// 最后一条消息是否我发的
// final isSelf = arguments.value.lastMessage!.isSelf;
final isSelf = controller.chatList.first.isSelf;
return isSelf == true ? false : true;
} else {
return true;
}
}
}
void cleanUnRead() async {
if ((arguments.value.unreadCount ?? 0) > 0) {
final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.value.conversationID);
2025-08-27 23:26:29 +08:00
2025-08-21 10:50:38 +08:00
if (!res.success) {
MyDialog.toast('清理未读异常:${res.desc}', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
2025-08-27 23:26:29 +08:00
}
logger.w('清理陌生人会话未读消息,准备刷新消息页的UI陌生人会话列表的UI');
// 是否从未读会话列表而来
if (Get.isRegistered<NotifyNoFriendController>()) {
final ctl = Get.find<NotifyNoFriendController>();
ctl.updateUnread(conversationID: arguments.value.conversationID);
2025-08-21 10:50:38 +08:00
}
}
}
void getUserId() async {
final idRes = await ImService.instance.selfUserId();
if (idRes.success) {
selfUserId = idRes.data;
}
}
Future<void> getMsgData({bool initFlag = false}) async {
if (isLoading || !hasMore) return; // 正在加载 or 没有更多了
isLoading = true;
// 获取最旧一条消息作为游标
V2TimMessage? lastRealMsg;
// for (var msg in controller.chatList.reversed) {
for (var msg in controller.chatList.reversed) {
if (msg.localCustomData != 'time_label') {
lastRealMsg = msg;
break;
}
}
final lastMsg = lastRealMsg ?? arguments.value.lastMessage; // 如果找不到,就用传入的参数
// print(lastMsg?.toJson());
// final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage;
final res = await ImService.instance.getHistoryMessageList(
userID: arguments.value.userID,
lastMsg: lastMsg,
);
if (res.success) {
final newMessages = res.data ?? [];
if (newMessages.isEmpty) {
hasMore = false;
// MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
if (initFlag && lastMsg != null) {
newMessages.insert(0, lastMsg);
// controller.scrollToBottom();
}
controller.updateChatListWithTimeLabels(newMessages);
if (initFlag) {
// 初始化时滚到最底部
WidgetsBinding.instance.addPostFrameCallback((_) {
if (chatController.hasClients) {
// controller.scrollToBottom();
// final bottomPadding = MediaQuery.of(context).padding.bottom; // 底部安全区域高度
// chatController.jumpTo(chatController.position.maxScrollExtent); // 60为底部操作栏高度
chatController.jumpTo(0);
}
});
}
print('聊天数据加载成功');
} else {
MyDialog.toast("获取聊天记录失败:${res.desc}", icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
isLoading = false;
}
// 渲染聊天消息
List<Widget> renderChatList() {
List<Widget> msgtpl = [];
for (var item in controller.chatList) {
// 时间提示,公告提示
if (item.localCustomData == 'time_label') {
msgtpl.add(Container(
margin: const EdgeInsets.only(bottom: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.customElem!.data ?? '',
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
),
],
),
));
}
// 文本消息模板=1
else if (item.elemType == 1) {
msgtpl.add(
RenderChatItem(
data: item,
child: Ink(
decoration: BoxDecoration(
color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B),
borderRadius: BorderRadius.circular(10.0),
),
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
borderRadius: BorderRadius.circular(10.0),
child: Container(
padding: const EdgeInsets.all(10.0),
child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
),
2025-09-03 11:25:31 +08:00
// onLongPress: () {
// contextMenuDialog();
// },
2025-08-21 10:50:38 +08:00
),
),
),
);
}
// gif表情模板=8
else if (item.elemType == 8) {
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: Container(
constraints: const BoxConstraints(
maxHeight: 100.0,
maxWidth: 100.0,
),
// child: Image.asset('assets/images/emotion/${item.faceElem?.data}'),
child: Image.asset('${item.faceElem?.data}'),
),
onLongPress: () {
contextMenuDialog();
},
),
),
));
}
// 图片模板=3
else if (item.elemType == 3) {
// List<String> imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? [];
final originImage = item.imageElem?.imageList?.firstWhere((e) => e?.type == 0 && e?.url != null, orElse: () => null);
List<String> imagePaths = originImage != null ? [originImage.url!] : [];
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
// child: ImageGroup(
// images: imagePaths,
// width: 120,
// ),
child: Image.network(
imagePaths.first,
width: 120,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
// controller.scrollToBottom();
return child; // 加载完成,显示图片
}
return Container(
width: 120,
height: 240,
color: Colors.grey[300],
alignment: Alignment.center,
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[300],
alignment: Alignment.center,
child: Icon(Icons.broken_image, color: Colors.grey, size: 40),
);
},
),
),
onLongPress: () {
contextMenuDialog();
},
),
),
));
}
// 视频模板=5
else if (item.elemType == 5) {
// print(item.videoElem!.toLogString());
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
child: SizedBox(
width: 120.0,
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(
fit: BoxFit.cover,
item.videoElem?.snapshotUrl ?? '',
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/pic1.jpg',
height: 60.0,
width: 60.0,
fit: BoxFit.cover,
);
},
),
),
const Align(
alignment: Alignment.center,
child: Icon(
Icons.play_circle,
color: Colors.white,
size: 30.0,
),
),
],
),
),
// onTap: () {
// MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
// },
onTap: () {
showGeneralDialog(
context: context,
// barrierDismissible: true,
barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
pageBuilder: (_, __, ___) {
return SafeArea(
child: PreviewVideo(
videoUrl: item.videoElem?.videoUrl ?? '',
width: item.videoElem?.snapshotWidth?.toDouble(),
height: item.videoElem?.snapshotHeight?.toDouble(),
),
);
},
transitionBuilder: (_, anim, __, child) {
return FadeTransition(opacity: anim, child: child);
},
transitionDuration: const Duration(milliseconds: 200),
);
},
onLongPress: () {
contextMenuDialog();
},
),
),
));
}
// 语音模板=4
else if (item.elemType == 4) {
List<Widget> audiobody = [
Ink(
decoration: BoxDecoration(
color: !(item.isSelf ?? false) ? const Color(0xFFFFFFFF) : const Color(0xFF89E45B),
borderRadius: BorderRadius.circular(10.0),
),
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
borderRadius: BorderRadius.circular(10.0),
child: Container(
padding: const EdgeInsets.all(10.0),
constraints: BoxConstraints(
// maxWidth: 120.0,
maxWidth: (item.soundElem?.duration)! / 60 * 230,
),
child: Row(
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
children: !(item.isSelf ?? false)
? [
const Icon(Icons.multitrack_audio),
const SizedBox(
width: 5.0,
),
Text('${item.soundElem?.duration}'),
]
: [
Text('${item.soundElem?.duration}'),
const SizedBox(
width: 5.0,
),
const Icon(Icons.multitrack_audio),
],
),
),
onTap: () {
MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
},
onLongPress: () {
contextMenuDialog();
},
),
),
const SizedBox(
width: 5.0,
),
FStyle.badge(0, isdot: true),
];
if (item.isSelf ?? false) {
// 内容反转
audiobody = audiobody.reversed.toList();
} else {
audiobody = audiobody;
}
msgtpl.add(RenderChatItem(
data: item,
child: Row(
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
children: audiobody,
)));
}
// 红包模板=自定义=2
else if (item.elemType == 2 && item.cloudCustomData == 'hongbao') {
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
decoration: BoxDecoration(
color: const Color(0xFFFF7F43),
borderRadius: BorderRadius.circular(10.0),
),
child: InkWell(
overlayColor: WidgetStateProperty.all(Colors.transparent),
borderRadius: BorderRadius.circular(10.0),
child: Container(
constraints: const BoxConstraints(
maxWidth: 210.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10.0),
child: Row(
spacing: 10.0,
children: <Widget>[
Image.asset(
'assets/images/hbico.png',
width: 32.0,
fit: BoxFit.contain,
),
Text(item.customElem?.data ?? '', style: const TextStyle(color: Colors.white, fontSize: 14.0)),
],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 10.0),
padding: const EdgeInsets.symmetric(vertical: 5.0),
width: double.infinity,
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
child: const Text(
'拼手气红包',
style: TextStyle(color: Colors.white70, fontSize: 11.0),
),
),
],
),
),
onTap: () {
receiveRedPacketDialog(item);
},
onLongPress: () {
contextMenuDialog();
},
),
),
));
}
// 位置模板=7
else if (item.elemType == 7) {
msgtpl.add(RenderChatItem(
data: item,
child: Ink(
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(10.0),
),
child: InkWell(
// splashColor: Colors.transparent,
overlayColor: WidgetStateProperty.all(Colors.transparent),
borderRadius: BorderRadius.circular(10.0),
child: Container(
constraints: const BoxConstraints(
maxWidth: 210.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.locationElem?.desc ?? '位置信息异常',
overflow: TextOverflow.ellipsis,
),
Text(
"${item.locationElem?.latitude},${item.locationElem?.longitude}",
style: const TextStyle(color: Colors.grey, fontSize: 12.0),
overflow: TextOverflow.ellipsis,
),
],
),
),
ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(10.0)),
child: Image.asset('assets/images/map.jpg', width: 210.0, height: 70.0, fit: BoxFit.cover),
)
],
),
),
onTap: () {
MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
},
onLongPress: () {
contextMenuDialog();
},
),
),
));
}
}
// msgtpl.insert(
// 0,
// SizedBox.shrink(),
// );
return msgtpl;
}
// 表情列表集合
List<Widget> renderEmojWidget() {
return [
// Tab切换
Container(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Row(
children: emoJson.map((item) {
return InkWell(
child: Container(
margin: const EdgeInsets.all(5.0),
alignment: Alignment.center,
height: 40.0,
width: 40.0,
decoration: BoxDecoration(color: item['selected'] ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(5.0)),
child: item['index'] == 0
? Text(
item['pathLabel'],
style: const TextStyle(fontSize: 22.0),
)
: Image.asset(item['pathLabel'], height: 24.0, width: 24.0, fit: BoxFit.cover),
),
onTap: () {
handleEmojTab(item['index']);
},
);
}).toList(),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
border: const Border(top: BorderSide(color: Colors.black54, width: .1)),
),
child: ListView(
controller: emojController,
padding: const EdgeInsets.all(10.0),
children: emoJson.map((item) {
return Visibility(
visible: item['selected'],
child: GridView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 横轴元素个数
crossAxisCount: item['type'] == 'emoj' ? 8 : 5,
// 纵轴间距
mainAxisSpacing: 5.0,
// 横轴间距
crossAxisSpacing: 5.0,
// 子组件宽高比例
childAspectRatio: 1,
),
children: item['nodes'].map<Widget>((emoj) {
if (item['type'] == 'emoj') {
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(5.0),
child: Container(
alignment: Alignment.center,
height: 40.0,
width: 40.0,
child: Text(
emoj,
style: const TextStyle(fontSize: 24.0),
),
),
onTap: () {
handleEmojClick(emoj);
},
),
);
} else {
return Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(5.0),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(5.0),
height: 68.0,
width: 68.0,
child: Image.asset(emoj),
),
onTap: () {
handleGIFClick(emoj, item['index']);
},
),
);
}
}).toList(),
),
);
}).toList(),
),
),
),
];
}
// 选择功能列表
List<Widget> renderChooseWidget() {
return [
Expanded(
child: Container(
padding: const EdgeInsets.fromLTRB(30.0, 35.0, 30.0, 15.0),
decoration: BoxDecoration(
color: Colors.grey[200],
border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
),
child: GridView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
// 横轴元素个数
crossAxisCount: 4,
// 纵轴间距
mainAxisSpacing: 30.0,
// 横轴间距
crossAxisSpacing: 25.0,
// 子组件宽高比例
childAspectRatio: .8,
),
children: chooseOptions.map((item) {
return Column(
children: [
Expanded(
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
),
child: InkWell(
borderRadius: BorderRadius.circular(15.0),
child: Image.asset(item['icon'], height: 40.0, fit: BoxFit.cover),
onTap: () {
handleChooseAction(item['key']);
},
),
),
),
),
const SizedBox(height: 5.0),
Text(
item['name'],
style: const TextStyle(color: Colors.black87, fontSize: 12.0),
)
],
);
}).toList(),
),
),
),
];
}
/* ---------- { 聊天消息模块 } ---------- */
// 聊天消息滚动到底部
// void scrollToBottom() async {
// chatList = await fetchChatList();
// chatController.animateTo(isNeedScrollBottom ? 0 : chatController.position.maxScrollExtent,
// duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
// }
// void scrollToBottom() {
// Future.delayed(Duration(milliseconds: 300), () {
// if (chatController.hasClients) {
// chatController.animateTo(
// // 0, // reverse: true 时滚动到底部是 offset: 0
// chatController.position.maxScrollExtent,
// duration: Duration(milliseconds: 300),
// curve: Curves.easeOut,
// );
// }
// });
// }
// 点击消息区域
void handleClickChatArea() {
hideKeyboard();
setState(() {
toolbarEnable = false;
});
}
/* ---------- { 底部Toolbar模块 } ---------- */
// 光标处插入内容
void insertTextAtCursor(String html) {
var editorNotifier = editorController.value; // The current value stored in this notifier.
var start = editorNotifier.selection.baseOffset;
var end = editorNotifier.selection.extentOffset;
if (editorNotifier.selection.isValid) {
String newText = '';
if (editorNotifier.selection.isCollapsed) {
if (end > 0) {
newText += editorNotifier.text.substring(0, end);
}
newText += html;
if (editorNotifier.text.length > end) {
newText += editorNotifier.text.substring(end, editorNotifier.text.length);
}
} else {
newText = editorNotifier.text.replaceRange(start, end, html);
end = start;
}
editorController.value =
editorNotifier.copyWith(text: newText, selection: editorNotifier.selection.copyWith(baseOffset: end + html.length, extentOffset: end + html.length));
} else {
editorController.value = TextEditingValue(
text: html,
selection: TextSelection.fromPosition(TextPosition(offset: html.length)),
);
}
}
// 发送消息队列
void sendMessage(message) async {
final canSend = checkSend();
if (!canSend) {
final baseStyle = MyDialog.theme.toastStyle?.top();
MyDialog.toast(
'对方未回关或回复,只能发送一条消息',
icon: const Icon(Icons.check_circle),
duration: Duration(milliseconds: 5000),
style: baseStyle?.copyWith(
backgroundColor: Colors.red.withAlpha(200),
),
);
print('禁止发送$canSend');
return;
}
// 待插入的消息
List<V2TimMessage> messagesToInsert = [];
V2TimMessage? lastRealMsg;
for (var msg in controller.chatList) {
if (msg.localCustomData != 'time_label') {
lastRealMsg = msg;
break;
}
}
// 如果有数据,检测时间,是否需要插入伪消息
if (lastRealMsg != null &&
needInsertTimeLabel(
(lastRealMsg.timestamp ?? 0) * 1000, // 转为毫秒级
DateTime.now().millisecondsSinceEpoch,
)) {
// 消息时间间隔超过3分钟插入伪消息
2025-09-03 11:25:31 +08:00
final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000);
2025-08-21 10:50:38 +08:00
final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId);
messagesToInsert.add(resMsg.data);
} else {
// 没数据的时候直接插入伪消息
2025-09-03 11:25:31 +08:00
final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000);
2025-08-21 10:50:38 +08:00
final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId);
messagesToInsert.add(resMsg.data);
}
// 不需要时间标签
// 消息类型message['type']
late final ImResult res;
res = await IMMessage().sendMessage(
msg: message,
toUserID: arguments.value.userID,
cloudCustomData: canSend == true ? ConversationType.noFriend.name : '',
);
if (res.success && res.data != null) {
messagesToInsert.insert(0, res.data); // 加入消息本体
// messagesToInsert.add(res.data); // 加入消息本体
controller.chatList.insertAll(0, messagesToInsert);
// controller.chatList.addAll(messagesToInsert);
controller.scrollToBottom();
print('发送成功');
} else {
print('消息发送失败: ${res.code} - ${res.desc}');
}
}
bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) {
return (newTimestamp - lastTimestamp) > interval * 1000;
}
// 隐藏键盘
void hideKeyboard() {
if (editorFocusNode.hasFocus) {
editorFocusNode.unfocus();
}
}
// 表情/选择切换
void handleEmojChooseState(index) {
hideKeyboard();
setState(() {
toolbarEnable = true;
toolbarIndex = index;
voiceBtnEnable = false;
});
controller.scrollToBottom();
}
// 表情Tab切换
void handleEmojTab(index) {
var emols = emoJson;
for (var i = 0, len = emols.length; i < len; i++) {
emols[i]['selected'] = false;
}
emols[index]['selected'] = true;
setState(() {
emoJson = emols;
});
emojController.jumpTo(0);
}
// 点击表情插入到输入框
void handleEmojClick(emoj) {
insertTextAtCursor(emoj);
}
// 点击Gif大图发送=8
void handleGIFClick(gifpath, index) async {
// 消息队列
// Map message = {
// 'contentType': 8,
// 'content': gifpath,
// };
final res = await IMMessage().createFaceMessage(data: gifpath, index: index);
if (res.success) {
sendMessage(res.data?.messageInfo);
}
}
// 发送文本消息=1
void handleSubmit() async {
if (editorController.text.isEmpty) return;
// 消息队列
// Map message = {
// 'contentType': 1,
// 'content': editorController.text,
// };
final res = await IMMessage().createTextMessage(text: editorController.text);
if (res.success) {
sendMessage(res.data?.messageInfo);
editorController.clear();
}
}
// 发送图片消息=3
void sendImage(imgPath) async {
final resImg = await IMMessage().createImageMessage(imagePath: imgPath);
if (resImg.success) {
sendMessage(resImg.data?.messageInfo);
}
}
// 发送视频消息=5
void sendVideo(videoFilePath, type, duration, snapshotPath) async {
final resImg = await IMMessage().createVideoMessage(
videoFilePath: videoFilePath,
type: type,
duration: duration,
snapshotPath: snapshotPath,
);
if (resImg.success) {
sendMessage(resImg.data?.messageInfo);
}
}
// 底部操作蓝选择区操作
void handleChooseAction(key) {
MyDialog.toast('$key');
switch (key) {
case 'photo':
// ....
pickFile(context);
break;
case 'camera':
// ....
break;
case 'redpacket':
sendRedPacketDialog();
break;
}
}
///从相册选取图片/视频
void pickFile(BuildContext context) async {
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 5,
requestType: RequestType.common,
filterOptions: FilterOptionGroup(
imageOption: const FilterOption(),
videoOption: const FilterOption(
durationConstraint: DurationConstraint(
max: Duration(seconds: 120),
),
),
),
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
for (final asset in pickedAssets) {
switch (asset.type) {
case AssetType.image:
print("选中了图片:${asset.title}");
var file = await asset.file;
if (file != null) {
var fileSizeInBytes = await file.length();
var sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 28) {
MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("图片合法,大小:$sizeInMB MB");
// 执行发送逻辑
sendImage(file.path);
}
}
break;
case AssetType.video:
print("选中了视频:${asset.title}");
var file = await asset.file;
if (file != null) {
var fileSizeInBytes = await file.length();
var sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 100) {
MyDialog.toast('视频大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
print("视频合法,大小:$sizeInMB MB");
// 执行发送逻辑
var snapshot = await generateVideoThumbnail(file.path);
String? mimeType = await asset.mimeTypeAsync;
String vdType = mimeType?.split('/').last ?? 'mp4';
print(vdType);
sendVideo(file.path, vdType, asset.duration, snapshot);
}
}
break;
default:
print("不支持的类型:${asset.type}");
}
}
// final asset = pickedAssets.first;
// final file = await asset.file; // 获取实际文件
// if (file != null) {
// final fileSizeInBytes = await file.length();
// final sizeInMB = fileSizeInBytes / (1024 * 1024);
// if (sizeInMB > 100) {
// MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
// } else {
// print("图片合法,大小:$sizeInMB MB");
// //走upload(file)上传图片拿到url地址
// // file;
// }
// }
}
}
/* ---------- { 弹窗功能模块 } ---------- */
// 红包弹窗
void receiveRedPacketDialog(data) {
showDialog(
context: context,
builder: (context) {
return Material(
type: MaterialType.transparency,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 50.0),
padding: const EdgeInsets.symmetric(vertical: 50.0),
decoration: const BoxDecoration(
color: Color(0xFFFF7F43),
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Image.asset(data['avatar'], height: 40.0, width: 40.0, fit: BoxFit.cover),
),
const SizedBox(
height: 5.0,
),
Text(
data['author'],
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w600),
),
Text(
data['content'],
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0),
),
SizedBox(
height: 100.0,
),
AnimatedBuilder(
animation: animTurns,
builder: (context, child) {
return Transform(
transform: Matrix4.rotationY(animTurns.value),
alignment: Alignment.center,
child: FilledButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)),
padding: WidgetStateProperty.all(EdgeInsets.zero),
minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)),
shape: WidgetStateProperty.all(const CircleBorder()),
elevation: WidgetStateProperty.all(3.0),
),
child: Text(
'',
style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0),
),
onPressed: () {
// 开始动画
animController.repeat();
// 模拟开红包逻辑1 秒后停止动画
Future.delayed(Duration(seconds: 1), () {
animController.stop();
animController.reset();
Get.back();
});
},
),
);
},
),
],
),
),
GestureDetector(
child: Container(
margin: const EdgeInsets.only(top: 20.0),
height: 30.0,
width: 30.0,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1.5),
borderRadius: BorderRadius.circular(50.0),
),
child: const Icon(
Icons.close_outlined,
color: Colors.white,
size: 18.0,
),
),
onTap: () {
Navigator.of(context).pop();
},
)
],
));
});
}
// 长按消息菜单
void contextMenuDialog() {
2025-09-03 11:25:31 +08:00
return;
2025-08-21 10:50:38 +08:00
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
backgroundColor: Colors.white,
surfaceTintColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 5.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
children: [
SimpleDialogOption(
child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')),
onPressed: () {},
),
SimpleDialogOption(
child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')),
onPressed: () {},
),
SimpleDialogOption(
child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')),
onPressed: () {},
),
SimpleDialogOption(
child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')),
onPressed: () {},
),
],
);
},
);
}
// 发群红包弹窗
void sendRedPacketDialog() {
showModalBottomSheet(
backgroundColor: Colors.grey[50],
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))),
showDragHandle: true,
clipBehavior: Clip.hardEdge,
isScrollControlled: true, // 屏幕最大高度
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height - 180, // 自定义最大高度
),
context: context,
builder: (context) {
return RedPacket(
flag: false,
);
},
);
}
/* ---------- { 其它功能模块 } ---------- */
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
// 页面主体(聊天消息区/底部操作区)
Scaffold(
backgroundColor: Colors.grey[200],
resizeToAvoidBottomInset: true, // 启用键盘自动避让
appBar: AppBar(
centerTitle: true,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios_rounded,
size: 20.0,
),
onPressed: () {
Get.back();
},
),
titleSpacing: 1.0,
title: Obx(() {
return Text(
// '${arguments['title']}',
'${arguments.value.showName}',
style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'),
);
}),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFBE4EFF), Color(0xFF1DFFC7)],
)),
),
actions: [
2025-09-03 11:25:31 +08:00
// IconButton(
// icon: const Icon(
// Icons.more_horiz,
// color: Colors.white,
// ),
// onPressed: () async {
// final paddingTop = MediaQuery.of(Get.context!).padding.top;
// final selected = await showMenu(
// context: Get.context!,
// position: RelativeRect.fromLTRB(
// double.infinity,
// kToolbarHeight + paddingTop - 12,
// 8,
// double.infinity,
// ),
// color: FStyle.primaryColor,
// elevation: 8,
// items: [
// PopupMenuItem<String>(
// value: 'report',
// child: Row(
// children: [
// Icon(Icons.report, color: Colors.white, size: 18),
// SizedBox(width: 8),
// Text(
// '举报',
// style: TextStyle(color: Colors.white),
// ),
// ],
// ),
// ),
// PopupMenuItem<String>(
// value: 'block',
// child: Row(
// children: [
// Icon(Icons.block, color: Colors.white, size: 18),
// SizedBox(width: 8),
// Text(
// '拉黑',
// style: TextStyle(color: Colors.white),
// ),
// ],
// ),
// ),
// ],
// );
// if (selected != null) {
// switch (selected) {
// case 'report':
// print('点击了举报');
// break;
// case 'block':
// print('点击了拉黑');
// break;
// }
// }
// },
// ),
2025-08-21 10:50:38 +08:00
],
),
body: Flex(
direction: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 渲染聊天消息
// Expanded(
// child: ScrollConfiguration(
// behavior: CustomScrollBehavior(),
// child: GestureDetector(
// child: Obx(() {
// return ListView(
// reverse: true,
// controller: chatController,
// padding: const EdgeInsets.all(10.0),
// children: renderChatList(),
// );
// }),
// onTap: () {
// handleClickChatArea();
// },
// ),
// ),
// ),
2025-09-03 11:25:31 +08:00
// 聊天内容
// Expanded(
// child: GestureDetector(
// behavior: HitTestBehavior.opaque,
// onTap: handleClickChatArea,
// child: LayoutBuilder(
// builder: (context, constraints) {
// return Obx(() {
// // final tipWidget = Builder(builder: (context) => Text(tips));
// final msgWidgets = renderChatList().reversed.toList();
// // if (controller.isFriend.value) {}
// return ListView(
// controller: chatController,
// reverse: true,
// padding: const EdgeInsets.all(10.0),
// children: [
// ConstrainedBox(
// constraints: BoxConstraints(
// minHeight: constraints.maxHeight - 20,
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: msgWidgets,
// ),
// ),
// ],
// );
// });
// },
// ),
// ),
// ),
2025-08-21 10:50:38 +08:00
// 聊天内容
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: handleClickChatArea,
child: LayoutBuilder(
builder: (context, constraints) {
return Obx(() {
2025-09-03 11:25:31 +08:00
final showTipWidget = controller.followType.value;
const double tipHeight = 50; // 提示横幅固定高度
2025-08-21 10:50:38 +08:00
final msgWidgets = renderChatList().reversed.toList();
2025-09-03 11:25:31 +08:00
return Stack(
2025-08-21 10:50:38 +08:00
children: [
2025-09-03 11:25:31 +08:00
ListView(
controller: chatController,
reverse: true,
padding: EdgeInsets.only(
top: [1, 2].contains(showTipWidget) ? tipHeight + 10 : 0, // 动态预留顶部空间
left: 10,
right: 10,
bottom: 10,
2025-08-21 10:50:38 +08:00
),
2025-09-03 11:25:31 +08:00
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - ([1, 2].contains(showTipWidget) ? tipHeight : 0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: msgWidgets,
),
),
],
2025-08-21 10:50:38 +08:00
),
2025-09-03 11:25:31 +08:00
if ([1, 2].contains(showTipWidget))
Positioned(
top: 0,
left: 0,
right: 0,
height: tipHeight, // 固定高度
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.error_outline,
color: FStyle.primaryColor,
size: 20,
),
SizedBox(
width: 10,
),
Expanded(
child: Text(
'对方未回复或互关前仅可发送一条消息',
style: const TextStyle(color: Colors.black, fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
Visibility(
visible: [2].contains(showTipWidget),
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: FStyle.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
onPressed: () async {
// 回关
final res = await ImService.instance.followUser(userIDList: [arguments.value.userID!]);
if (res.success) {
controller.isFriend.value = true;
controller.followType.value = 3;
if (arguments.value.conversationGroupList?.isNotEmpty == true) {
//把会话数据从陌生人分组移除
final ctl = Get.find<ChatController>();
ctl.removeNoFriend(conversationID: arguments.value.conversationID);
ctl.updateNoFriendMenu();
}
}
},
child: const Text('回关', style: TextStyle(color: Colors.white)),
),
)
],
),
),
),
2025-08-21 10:50:38 +08:00
],
);
});
},
),
),
),
// 底部操作栏
Container(
color: Colors.grey[100],
child: SafeArea(
bottom: true,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
),
child: Column(
children: [
// 输入框编辑器模块
Container(
padding: const EdgeInsets.all(10.0),
child: Row(
children: [
InkWell(
child: Icon(
2025-09-03 11:25:31 +08:00
// voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
Icons.keyboard_outlined,
2025-08-21 10:50:38 +08:00
color: const Color(0xFF3B3B3B),
size: 30.0,
),
onTap: () {
setState(() {
toolbarEnable = false;
2025-09-03 11:25:31 +08:00
if (editorFocusNode.hasFocus) {
editorFocusNode.unfocus();
} else {
2025-08-21 10:50:38 +08:00
editorFocusNode.requestFocus();
2025-09-03 11:25:31 +08:00
}
if (toolbarEnable) {
// voiceBtnEnable = false;
// editorFocusNode.requestFocus();
// editorFocusNode.requestFocus();
2025-08-21 10:50:38 +08:00
} else {
2025-09-03 11:25:31 +08:00
// voiceBtnEnable = true;
// toolbarEnable = true;
// editorFocusNode.unfocus();
2025-08-21 10:50:38 +08:00
}
});
},
),
const SizedBox(
width: 10.0,
),
Expanded(
child: Container(
constraints: const BoxConstraints(minHeight: 40.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
child: Stack(
children: [
// 输入框
Offstage(
offstage: voiceBtnEnable,
child: TextField(
decoration: const InputDecoration(
isDense: true,
hoverColor: Colors.transparent,
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(borderSide: BorderSide.none),
),
style: const TextStyle(
fontSize: 16.0,
),
maxLines: null,
controller: editorController,
focusNode: editorFocusNode,
cursorColor: const Color(0xFF07C160),
onChanged: (value) {},
),
),
// 语音
Offstage(
offstage: !voiceBtnEnable,
child: GestureDetector(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
alignment: Alignment.center,
height: 40.0,
width: double.infinity,
child: Text(
voiceTypeMap[voiceType],
style: const TextStyle(fontSize: 15.0),
),
),
onPanStart: (details) {
setState(() {
voiceType = 1;
voicePanelEnable = true;
});
},
onPanUpdate: (details) {
Offset pos = details.globalPosition;
double swipeY = MediaQuery.of(context).size.height - 120;
double swipeX = MediaQuery.of(context).size.width / 2 + 50;
setState(() {
if (pos.dy >= swipeY) {
voiceType = 1; // 松开发送
} else if (pos.dy < swipeY && pos.dx < swipeX) {
voiceType = 2; // 左滑松开取消
} else if (pos.dy < swipeY && pos.dx >= swipeX) {
voiceType = 3; // 右滑语音转文字
}
});
},
onPanEnd: (details) {
// print('停止录音');
setState(() {
switch (voiceType) {
case 1:
MyDialog.toast('发送录音文件');
voicePanelEnable = false;
break;
case 2:
MyDialog.toast('取消发送');
voicePanelEnable = false;
break;
case 3:
MyDialog.toast('语音转文字');
voicePanelEnable = true;
voiceToTransfer = true;
break;
}
voiceType = 0;
});
},
),
),
],
),
),
),
const SizedBox(
width: 10.0,
),
2025-09-03 11:25:31 +08:00
// InkWell(
// child: const Icon(
// Icons.add_reaction_rounded,
// color: Color(0xFF3B3B3B),
// size: 30.0,
// ),
// onTap: () {
// handleEmojChooseState(0);
// },
// ),
// const SizedBox(
// width: 8.0,
// ),
// InkWell(
// child: const Icon(
// Icons.add,
// color: Color(0xFF3B3B3B),
// size: 30.0,
// ),
// onTap: () {
// handleEmojChooseState(1);
// },
// ),
2025-08-21 10:50:38 +08:00
const SizedBox(
width: 8.0,
),
InkWell(
child: Container(
height: 25.0,
width: 25.0,
decoration: BoxDecoration(
color: const Color(0xFF07C160),
borderRadius: BorderRadius.circular(20.0),
),
child: const Icon(
Icons.arrow_upward,
color: Colors.white,
size: 20.0,
),
),
onTap: () {
handleSubmit();
},
),
],
),
),
// 表情+选择模块
Visibility(
visible: toolbarEnable,
child: SizedBox(
height: keyboardHeight,
child: Column(
children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(),
),
),
)
],
),
),
),
)
],
),
),
// 录音主体(按住说话/松开取消/语音转文本)
IgnorePointer(
ignoring: false,
child: Visibility(
visible: voicePanelEnable,
child: Material(
color: const Color(0xDD1B1B1B),
child: Stack(
children: [
// 取消发送+语音转文字
Positioned(
bottom: 120,
left: 30,
right: 30,
child: Visibility(
visible: !voiceToTransfer,
child: Column(
crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
// 语音动画层
Stack(
alignment: Alignment.bottomCenter,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 200),
height: 70.0,
width: voiceType == 2 ? 70.0 : 200.0,
decoration: BoxDecoration(
color: voiceType == 2 ? Colors.red : Color(0xFF89E45B),
borderRadius: BorderRadius.circular(15.0),
),
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/voice_waves.gif', height: 23.0, width: voiceType == 2 ? 30.0 : 70.0, fit: BoxFit.cover)
],
),
),
RotatedBox(
quarterTurns: 0,
child: CustomPaint(painter: ArrowShape(arrowColor: voiceType == 2 ? Colors.red : Color(0xFF89E45B), arrowSize: 10.0)),
)
],
),
const SizedBox(
height: 50.0,
),
// 操作项
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 取消发送
Container(
height: 60.0,
width: 60.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
color: voiceType == 2 ? Colors.red : Colors.black38,
),
child: Icon(
Icons.close,
color: Colors.white54,
),
),
// 语音转文字
Container(
height: 60.0,
width: 60.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38,
),
child: Icon(
Icons.translate,
color: Colors.white54,
),
),
],
),
],
),
),
),
// 语音转文字(识别结果状态)
Positioned(
bottom: 120,
left: 30,
right: 30,
child: Visibility(
visible: voiceToTransfer,
child: Column(
children: [
// 提示结果
Stack(
children: [
Container(
height: 100.0,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(15.0),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outlined,
color: Colors.white,
),
Text(
'未识别到文字。',
style: TextStyle(color: Colors.white),
),
],
),
),
Positioned(
right: 35.0,
bottom: 1,
child: RotatedBox(
quarterTurns: 0,
child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)),
)),
],
),
const SizedBox(
height: 50.0,
),
// 操作项
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
child: Container(
height: 60.0,
width: 60.0,
decoration: const BoxDecoration(
color: Colors.transparent,
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.undo,
color: Colors.white54,
),
Text(
'取消',
style: TextStyle(color: Colors.white70),
)
],
),
),
onTap: () {
setState(() {
voicePanelEnable = false;
voiceToTransfer = false;
});
},
),
GestureDetector(
child: Container(
height: 60.0,
width: 100.0,
decoration: const BoxDecoration(
color: Colors.transparent,
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.graphic_eq_rounded,
color: Colors.white54,
),
Text(
'发送原语音',
style: TextStyle(color: Colors.white70),
)
],
),
),
onTap: () {},
),
GestureDetector(
child: Container(
height: 60.0,
width: 60.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
color: Colors.white12,
),
child: const Icon(
Icons.check,
color: Colors.white12,
),
),
onTap: () {},
),
],
),
],
),
),
),
// 提示文字(操作状态)
Positioned(
bottom: 120,
left: 0,
width: MediaQuery.of(context).size.width,
child: Visibility(
visible: !voiceToTransfer,
child: Align(
child: Text(
voiceTypeMap[voiceType],
style: const TextStyle(color: Colors.white70),
),
),
),
),
// 背景
Align(
alignment: Alignment.bottomCenter,
child: Visibility(
visible: !voiceToTransfer,
child: Image.asset('assets/images/voice_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill),
),
),
// 背景图标
Positioned(
bottom: 25,
left: 0,
width: MediaQuery.of(context).size.width,
child: Visibility(
visible: !voiceToTransfer,
child: const Align(
child: Icon(
Icons.graphic_eq_rounded,
color: Colors.black54,
),
),
),
),
],
),
),
),
)
],
);
}
}
// 渲染聊天消息公共部分
class RenderChatItem extends StatelessWidget {
const RenderChatItem({
super.key,
required this.data,
required this.child,
});
final V2TimMessage data; // 消息数据
final Widget? child; // 消息体
// 设置箭头颜色
// Color arrowColor(data) {
// Color color = Colors.transparent;
// if ([8].contains(data.elemType)) {
// // 红包箭头颜色
// color = const Color(0xFFFFA52F);
// } else if ([9].contains(data.elemType)) {
// // 位置箭头颜色
// color = const Color(0xFFFFFFFF);
// } else {
// color = !data['isme'] ? const Color(0xFFFFFFFF) : const Color(0xFF9543FF);
// }
// return color;
// }
@override
Widget build(BuildContext context) {
2025-09-03 11:25:31 +08:00
String? displayName = (data.friendRemark?.isNotEmpty ?? false)
? data.friendRemark
: (data.nameCard?.isNotEmpty ?? false)
? data.nameCard
: (data.nickName?.isNotEmpty ?? false)
? data.nickName
: '未知昵称';
2025-08-21 10:50:38 +08:00
return Container(
margin: const EdgeInsets.only(bottom: 10.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
!(data.isSelf ?? false)
2025-09-03 11:25:31 +08:00
? GestureDetector(
onTap: () {
// 头像点击事件
logger.e("点击了头像");
Get.toNamed('/vloger', arguments: {'memberId': data.sender});
// 你可以在这里处理点击事件,例如打开用户详情页
},
child: SizedBox(
height: 35.0,
width: 35.0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
child: NetworkOrAssetImage(imageUrl: data.faceUrl),
2025-08-21 10:50:38 +08:00
),
),
)
: const SizedBox.shrink(),
Expanded(
child: Padding(
padding: !(data.isSelf ?? false) ? const EdgeInsets.only(left: 10.0, right: 40.0) : const EdgeInsets.only(left: 40.0, right: 10.0),
child: Column(
crossAxisAlignment: !(data.isSelf ?? false) ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
2025-09-03 11:25:31 +08:00
// Text(
// displayName ?? '未知昵称',
// style: const TextStyle(color: Colors.grey, fontSize: 12.0),
// ),
// const SizedBox(
// height: 3.0,
// ),
2025-08-21 10:50:38 +08:00
Stack(
children: [
// 气泡箭头
/* Visibility(
// 显示箭头(消息+语音+红包+位置)
visible: [3, 7, 8, 9].contains(data['contentType']),
child: Positioned(
left: !data['isme'] ? 1 : null,
right: data['isme'] ? 1 : null,
top: 20.0,
child: RotatedBox(
quarterTurns: !data['isme'] ? 1 : -1,
child: CustomPaint(painter: ArrowShape(arrowColor: arrowColor(data))),
)
),
), */
Container(
child: child,
),
],
),
],
),
),
),
data.isSelf ?? false
? SizedBox(
height: 35.0,
width: 35.0,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
2025-09-03 11:25:31 +08:00
child: NetworkOrAssetImage(imageUrl: data.faceUrl),
2025-08-21 10:50:38 +08:00
),
)
: const SizedBox.shrink(),
],
),
);
}
}
// 绘制气泡箭头
class ArrowShape extends CustomPainter {
ArrowShape({
required this.arrowColor,
this.arrowSize = 7,
});
final Color arrowColor; // 箭头颜色
final double arrowSize; // 箭头大小
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()..color = arrowColor;
var path = Path();
path.lineTo(-arrowSize, 0);
path.lineTo(0, arrowSize);
path.lineTo(arrowSize, 0);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}