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/config/AppConfig.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/sp_utils.dart'; class ChatPage extends StatefulWidget { final ContactInfo contactInfo; final int conversationId; const ChatPage({ super.key, required this.contactInfo, required this.conversationId, }); @override State createState() => _ChatPageState(); } class _ChatPageState extends State { late StreamSubscription _msgSub; final TextEditingController _textController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final FocusNode _focusNode = FocusNode(); late int userId; late String nickname; late String avatarUrl; late List _messages; @override void initState() { super.initState(); userId = SpUtil.getInt('userId') ?? 0; nickname = SpUtil.getString('nickname') ?? ''; avatarUrl = SpUtil.getString('avatarUrl') ?? ''; _messages = []; _loadChatMessage(); WidgetsBinding.instance.addPostFrameCallback((_) { scrollToBottom(); }); listenMessage(); _focusNode.addListener(() { if (_focusNode.hasFocus) { Future.delayed(const Duration(milliseconds: 350), scrollToBottom); } }); } Future _loadChatMessage() async { final list = await getChatMessageApi(widget.conversationId); if (!mounted) return; setState(() { _messages = list; }); } void listenMessage() { WebSocketService().connect('${AppConfig.wsUrl}?userId=$userId'); _msgSub = WebSocketService().messages.listen((websocketMessage) { _handleMessage(websocketMessage); scrollToBottom(); }); } @override void dispose() { _msgSub.cancel(); _focusNode.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); } void _sendMessage() { final text = _textController.text.trim(); if (text.isEmpty) return; final chat = ImMessage( type: ImMessageType.chatPrivate, title: nickname, from: userId.toString(), to: widget.contactInfo.id.toString(), content: text, ); WebSocketService().send(chat.toJsonString()); _textController.clear(); } 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: int.parse(imMsg.from), 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: NetworkImage(url), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(), body: SafeArea( child: Padding( padding: const EdgeInsets.all(10), child: Column( children: [ Expanded(child: _buildMessage()), SizedBox(height: 10), _buildInput(), ], ), ), ), ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: Color(0xFF120D25), foregroundColor: Colors.white, title: Text( widget.contactInfo.name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), centerTitle: true, ); } Widget _buildMessage() { return GlassCard( child: ListView.separated( controller: _scrollController, separatorBuilder: (BuildContext context, int index) { return SizedBox(height: 10); }, itemCount: _messages.length, itemBuilder: (context, index) { final msg = _messages[index]; return Row( mainAxisAlignment: checkIsMe(msg) ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!checkIsMe(msg)) _buildAvatar(widget.contactInfo.avatar), Flexible( child: Container( decoration: BoxDecoration( color: checkIsMe(msg) ? Color(0xFF2FA951) : Color(0xFF030816), borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), child: Text( msg.content, style: const TextStyle(fontSize: 16, color: Colors.white), ), ), ), const SizedBox(width: 8), if (checkIsMe(msg)) _buildAvatar(avatarUrl), ], ); }, ), ); } Widget _buildInput() { return SafeArea( child: GlassContainer( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ Expanded( child: TextField( controller: _textController, focusNode: _focusNode, onSubmitted: (_) => _sendMessage(), style: const TextStyle(color: Colors.white), cursorColor: Colors.white, decoration: const InputDecoration( hintText: '输入消息...', hintStyle: TextStyle(color: Colors.white), border: InputBorder.none, ), onTap: () { Future.delayed(const Duration(milliseconds: 350), scrollToBottom); }, ), ), IconButton( onPressed: _sendMessage, icon: const Icon(Icons.send, color: Colors.green), ), ], ), ), ); } void scrollToBottom() { Future.delayed(const Duration(milliseconds: 100), () { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }); } }