From 5d1237760651708c7043df6b7845a425e5b0c158 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Fri, 19 Jun 2026 21:10:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/apis/message_api.dart | 19 +++ lib/config/AppConfig.dart | 4 + lib/main.dart | 2 +- lib/models/auth.dart | 5 +- lib/models/auth.g.dart | 4 +- lib/models/chat_message.dart | 26 ++++ lib/models/chat_message.g.dart | 24 ++++ lib/models/chat_session.dart | 30 ++++ lib/models/chat_session.g.dart | 30 ++++ lib/models/contact_info.dart | 20 +++ .../{ImMessage.dart => im_message.dart} | 30 +++- lib/pages/chat_page.dart | 133 ++++++++++-------- .../{contacts_page.dart => contact_page.dart} | 32 +---- lib/pages/home_page.dart | 25 +++- lib/pages/login.dart | 37 ++--- .../{messages_page.dart => message_page.dart} | 116 +++++++++------ lib/services/dio_service.dart | 5 +- lib/services/message_store.dart | 13 ++ lib/utils/date_utils.dart | 26 ++++ lib/utils/{SpUtil.dart => sp_utils.dart} | 0 lib/utils/toast_utils.dart | 37 +++++ pubspec.lock | 8 ++ pubspec.yaml | 2 +- 23 files changed, 458 insertions(+), 170 deletions(-) create mode 100644 lib/apis/message_api.dart create mode 100644 lib/config/AppConfig.dart create mode 100644 lib/models/chat_message.dart create mode 100644 lib/models/chat_message.g.dart create mode 100644 lib/models/chat_session.dart create mode 100644 lib/models/chat_session.g.dart create mode 100644 lib/models/contact_info.dart rename lib/models/{ImMessage.dart => im_message.dart} (55%) rename lib/pages/{contacts_page.dart => contact_page.dart} (85%) rename lib/pages/{messages_page.dart => message_page.dart} (51%) create mode 100644 lib/services/message_store.dart create mode 100644 lib/utils/date_utils.dart rename lib/utils/{SpUtil.dart => sp_utils.dart} (100%) create mode 100644 lib/utils/toast_utils.dart diff --git a/lib/apis/message_api.dart b/lib/apis/message_api.dart new file mode 100644 index 0000000..0eaf6e8 --- /dev/null +++ b/lib/apis/message_api.dart @@ -0,0 +1,19 @@ +import 'package:sweet_chat_app/models/chat_message.dart'; +import 'package:sweet_chat_app/models/chat_session.dart'; +import 'package:sweet_chat_app/services/dio_service.dart'; + +Future> getChatSessionApi(int userId) async { + final res = await dioService.get('/message/session/$userId'); + + if (res.data == null) return []; + + return (res.data as List).map((e) => ChatSession.fromJson(e)).toList(); +} + +Future> getChatMessageApi(int conversationId) async { + final res = await dioService.get('/message/session/$conversationId/detail'); + + if (res.data == null) return []; + + return (res.data as List).map((e) => ChatMessage.fromJson(e)).toList(); +} diff --git a/lib/config/AppConfig.dart b/lib/config/AppConfig.dart new file mode 100644 index 0000000..f828ad2 --- /dev/null +++ b/lib/config/AppConfig.dart @@ -0,0 +1,4 @@ +class AppConfig { + static const String baseUrl = 'http://192.168.31.108:2836/api'; + static const String wsUrl = 'ws://192.168.31.108:5050'; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8c6141a..d0a1e9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; import 'package:sweet_chat_app/pages/login.dart'; -import 'package:sweet_chat_app/utils/SpUtil.dart'; +import 'package:sweet_chat_app/utils/sp_utils.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/models/auth.dart b/lib/models/auth.dart index 736f2ab..84d45c9 100644 --- a/lib/models/auth.dart +++ b/lib/models/auth.dart @@ -1,12 +1,13 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:sweet_chat_app/models/user.dart'; part 'auth.g.dart'; @JsonSerializable() class Auth { - final int userId; + final User user; - Auth({required this.userId}); + Auth({required this.user}); factory Auth.fromJson(Map json) => _$AuthFromJson(json); diff --git a/lib/models/auth.g.dart b/lib/models/auth.g.dart index 93069f2..dbc8682 100644 --- a/lib/models/auth.g.dart +++ b/lib/models/auth.g.dart @@ -7,8 +7,8 @@ part of 'auth.dart'; // ************************************************************************** Auth _$AuthFromJson(Map json) => - Auth(userId: (json['userId'] as num).toInt()); + Auth(user: User.fromJson(json['user'] as Map)); Map _$AuthToJson(Auth instance) => { - 'userId': instance.userId, + 'user': instance.user, }; diff --git a/lib/models/chat_message.dart b/lib/models/chat_message.dart new file mode 100644 index 0000000..2eb4261 --- /dev/null +++ b/lib/models/chat_message.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'chat_message.g.dart'; + +@JsonSerializable() +class ChatMessage { + final int msgId; + final int senderId; + final String content; + /// 消息类型(1文本 2图片 3语音) + final int msgType; + final DateTime createTime; + + ChatMessage({ + required this.msgId, + required this.senderId, + required this.content, + required this.msgType, + required this.createTime, + }); + + factory ChatMessage.fromJson(Map json) => + _$ChatMessageFromJson(json); + + Map toJson() => _$ChatMessageToJson(this); +} \ No newline at end of file diff --git a/lib/models/chat_message.g.dart b/lib/models/chat_message.g.dart new file mode 100644 index 0000000..c05433f --- /dev/null +++ b/lib/models/chat_message.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_message.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChatMessage _$ChatMessageFromJson(Map json) => ChatMessage( + msgId: (json['msgId'] as num).toInt(), + senderId: (json['senderId'] as num).toInt(), + content: json['content'] as String, + msgType: (json['msgType'] as num).toInt(), + createTime: DateTime.parse(json['createTime'] as String), +); + +Map _$ChatMessageToJson(ChatMessage instance) => + { + 'msgId': instance.msgId, + 'senderId': instance.senderId, + 'content': instance.content, + 'msgType': instance.msgType, + 'createTime': instance.createTime.toIso8601String(), + }; diff --git a/lib/models/chat_session.dart b/lib/models/chat_session.dart new file mode 100644 index 0000000..18d3076 --- /dev/null +++ b/lib/models/chat_session.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'chat_session.g.dart'; + +@JsonSerializable() +class ChatSession { + final int conversationId; + final int targetUserId; + final String title; + final String avatarUrl; + final int msgType; + final String lastMessageContent; + final DateTime lastMessageTime; + final int unreadCount; + + ChatSession({ + required this.conversationId, + required this.targetUserId, + required this.title, + required this.avatarUrl, + required this.msgType, + required this.lastMessageContent, + required this.lastMessageTime, + required this.unreadCount, + }); + + factory ChatSession.fromJson(Map json) => _$ChatSessionFromJson(json); + + Map toJson() => _$ChatSessionToJson(this); +} \ No newline at end of file diff --git a/lib/models/chat_session.g.dart b/lib/models/chat_session.g.dart new file mode 100644 index 0000000..8150779 --- /dev/null +++ b/lib/models/chat_session.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_session.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChatSession _$ChatSessionFromJson(Map json) => ChatSession( + conversationId: (json['conversationId'] as num).toInt(), + targetUserId: (json['targetUserId'] as num).toInt(), + title: json['title'] as String, + avatarUrl: json['avatarUrl'] as String, + msgType: (json['msgType'] as num).toInt(), + lastMessageContent: json['lastMessageContent'] as String, + lastMessageTime: DateTime.parse(json['lastMessageTime'] as String), + unreadCount: (json['unreadCount'] as num).toInt(), +); + +Map _$ChatSessionToJson(ChatSession instance) => + { + 'conversationId': instance.conversationId, + 'targetUserId': instance.targetUserId, + 'title': instance.title, + 'avatarUrl': instance.avatarUrl, + 'msgType': instance.msgType, + 'lastMessageContent': instance.lastMessageContent, + 'lastMessageTime': instance.lastMessageTime.toIso8601String(), + 'unreadCount': instance.unreadCount, + }; diff --git a/lib/models/contact_info.dart b/lib/models/contact_info.dart new file mode 100644 index 0000000..1df542e --- /dev/null +++ b/lib/models/contact_info.dart @@ -0,0 +1,20 @@ +import 'package:azlistview/azlistview.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 ?? '#'; +} \ No newline at end of file diff --git a/lib/models/ImMessage.dart b/lib/models/im_message.dart similarity index 55% rename from lib/models/ImMessage.dart rename to lib/models/im_message.dart index ad63b9c..d8ee825 100644 --- a/lib/models/ImMessage.dart +++ b/lib/models/im_message.dart @@ -1,7 +1,20 @@ import 'dart:convert'; +enum ImMessageType { + chatPrivate('CHAT_PRIVATE'), + chatGroup('CHAT_GROUP'), + online('ONLINE'), + heartbeat('HEARTBEAT'), + system('SYSTEM'), + notice('NOTICE'); + + final String value; + + const ImMessageType(this.value); +} + class ImMessage { - final String type; + final ImMessageType type; final String from; final String to; final String content; @@ -19,7 +32,7 @@ class ImMessage { factory ImMessage.fromJson(Map json) { return ImMessage( - type: json['type'], + type: ImMessageType.values.firstWhere((e) => e.value == json['type']), from: json['from'], to: json['to'], content: json['content'], @@ -27,7 +40,7 @@ class ImMessage { } Map toJson() => { - 'type': type, + 'type': type.value, 'from': from, 'to': to, 'content': content, @@ -36,4 +49,13 @@ class ImMessage { String toJsonString() { return jsonEncode(toJson()); } -} \ No newline at end of file + + static ImMessage online(int userId) { + return ImMessage( + type: ImMessageType.online, + from: userId.toString(), + to: 'SYSTEM', + content: '', + ); + } +} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 8cfba56..e38a0ef 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -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 { - final TextEditingController _controller = TextEditingController(); + late StreamSubscription _msgSub; + final TextEditingController _textController = TextEditingController(); final ScrollController _scrollController = ScrollController(); late int userId; + late String avatarUrl; late List _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 _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 { 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 { 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 { 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 { children: [ Expanded( child: TextField( - controller: _controller, + controller: _textController, onSubmitted: (_) => _sendMessage(), style: const TextStyle(color: Colors.white), cursorColor: Colors.white, diff --git a/lib/pages/contacts_page.dart b/lib/pages/contact_page.dart similarity index 85% rename from lib/pages/contacts_page.dart rename to lib/pages/contact_page.dart index 5bf9804..493701a 100644 --- a/lib/pages/contacts_page.dart +++ b/lib/pages/contact_page.dart @@ -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 { 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)), ); } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index afde912..a6e1e03 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -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 { 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( diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 58f947a..d6b6284 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -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 { 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("登录失败"); } } diff --git a/lib/pages/messages_page.dart b/lib/pages/message_page.dart similarity index 51% rename from lib/pages/messages_page.dart rename to lib/pages/message_page.dart index 5ba2e31..9081f1c 100644 --- a/lib/pages/messages_page.dart +++ b/lib/pages/message_page.dart @@ -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 createState() => _MessagePageState(); +} + +class _MessagePageState extends State { + late StreamSubscription _msgSub; + late List _chatSessionList; + + @override + void initState() { + super.initState(); + _chatSessionList = []; + _loadChatSession(); + _msgSub = WebSocketService().messages.listen((websocketMessage) {}); + } + + @override + void dispose() { + _msgSub.cancel(); + super.dispose(); + } + + Future _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 _messages = []; diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index 0091cf7..1ceac7e 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -1,7 +1,8 @@ import 'package:dio/dio.dart'; +import 'package:sweet_chat_app/config/AppConfig.dart'; final dioService = Dio(BaseOptions( - baseUrl: 'http://172.29.100.146:2836/api', - connectTimeout: Duration(seconds: 10), + baseUrl: AppConfig.baseUrl, + connectTimeout: Duration(seconds: 5), headers: {'Content-Type': 'application/json'}, )); \ No newline at end of file diff --git a/lib/services/message_store.dart b/lib/services/message_store.dart new file mode 100644 index 0000000..7d917c2 --- /dev/null +++ b/lib/services/message_store.dart @@ -0,0 +1,13 @@ +import 'package:sweet_chat_app/models/im_message.dart'; + +class MessageStore { + static final Map> _data = {}; + + static void add(ImMessage msg) { + _data.putIfAbsent(msg.to, () => []).add(msg); + _data.putIfAbsent(msg.from, () => []).add(msg); + } + + static List messagesFor(String userId) => + _data[userId] ?? []; +} \ No newline at end of file diff --git a/lib/utils/date_utils.dart b/lib/utils/date_utils.dart new file mode 100644 index 0000000..0ae1ae0 --- /dev/null +++ b/lib/utils/date_utils.dart @@ -0,0 +1,26 @@ +import 'package:intl/intl.dart'; + +String formatChatTime(DateTime time) { + final now = DateTime.now(); + final t = time.toLocal(); + + final today = DateTime(now.year, now.month, now.day); + final d = DateTime(t.year, t.month, t.day); + + final diff = today.difference(d).inDays; + final hm = DateFormat('HH:mm').format(t); + + if (diff == 0) return hm; + if (diff == 1) return '昨天 $hm'; + + // 本周 + if (diff < 7 && d.weekday <= now.weekday) { + return '${DateFormat('EEEEE', 'zh_CN').format(t)} $hm'; + } + + if (t.year == now.year) { + return DateFormat('M月d日 HH:mm').format(t); + } + + return DateFormat('yyyy年M月d日 HH:mm').format(t); +} \ No newline at end of file diff --git a/lib/utils/SpUtil.dart b/lib/utils/sp_utils.dart similarity index 100% rename from lib/utils/SpUtil.dart rename to lib/utils/sp_utils.dart diff --git a/lib/utils/toast_utils.dart b/lib/utils/toast_utils.dart new file mode 100644 index 0000000..ad5f75a --- /dev/null +++ b/lib/utils/toast_utils.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class ToastUtils { + static void _show({ + required String msg, + Color backgroundColor = Colors.black87, + Color textColor = Colors.white, + ToastGravity gravity = ToastGravity.CENTER, + Toast toastLength = Toast.LENGTH_SHORT, + }) { + Fluttertoast.showToast( + msg: msg, + toastLength: toastLength, + gravity: gravity, + backgroundColor: backgroundColor, + textColor: textColor, + fontSize: 14.0, + ); + } + + static void success(String msg) { + _show(msg: msg, backgroundColor: Colors.green); + } + + static void error(String msg) { + _show(msg: msg, backgroundColor: Colors.redAccent); + } + + static void warning(String msg) { + _show(msg: msg, backgroundColor: Colors.orange); + } + + static void info(String msg) { + _show(msg: msg, backgroundColor: Colors.grey[800]!); + } +} diff --git a/pubspec.lock b/pubspec.lock index fcd58d5..4f55935 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,6 +288,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65d3b38..af85171 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1 +1 @@ -name: sweet_chat_app description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.7.0 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 liquid_glass_widgets: ^0.16.1 azlistview: ^2.0.0 lpinyin: ^2.0.3 web_socket_channel: ^3.0.3 dio: ^5.7.0 json_annotation: ^4.9.0 fluttertoast: ^8.2.4 shared_preferences: ^2.5.5 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.8 json_serializable: ^6.8.0 flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package \ No newline at end of file +name: sweet_chat_app description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.7.0 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 liquid_glass_widgets: ^0.16.1 azlistview: ^2.0.0 lpinyin: ^2.0.3 web_socket_channel: ^3.0.3 dio: ^5.7.0 json_annotation: ^4.9.0 fluttertoast: ^8.2.4 shared_preferences: ^2.5.5 intl: ^0.19.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.8 json_serializable: ^6.8.0 flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package \ No newline at end of file