feat:增加消息记录功能
This commit is contained in:
19
lib/apis/message_api.dart
Normal file
19
lib/apis/message_api.dart
Normal file
@@ -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<List<ChatSession>> 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<List<ChatMessage>> 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();
|
||||||
|
}
|
||||||
4
lib/config/AppConfig.dart
Normal file
4
lib/config/AppConfig.dart
Normal file
@@ -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';
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
import 'package:sweet_chat_app/pages/login.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 {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:sweet_chat_app/models/user.dart';
|
||||||
|
|
||||||
part 'auth.g.dart';
|
part 'auth.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Auth {
|
class Auth {
|
||||||
final int userId;
|
final User user;
|
||||||
|
|
||||||
Auth({required this.userId});
|
Auth({required this.user});
|
||||||
|
|
||||||
factory Auth.fromJson(Map<String, dynamic> json) => _$AuthFromJson(json);
|
factory Auth.fromJson(Map<String, dynamic> json) => _$AuthFromJson(json);
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ part of 'auth.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Auth _$AuthFromJson(Map<String, dynamic> json) =>
|
Auth _$AuthFromJson(Map<String, dynamic> json) =>
|
||||||
Auth(userId: (json['userId'] as num).toInt());
|
Auth(user: User.fromJson(json['user'] as Map<String, dynamic>));
|
||||||
|
|
||||||
Map<String, dynamic> _$AuthToJson(Auth instance) => <String, dynamic>{
|
Map<String, dynamic> _$AuthToJson(Auth instance) => <String, dynamic>{
|
||||||
'userId': instance.userId,
|
'user': instance.user,
|
||||||
};
|
};
|
||||||
|
|||||||
26
lib/models/chat_message.dart
Normal file
26
lib/models/chat_message.dart
Normal file
@@ -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<String, dynamic> json) =>
|
||||||
|
_$ChatMessageFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ChatMessageToJson(this);
|
||||||
|
}
|
||||||
24
lib/models/chat_message.g.dart
Normal file
24
lib/models/chat_message.g.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_message.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ChatMessage _$ChatMessageFromJson(Map<String, dynamic> 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<String, dynamic> _$ChatMessageToJson(ChatMessage instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'msgId': instance.msgId,
|
||||||
|
'senderId': instance.senderId,
|
||||||
|
'content': instance.content,
|
||||||
|
'msgType': instance.msgType,
|
||||||
|
'createTime': instance.createTime.toIso8601String(),
|
||||||
|
};
|
||||||
30
lib/models/chat_session.dart
Normal file
30
lib/models/chat_session.dart
Normal file
@@ -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<String, dynamic> json) => _$ChatSessionFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ChatSessionToJson(this);
|
||||||
|
}
|
||||||
30
lib/models/chat_session.g.dart
Normal file
30
lib/models/chat_session.g.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_session.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ChatSession _$ChatSessionFromJson(Map<String, dynamic> 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<String, dynamic> _$ChatSessionToJson(ChatSession instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'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,
|
||||||
|
};
|
||||||
20
lib/models/contact_info.dart
Normal file
20
lib/models/contact_info.dart
Normal file
@@ -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 ?? '#';
|
||||||
|
}
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
import 'dart:convert';
|
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 {
|
class ImMessage {
|
||||||
final String type;
|
final ImMessageType type;
|
||||||
final String from;
|
final String from;
|
||||||
final String to;
|
final String to;
|
||||||
final String content;
|
final String content;
|
||||||
@@ -19,7 +32,7 @@ class ImMessage {
|
|||||||
|
|
||||||
factory ImMessage.fromJson(Map<String, dynamic> json) {
|
factory ImMessage.fromJson(Map<String, dynamic> json) {
|
||||||
return ImMessage(
|
return ImMessage(
|
||||||
type: json['type'],
|
type: ImMessageType.values.firstWhere((e) => e.value == json['type']),
|
||||||
from: json['from'],
|
from: json['from'],
|
||||||
to: json['to'],
|
to: json['to'],
|
||||||
content: json['content'],
|
content: json['content'],
|
||||||
@@ -27,7 +40,7 @@ class ImMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'type': type,
|
'type': type.value,
|
||||||
'from': from,
|
'from': from,
|
||||||
'to': to,
|
'to': to,
|
||||||
'content': content,
|
'content': content,
|
||||||
@@ -36,4 +49,13 @@ class ImMessage {
|
|||||||
String toJsonString() {
|
String toJsonString() {
|
||||||
return jsonEncode(toJson());
|
return jsonEncode(toJson());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
static ImMessage online(int userId) {
|
||||||
|
return ImMessage(
|
||||||
|
type: ImMessageType.online,
|
||||||
|
from: userId.toString(),
|
||||||
|
to: 'SYSTEM',
|
||||||
|
content: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,22 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.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/services/webSocket_service.dart';
|
||||||
import 'package:sweet_chat_app/utils/SpUtil.dart';
|
import 'package:sweet_chat_app/utils/sp_utils.dart';
|
||||||
|
|
||||||
class ChatMessage {
|
|
||||||
final String text;
|
|
||||||
final bool isMe;
|
|
||||||
final String? avatar;
|
|
||||||
|
|
||||||
ChatMessage({required this.text, required this.isMe, this.avatar});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatPage extends StatefulWidget {
|
class ChatPage extends StatefulWidget {
|
||||||
final int contractId;
|
final ContactInfo contactInfo;
|
||||||
final String contactName;
|
final int conversationId;
|
||||||
final String avatarUrl;
|
|
||||||
|
|
||||||
const ChatPage({
|
const ChatPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.contractId,
|
required this.contactInfo,
|
||||||
required this.contactName,
|
required this.conversationId,
|
||||||
required this.avatarUrl,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,79 +24,93 @@ class ChatPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChatPageState extends State<ChatPage> {
|
class _ChatPageState extends State<ChatPage> {
|
||||||
final TextEditingController _controller = TextEditingController();
|
late StreamSubscription<String> _msgSub;
|
||||||
|
final TextEditingController _textController = TextEditingController();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
late int userId;
|
late int userId;
|
||||||
|
late String avatarUrl;
|
||||||
late List<ChatMessage> _messages;
|
late List<ChatMessage> _messages;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_messages = [];
|
|
||||||
|
|
||||||
userId = SpUtil.getInt('userId') ?? 0;
|
userId = SpUtil.getInt('userId') ?? 0;
|
||||||
final online = ImMessage(
|
avatarUrl = SpUtil.getString('avatarUrl') ?? '';
|
||||||
type: "ONLINE",
|
|
||||||
from: userId.toString(),
|
_messages = [];
|
||||||
to: 'SYSTEM',
|
_loadChatMessage();
|
||||||
content: '',
|
|
||||||
);
|
|
||||||
|
|
||||||
WebSocketService().connect('ws://172.29.100.146:5050?userId=$userId');
|
_msgSub = WebSocketService().messages.listen((websocketMessage) {
|
||||||
WebSocketService().send(online.toJsonString());
|
_handleMessage(websocketMessage);
|
||||||
|
_textController.clear();
|
||||||
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();
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadChatMessage() async {
|
||||||
|
final list = await getChatMessageApi(widget.conversationId);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_messages = list;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_msgSub.cancel();
|
||||||
|
_textController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
WebSocketService().disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendMessage() {
|
void _sendMessage() {
|
||||||
final text = _controller.text.trim();
|
final text = _textController.text.trim();
|
||||||
if (text.isEmpty) return;
|
if (text.isEmpty) return;
|
||||||
|
|
||||||
final chat = ImMessage(
|
final chat = ImMessage(
|
||||||
type: "CHAT_PRIVATE",
|
type: ImMessageType.chatPrivate,
|
||||||
from: userId.toString(),
|
from: userId.toString(),
|
||||||
to: widget.contractId.toString(),
|
to: widget.contactInfo.id.toString(),
|
||||||
content: text,
|
content: text,
|
||||||
);
|
);
|
||||||
WebSocketService().send(chat.toJsonString());
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8, top: 4),
|
padding: const EdgeInsets.only(right: 8, top: 4),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 18,
|
radius: 18,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
backgroundImage: url != null ? NetworkImage(url) : null,
|
backgroundImage: NetworkImage(url),
|
||||||
child:
|
|
||||||
url == null
|
|
||||||
? const Icon(Icons.person, size: 18, color: Colors.grey)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -130,7 +139,7 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
backgroundColor: Color(0xFF120D25),
|
backgroundColor: Color(0xFF120D25),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.contactName,
|
widget.contactInfo.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -154,14 +163,16 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
msg.isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
checkIsMe(msg)
|
||||||
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!msg.isMe) _buildAvatar(msg.avatar),
|
if (!checkIsMe(msg)) _buildAvatar(widget.contactInfo.avatar),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: msg.isMe ? Color(0xFF2FA951): Color(0xFF030816),
|
color: checkIsMe(msg) ? Color(0xFF2FA951) : Color(0xFF030816),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -169,13 +180,13 @@ class _ChatPageState extends State<ChatPage> {
|
|||||||
vertical: 10,
|
vertical: 10,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
msg.text,
|
msg.content,
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.white),
|
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
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: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _textController,
|
||||||
onSubmitted: (_) => _sendMessage(),
|
onSubmitted: (_) => _sendMessage(),
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
cursorColor: Colors.white,
|
cursorColor: Colors.white,
|
||||||
|
|||||||
@@ -2,31 +2,12 @@ import 'package:azlistview/azlistview.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lpinyin/lpinyin.dart';
|
import 'package:lpinyin/lpinyin.dart';
|
||||||
import 'package:sweet_chat_app/apis/user_api.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/models/user.dart';
|
||||||
import 'package:sweet_chat_app/utils/SpUtil.dart';
|
import 'package:sweet_chat_app/utils/sp_utils.dart';
|
||||||
|
|
||||||
import 'chat_page.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 {
|
class ContactsPage extends StatefulWidget {
|
||||||
const ContactsPage({super.key});
|
const ContactsPage({super.key});
|
||||||
|
|
||||||
@@ -163,14 +144,7 @@ class _ContactsPageState extends State<ContactsPage> {
|
|||||||
void onTapContact(ContactInfo contact) {
|
void onTapContact(ContactInfo contact) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (_) => ChatPage(contactInfo: contact, conversationId: 0)),
|
||||||
builder:
|
|
||||||
(_) => ChatPage(
|
|
||||||
contractId: contact.id,
|
|
||||||
contactName: contact.name,
|
|
||||||
avatarUrl: contact.avatar,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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/shared/glass_page.dart';
|
||||||
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
|
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
|
||||||
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.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/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 {
|
class HomePage extends StatefulWidget {
|
||||||
@@ -23,7 +27,24 @@ class _HomePageState extends State<HomePage> {
|
|||||||
const ExplorePage(),
|
const ExplorePage(),
|
||||||
const Center(child: Text('⚙ 我的', style: TextStyle(fontSize: 26))),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GlassScaffold(
|
return GlassScaffold(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
import 'package:sweet_chat_app/apis/auth_api.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';
|
import 'home_page.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -18,30 +18,21 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
void _login() async {
|
void _login() async {
|
||||||
try {
|
try {
|
||||||
final auth = await loginApi(_usernameController.text, _passwordController.text);
|
final auth = await loginApi(
|
||||||
await SpUtil.setInt('userId', auth.userId);
|
_usernameController.text,
|
||||||
|
_passwordController.text,
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => const HomePage(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
await SpUtil.setInt('userId', auth.user.id);
|
||||||
|
await SpUtil.setString('avatarUrl', auth.user.avatarUrl);
|
||||||
|
|
||||||
Fluttertoast.showToast(
|
Navigator.of(
|
||||||
msg: "登录成功",
|
context,
|
||||||
toastLength: Toast.LENGTH_SHORT,
|
).push(MaterialPageRoute(builder: (_) => const HomePage()));
|
||||||
gravity: ToastGravity.TOP,
|
|
||||||
backgroundColor: Colors.white,
|
ToastUtils.success("登录成功");
|
||||||
textColor: Colors.black,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Fluttertoast.showToast(
|
print(e.toString());
|
||||||
msg: "登录失败",
|
ToastUtils.error("登录失败");
|
||||||
toastLength: Toast.LENGTH_SHORT,
|
|
||||||
gravity: ToastGravity.TOP,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
textColor: Colors.black,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,70 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.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 StatefulWidget {
|
||||||
|
|
||||||
class MessagePage extends StatelessWidget {
|
|
||||||
const MessagePage({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -25,9 +84,9 @@ class MessagePage extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.menu, size: 22, color: Colors.white),
|
icon: const Icon(Icons.menu, size: 22, color: Colors.white),
|
||||||
splashRadius: 20,
|
splashRadius: 20,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'消息',
|
'消息 ${WebSocketService().isConnected}' ,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -59,71 +118,42 @@ class MessagePage extends StatelessWidget {
|
|||||||
separatorBuilder: (BuildContext context, int index) {
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
return SizedBox(height: 10);
|
return SizedBox(height: 10);
|
||||||
},
|
},
|
||||||
itemCount: _messages.length,
|
itemCount: _chatSessionList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final msg = _messages[index];
|
final msg = _chatSessionList[index];
|
||||||
return _buildMessageItem(context, msg);
|
return _buildMessageItem(context, msg);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageItem(BuildContext context, Message msg) {
|
Widget _buildMessageItem(BuildContext context, ChatSession session) {
|
||||||
return GlassCard(
|
return GlassCard(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
onTap:
|
onTap: () => onTapSession(session),
|
||||||
() => {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder:
|
|
||||||
(_) => const ChatPage(
|
|
||||||
contractId: 0,
|
|
||||||
contactName: '张三',
|
|
||||||
avatarUrl: 'https://picsum.photos/id/1012/200',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
backgroundImage: NetworkImage(msg.avatar),
|
backgroundImage: NetworkImage(session.avatarUrl),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
msg.name,
|
session.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
msg.lastMsg,
|
session.lastMessageContent,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 13, color: Color(0xFFAEAEB2)),
|
style: const TextStyle(fontSize: 13, color: Color(0xFFAEAEB2)),
|
||||||
),
|
),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
msg.time,
|
formatChatTime(session.lastMessageTime),
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF8E8E93)),
|
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 = [];
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:sweet_chat_app/config/AppConfig.dart';
|
||||||
|
|
||||||
final dioService = Dio(BaseOptions(
|
final dioService = Dio(BaseOptions(
|
||||||
baseUrl: 'http://172.29.100.146:2836/api',
|
baseUrl: AppConfig.baseUrl,
|
||||||
connectTimeout: Duration(seconds: 10),
|
connectTimeout: Duration(seconds: 5),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
));
|
));
|
||||||
13
lib/services/message_store.dart
Normal file
13
lib/services/message_store.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:sweet_chat_app/models/im_message.dart';
|
||||||
|
|
||||||
|
class MessageStore {
|
||||||
|
static final Map<String, List<ImMessage>> _data = {};
|
||||||
|
|
||||||
|
static void add(ImMessage msg) {
|
||||||
|
_data.putIfAbsent(msg.to, () => []).add(msg);
|
||||||
|
_data.putIfAbsent(msg.from, () => []).add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ImMessage> messagesFor(String userId) =>
|
||||||
|
_data[userId] ?? [];
|
||||||
|
}
|
||||||
26
lib/utils/date_utils.dart
Normal file
26
lib/utils/date_utils.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
37
lib/utils/toast_utils.dart
Normal file
37
lib/utils/toast_utils.dart
Normal file
@@ -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]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
name: sweet_chat_app
|
name: sweet_chat_app
|
||||||
Reference in New Issue
Block a user