feat:增加消息记录功能

This commit is contained in:
2026-06-19 21:10:03 +08:00
parent bb1160ad18
commit 5d12377606
23 changed files with 458 additions and 170 deletions

View File

@@ -1,27 +1,22 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:sweet_chat_app/models/ImMessage.dart';
import 'package:sweet_chat_app/apis/message_api.dart';
import 'package:sweet_chat_app/models/chat_message.dart';
import 'package:sweet_chat_app/models/contact_info.dart';
import 'package:sweet_chat_app/models/im_message.dart';
import 'package:sweet_chat_app/services/webSocket_service.dart';
import 'package:sweet_chat_app/utils/SpUtil.dart';
class ChatMessage {
final String text;
final bool isMe;
final String? avatar;
ChatMessage({required this.text, required this.isMe, this.avatar});
}
import 'package:sweet_chat_app/utils/sp_utils.dart';
class ChatPage extends StatefulWidget {
final int contractId;
final String contactName;
final String avatarUrl;
final ContactInfo contactInfo;
final int conversationId;
const ChatPage({
super.key,
required this.contractId,
required this.contactName,
required this.avatarUrl,
required this.contactInfo,
required this.conversationId,
});
@override
@@ -29,79 +24,93 @@ class ChatPage extends StatefulWidget {
}
class _ChatPageState extends State<ChatPage> {
final TextEditingController _controller = TextEditingController();
late StreamSubscription<String> _msgSub;
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
late int userId;
late String avatarUrl;
late List<ChatMessage> _messages;
@override
void initState() {
super.initState();
_messages = [];
userId = SpUtil.getInt('userId') ?? 0;
final online = ImMessage(
type: "ONLINE",
from: userId.toString(),
to: 'SYSTEM',
content: '',
);
avatarUrl = SpUtil.getString('avatarUrl') ?? '';
_messages = [];
_loadChatMessage();
WebSocketService().connect('ws://172.29.100.146:5050?userId=$userId');
WebSocketService().send(online.toJsonString());
WebSocketService().messages.listen((wsMsg) {
final ImMessage imMsg = ImMessage.fromJsonString(wsMsg);
setState(() {
if (imMsg.type.contains('CHAT')) {
_messages.add(
ChatMessage(
text: imMsg.content,
isMe: userId.toString() == imMsg.from,
avatar: widget.avatarUrl,
),
);
}
});
_controller.clear();
_msgSub = WebSocketService().messages.listen((websocketMessage) {
_handleMessage(websocketMessage);
_textController.clear();
scrollToBottom();
});
}
Future<void> _loadChatMessage() async {
final list = await getChatMessageApi(widget.conversationId);
if (!mounted) return;
setState(() {
_messages = list;
});
}
@override
void dispose() {
_msgSub.cancel();
_textController.dispose();
_scrollController.dispose();
super.dispose();
WebSocketService().disconnect();
}
void _sendMessage() {
final text = _controller.text.trim();
final text = _textController.text.trim();
if (text.isEmpty) return;
final chat = ImMessage(
type: "CHAT_PRIVATE",
type: ImMessageType.chatPrivate,
from: userId.toString(),
to: widget.contractId.toString(),
to: widget.contactInfo.id.toString(),
content: text,
);
WebSocketService().send(chat.toJsonString());
}
Widget _buildAvatar(String? url) {
bool checkIsMe(ChatMessage message) {
return message.senderId == userId;
}
void _handleMessage(String wsMsg) {
if (!mounted) return;
final ImMessage imMsg = ImMessage.fromJsonString(wsMsg);
setState(() {
if (imMsg.type.value.contains('CHAT')) {
_messages.add(
ChatMessage(
msgId: DateTime.now().millisecondsSinceEpoch,
senderId: userId,
content: imMsg.content,
msgType: 1,
createTime: DateTime.now(),
),
);
}
});
}
Widget _buildAvatar(String url) {
return Padding(
padding: const EdgeInsets.only(right: 8, top: 4),
child: CircleAvatar(
radius: 18,
backgroundColor: Colors.grey[200],
backgroundImage: url != null ? NetworkImage(url) : null,
child:
url == null
? const Icon(Icons.person, size: 18, color: Colors.grey)
: null,
backgroundImage: NetworkImage(url),
),
);
}
@@ -130,7 +139,7 @@ class _ChatPageState extends State<ChatPage> {
backgroundColor: Color(0xFF120D25),
foregroundColor: Colors.white,
title: Text(
widget.contactName,
widget.contactInfo.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
@@ -154,14 +163,16 @@ class _ChatPageState extends State<ChatPage> {
return Row(
mainAxisAlignment:
msg.isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
checkIsMe(msg)
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!msg.isMe) _buildAvatar(msg.avatar),
if (!checkIsMe(msg)) _buildAvatar(widget.contactInfo.avatar),
Flexible(
child: Container(
decoration: BoxDecoration(
color: msg.isMe ? Color(0xFF2FA951): Color(0xFF030816),
color: checkIsMe(msg) ? Color(0xFF2FA951) : Color(0xFF030816),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
@@ -169,13 +180,13 @@ class _ChatPageState extends State<ChatPage> {
vertical: 10,
),
child: Text(
msg.text,
msg.content,
style: const TextStyle(fontSize: 16, color: Colors.white),
),
),
),
const SizedBox(width: 8),
if (msg.isMe) _buildAvatar("https://picsum.photos/id/1027/200"),
if (checkIsMe(msg)) _buildAvatar(avatarUrl),
],
);
},
@@ -192,7 +203,7 @@ class _ChatPageState extends State<ChatPage> {
children: [
Expanded(
child: TextField(
controller: _controller,
controller: _textController,
onSubmitted: (_) => _sendMessage(),
style: const TextStyle(color: Colors.white),
cursorColor: Colors.white,

View File

@@ -2,31 +2,12 @@ import 'package:azlistview/azlistview.dart';
import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:sweet_chat_app/apis/user_api.dart';
import 'package:sweet_chat_app/models/contact_info.dart';
import 'package:sweet_chat_app/models/user.dart';
import 'package:sweet_chat_app/utils/SpUtil.dart';
import 'package:sweet_chat_app/utils/sp_utils.dart';
import 'chat_page.dart';
class ContactInfo extends ISuspensionBean {
final int id;
final String name;
final String avatar;
String? tagIndex;
String? namePinyin;
ContactInfo({
required this.id,
required this.name,
required this.avatar,
this.tagIndex,
this.namePinyin,
});
@override
String getSuspensionTag() => tagIndex ?? '#';
}
/// ================= Page =================
class ContactsPage extends StatefulWidget {
const ContactsPage({super.key});
@@ -163,14 +144,7 @@ class _ContactsPageState extends State<ContactsPage> {
void onTapContact(ContactInfo contact) {
Navigator.push(
context,
MaterialPageRoute(
builder:
(_) => ChatPage(
contractId: contact.id,
contactName: contact.name,
avatarUrl: contact.avatar,
),
),
MaterialPageRoute(builder: (_) => ChatPage(contactInfo: contact, conversationId: 0)),
);
}
}

View File

@@ -2,9 +2,13 @@ import 'package:flutter/material.dart';
import 'package:liquid_glass_widgets/widgets/shared/glass_page.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart';
import 'package:sweet_chat_app/pages/contacts_page.dart';
import 'package:sweet_chat_app/config/AppConfig.dart';
import 'package:sweet_chat_app/models/im_message.dart';
import 'package:sweet_chat_app/pages/contact_page.dart';
import 'package:sweet_chat_app/pages/explore_page.dart';
import 'package:sweet_chat_app/pages/messages_page.dart';
import 'package:sweet_chat_app/pages/message_page.dart';
import 'package:sweet_chat_app/services/webSocket_service.dart';
import 'package:sweet_chat_app/utils/sp_utils.dart';
class HomePage extends StatefulWidget {
@@ -23,7 +27,24 @@ class _HomePageState extends State<HomePage> {
const ExplorePage(),
const Center(child: Text('⚙ 我的', style: TextStyle(fontSize: 26))),
];
@override
void initState() {
super.initState();
initWebsocket();
}
@override
void dispose() {
super.dispose();
}
void initWebsocket() {
final userId = SpUtil.getInt('userId') ?? 0;
WebSocketService().connect('${AppConfig.wsUrl}?userId=$userId');
WebSocketService().send(ImMessage.online(userId).toJsonString());
}
@override
Widget build(BuildContext context) {
return GlassScaffold(

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:sweet_chat_app/apis/auth_api.dart';
import 'package:sweet_chat_app/utils/SpUtil.dart';
import 'package:sweet_chat_app/utils/sp_utils.dart';
import 'package:sweet_chat_app/utils/toast_utils.dart';
import 'home_page.dart';
class LoginPage extends StatefulWidget {
@@ -18,30 +18,21 @@ class _LoginPageState extends State<LoginPage> {
void _login() async {
try {
final auth = await loginApi(_usernameController.text, _passwordController.text);
await SpUtil.setInt('userId', auth.userId);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const HomePage(),
),
final auth = await loginApi(
_usernameController.text,
_passwordController.text,
);
await SpUtil.setInt('userId', auth.user.id);
await SpUtil.setString('avatarUrl', auth.user.avatarUrl);
Fluttertoast.showToast(
msg: "登录成功",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.white,
textColor: Colors.black,
);
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const HomePage()));
ToastUtils.success("登录成功");
} catch (e) {
Fluttertoast.showToast(
msg: "登录失败",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.white,
textColor: Colors.black,
);
print(e.toString());
ToastUtils.error("登录失败");
}
}

View File

@@ -1,11 +1,70 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:sweet_chat_app/apis/message_api.dart';
import 'package:sweet_chat_app/models/chat_session.dart';
import 'package:sweet_chat_app/models/contact_info.dart';
import 'package:sweet_chat_app/pages/chat_page.dart';
import 'package:sweet_chat_app/services/webSocket_service.dart';
import 'package:sweet_chat_app/utils/date_utils.dart';
import 'package:sweet_chat_app/utils/sp_utils.dart';
import 'chat_page.dart';
class MessagePage extends StatelessWidget {
class MessagePage extends StatefulWidget {
const MessagePage({super.key});
@override
State<MessagePage> createState() => _MessagePageState();
}
class _MessagePageState extends State<MessagePage> {
late StreamSubscription<String> _msgSub;
late List<ChatSession> _chatSessionList;
@override
void initState() {
super.initState();
_chatSessionList = [];
_loadChatSession();
_msgSub = WebSocketService().messages.listen((websocketMessage) {});
}
@override
void dispose() {
_msgSub.cancel();
super.dispose();
}
Future<void> _loadChatSession() async {
final userId = SpUtil.getInt('userId') ?? 0;
final list = await getChatSessionApi(userId);
if (!mounted) return;
setState(() {
_chatSessionList = list;
});
}
void onTapSession(ChatSession chatSession) {
final ContactInfo contactInfo = ContactInfo(
id: chatSession.targetUserId,
name: chatSession.title,
avatar: chatSession.avatarUrl,
);
final ChatPage chatPage = ChatPage(
contactInfo: contactInfo,
conversationId: chatSession.conversationId,
);
Navigator.push(context, MaterialPageRoute(builder: (_) => chatPage)).then((
_,
) {
_loadChatSession();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -25,9 +84,9 @@ class MessagePage extends StatelessWidget {
icon: const Icon(Icons.menu, size: 22, color: Colors.white),
splashRadius: 20,
),
title: const Text(
'消息',
style: TextStyle(
title: Text(
'消息 ${WebSocketService().isConnected}' ,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
@@ -59,71 +118,42 @@ class MessagePage extends StatelessWidget {
separatorBuilder: (BuildContext context, int index) {
return SizedBox(height: 10);
},
itemCount: _messages.length,
itemCount: _chatSessionList.length,
itemBuilder: (context, index) {
final msg = _messages[index];
final msg = _chatSessionList[index];
return _buildMessageItem(context, msg);
},
);
}
Widget _buildMessageItem(BuildContext context, Message msg) {
Widget _buildMessageItem(BuildContext context, ChatSession session) {
return GlassCard(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
child: ListTile(
contentPadding: EdgeInsets.zero,
onTap:
() => {
Navigator.push(
context,
MaterialPageRoute(
builder:
(_) => const ChatPage(
contractId: 0,
contactName: '张三',
avatarUrl: 'https://picsum.photos/id/1012/200',
),
),
),
},
onTap: () => onTapSession(session),
leading: CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(msg.avatar),
backgroundImage: NetworkImage(session.avatarUrl),
),
title: Text(
msg.name,
session.title,
style: const TextStyle(
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
subtitle: Text(
msg.lastMsg,
session.lastMessageContent,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13, color: Color(0xFFAEAEB2)),
),
trailing: Text(
msg.time,
formatChatTime(session.lastMessageTime),
style: const TextStyle(fontSize: 12, color: Color(0xFF8E8E93)),
),
),
);
}
}
class Message {
final String name;
final String avatar;
final String lastMsg;
final String time;
Message({
required this.name,
required this.avatar,
required this.lastMsg,
required this.time,
});
}
List<Message> _messages = [];