flutter/lib/pages/chat/index.dart
2025-09-13 17:01:01 +08:00

539 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 聊天首页模板
library;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/global_badge.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/components/scan_util.dart';
import 'package:loopin/models/conversation_type.dart';
import 'package:loopin/models/conversation_view_model.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举
import 'package:loopin/utils/parse_message_summary.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart';
import '../../behavior/custom_scroll_behavior.dart';
import '../../styles/index.dart';
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => ChatPageState();
}
class ChatPageState extends State<ChatPage> {
late final ChatController controller;
@override
void initState() {
super.initState();
controller = Get.find<ChatController>();
}
void deletConv(context, ConversationViewModel item) async {
final res = await ImService.instance.deleteConversation(conversationID: item.conversation.conversationID);
if (res.success) {
Navigator.of(context).pop();
controller.chatList.remove(item);
}
}
// 长按坐标点
double posDX = 0.0;
double posDY = 0.0;
// 下拉刷新
Future<void> handleRefresh() async {
await Future.delayed(Duration(seconds: 1));
setState(() {});
}
// 处理扫码结果
void handleScanResult(String code) {
print('扫码结果11111111111111111111111$code');
// 使用外部枚举的方法检查扫码类型
final scanType = ScanCodeType.fromCode(code);
if (scanType != null) {
final value = code.substring(scanType.prefix.length + 1); // 获取后缀值
_processScanCode(scanType, value);
} else {
// 未知类型的码
Get.snackbar('未知的二维码类型', '无法识别此二维码: $code');
}
}
// 处理不同类型的扫码结果
void _processScanCode(ScanCodeType type, String value) {
switch (type) {
case ScanCodeType.verification:
_handleVerificationCode(value);
break;
case ScanCodeType.friend:
_handleFriendCode(value);
break;
case ScanCodeType.promotion:
_handlePromotionCode(value);
break;
}
}
// 处理核销码
void _handleVerificationCode (String value) async {
print('处理核销码: $value');
// 带着核销码,跳转到商家的商品详情页面,引导商家去手动点击核销按钮
Get.toNamed('/sellerOrder/detail', arguments: {'writeOffCodeId':value});
}
// 处理好友码
void _handleFriendCode(String value) {
print('处理好友码: $value');
// 模仿抖音,去个人页面手动点击关注
Get.toNamed('/vloger', arguments: {'memberId':value});
}
// 处理推广码
void _handlePromotionCode(String value) {
print('处理推广码: $value');
Get.toNamed('/vloger', arguments: {'memberId':value});
}
// 长按菜单
void showContextMenu(BuildContext context, ConversationViewModel item) {
bool isLeft = posDX > MediaQuery.of(context).size.width / 2 ? false : true;
bool isTop = posDY > MediaQuery.of(context).size.height / 2 ? false : true;
showDialog(
context: context,
barrierColor: Colors.black12, // 遮罩透明
builder: (context) {
return Stack(
children: [
Positioned(
top: isTop ? posDY : posDY - 135,
left: isLeft ? posDX : posDX - 135,
width: 135,
child: Material(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
color: Colors.white,
elevation: 2.0,
clipBehavior: Clip.hardEdge,
child: Column(
children: [
ListTile(
title: const Text(
'设为免打扰',
style: TextStyle(color: Colors.black87, fontSize: 14.0),
),
dense: true,
onTap: () {},
),
ListTile(
title: const Text(
'置顶消息',
style: TextStyle(color: Colors.black87, fontSize: 14.0),
),
dense: true,
onTap: () {},
),
ListTile(
title: const Text(
'不显示该消息',
style: TextStyle(color: Colors.black87, fontSize: 14.0),
),
dense: true,
onTap: () {},
),
ListTile(
title: const Text(
'删除',
style: TextStyle(color: Colors.black87, fontSize: 14.0),
),
dense: true,
onTap: () {
deletConv(context, item);
},
)
],
),
),
)
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
forceMaterialTransparency: true,
title: Row(
spacing: 8.0,
children: [
Text('消息'),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.0), boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(20),
offset: Offset(0.0, 1.0),
blurRadius: 2.0,
spreadRadius: 0.0,
),
]),
child: InkWell(
onTap: () async {
if (Get.find<GlobalBadge>().totalUnread > 0) {
final res = await ImService.instance.clearConversationUnreadCount(conversationID: '');
if (res.success) {
MyDialog.toast('操作成功', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200)));
} else {
MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
}
}
},
child: Row(
spacing: 3.0,
children: [
Icon(
Icons.cleaning_services_sharp,
size: 14.0,
),
Text(
'清除未读',
style: TextStyle(fontSize: 12.0),
)
],
),
)),
],
),
actions: [
/// 先不做搜索功能了后面再说
// IconButton(
// icon: const Icon(Icons.search),
// onPressed: () {},
// ),
IconButton(
icon: const Icon(Icons.add_circle_outline),
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: 'group',
child: Row(
children: [
Icon(Icons.group, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'发起群聊',
style: TextStyle(color: Colors.white),
),
],
),
),
PopupMenuItem<String>(
value: 'friend',
child: Row(
children: [
Icon(Icons.person_add, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'添加朋友',
style: TextStyle(color: Colors.white),
),
],
),
),
PopupMenuItem<String>(
value: 'scan',
child: Row(
children: [
Icon(Icons.qr_code_scanner, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'扫一扫',
style: TextStyle(color: Colors.white),
),
],
),
),
],
);
if (selected != null) {
switch (selected) {
case 'group':
logger.w('点击了发起群聊');
Get.toNamed('/group');
break;
case 'friend':
logger.w('点击了添加朋友');
break;
case 'scan':
logger.w('点击了扫一扫');
ScanUtil.openScanner(onResult: handleScanResult);
break;
}
}
},
),
],
),
body: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: Column(
children: [
Container(
margin: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(10),
offset: Offset(0.0, 1.0),
blurRadius: 2.0,
spreadRadius: 0.0,
),
]),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
GestureDetector(
onTap: () {
// 去群聊列表
Get.toNamed('/groupList');
},
child: Column(
children: [
SvgPicture.asset(
'assets/images/svg/order.svg',
height: 36.0,
width: 36.0,
),
Text('群聊'),
],
),
),
GestureDetector(
onTap: () {
//互关
Get.toNamed('/eachFlow');
},
child: Column(
children: [
SvgPicture.asset(
'assets/images/svg/kefu.svg',
height: 36.0,
width: 36.0,
),
Text('互关'),
],
),
),
GestureDetector(
onTap: () {
// 粉丝
Get.toNamed('/fans');
},
child: Column(
children: [
SvgPicture.asset(
'assets/images/svg/comment.svg',
height: 36.0,
width: 36.0,
),
Text('粉丝'),
],
),
),
GestureDetector(
onTap: () {
// 关注
Get.toNamed('/flow');
},
child: Column(
children: [
SvgPicture.asset(
'assets/images/svg/comment.svg',
height: 36.0,
width: 36.0,
),
Text('关注'),
],
),
),
],
),
),
Expanded(
child: Obx(
() {
final chatList = controller.chatList;
return ListView.builder(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemCount: chatList.length,
itemBuilder: (context, index) {
// logger.w(chatList[index].conversation.conversationGroupList);
// logger.w(chatList[index].isCustomAdmin);
// logger.w(chatList[index].conversation.recvOpt);
final bool quiet = [ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At].contains(chatList[index].conversation.recvOpt ?? 0)
? true
: false; // 是否设置了免打扰
final isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false;
final isAdmin =
chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0';
final placeholderAsset = chatList[index].conversation.type == 2 ? 'assets/images/group.png' : 'assets/images/avatar/default.png';
// logger.e(chatList[index].isCustomAdmin);
return Ink(
// color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色
child: InkWell(
key: ValueKey(chatList[index].conversation.conversationID),
splashColor: Colors.grey[200],
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
child: Row(
spacing: 10.0,
children: <Widget>[
// 头图
ClipOval(
child: NetworkOrAssetImage(
imageUrl: chatList[index].faceUrl,
width: 50,
height: 50,
placeholderAsset: placeholderAsset,
),
),
// 消息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 昵称
Text(
chatList[index].conversation.showName ?? '未知',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: (isAdmin || isNoFriend) ? 20 : 16,
fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal),
),
const SizedBox(height: 2.0),
// 消息内容
Text(
chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '',
style: const TextStyle(color: Colors.grey, fontSize: 13.0),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
],
),
),
// 右侧
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Visibility(
visible: !(isAdmin || isNoFriend),
child: Text(
// 转成日期字符串显示
// DateTime.fromMillisecondsSinceEpoch(
// (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000,
// ).toLocal().toString().substring(0, 16), // 简单截取年月日时分
Utils.formatTime(chatList[index].conversation.lastMessage!.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000),
style: const TextStyle(color: Colors.grey, fontSize: 12.0),
),
),
const SizedBox(height: 5.0),
// 数字角标
// class ReceiveMsgOptType {
// 在线正常接收消息,离线时会进行 APNs 推送
// static const int kTIMRecvMsgOpt_Receive = 0;
// 不会接收到消息,离线不会有推送通知
// static const int kTIMRecvMsgOpt_Not_Receive = 1;
// 在线正常接收消息,离线不会有推送通知
// static const int kTIMRecvMsgOpt_Not_Notify = 2;
// 在线接收消息,离线只接收 at 消息的推送
// static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3;
// 在线和离线都只接收@消息
// static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4;
// }
// 现阶段只允许设置为0和3
Visibility(
visible: (chatList[index].conversation.unreadCount ?? 0) > 0,
child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0, color: quiet ? Colors.grey : Colors.red),
),
],
),
Visibility(
visible: (isAdmin || isNoFriend),
child: const Icon(
Icons.arrow_forward_ios,
color: Colors.blueGrey,
size: 14.0,
),
),
],
),
),
onTap: () {
if (conversationTypeFromString(chatList[index].isCustomAdmin) != null) {
// 跳转对应的通知消息页
logger.e(chatList[index].isCustomAdmin);
Get.toNamed('/${chatList[index].isCustomAdmin}', arguments: chatList[index].conversation.lastMessage);
} else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) {
// 跳转陌生人消息页面
logger.e(chatList[index].conversation.conversationGroupList);
Get.toNamed('/noFriend');
} else if (chatList[index].conversation.type == 2) {
// 跳转群聊 type=0非法1=单聊2=群聊
Get.toNamed('/chatGroup', arguments: chatList[index].conversation);
} else {
// 会话id查询会话详情
Get.toNamed('/chat', arguments: chatList[index].conversation);
}
},
onTapDown: (TapDownDetails details) {
posDX = details.globalPosition.dx;
posDY = details.globalPosition.dy;
},
onLongPress: () {
showContextMenu(context, chatList[index]);
},
),
);
},
);
},
),
),
],
),
),
);
}
}