提交之GitHub
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package client;
|
||||
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import client.service.*;
|
||||
import client.view.LoginPage;
|
||||
import global.global;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class Client {
|
||||
private static boolean isConnected = false; // 是否连接成功
|
||||
|
||||
// 接收信息线程
|
||||
private ChatReceiver chatReceiver;
|
||||
// 发送信息线程
|
||||
private ChatSender chatSender;
|
||||
// 信息处理队列
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
// 用于分配动态端口的计数器
|
||||
private static int dynamicPortCounter = 10000;
|
||||
private static final int MAX_DYNAMIC_PORT = 20000;
|
||||
|
||||
// 程序入口
|
||||
public static void main(String[] args) {
|
||||
// 设置 FlatLaf 主题,使界面更现代化,符合当前审美
|
||||
try {
|
||||
FlatLightLaf.setup();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to initialize FlatLaf");
|
||||
}
|
||||
|
||||
Client client = new Client();
|
||||
client.startClient();
|
||||
|
||||
System.out.println("客户端启动成功,本地端口:" +
|
||||
(dynamicPortCounter > 10000 ? dynamicPortCounter - 1 : "未分配"));
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
LoginPage.get().setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
// 启动客户端,连接服务端,初始化相关内容
|
||||
public void startClient() {
|
||||
messageQueue = new ArrayBlockingQueue<>(40);
|
||||
// 连接服务器 创建 clientSocket
|
||||
try {
|
||||
int localPort = findAvailablePort(getNextDynamicPort());
|
||||
Socket socket;
|
||||
if (localPort > 0) {
|
||||
socket = new Socket(
|
||||
global.LOCAL_HOST,
|
||||
global.SERVER_PORT,
|
||||
null,
|
||||
localPort);
|
||||
} else {
|
||||
socket = new Socket(
|
||||
global.LOCAL_HOST,
|
||||
global.SERVER_PORT);
|
||||
}
|
||||
|
||||
chatSender = new ChatSender(socket, messageQueue);
|
||||
chatReceiver = new ChatReceiver(socket, messageQueue);
|
||||
|
||||
chatSender.start();
|
||||
Thread.sleep(200);
|
||||
chatReceiver.start();
|
||||
isConnected = true;
|
||||
} catch (IOException e) {
|
||||
isConnected = false;
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取动态分配的本地端口
|
||||
private synchronized int getNextDynamicPort() {
|
||||
int port = dynamicPortCounter;
|
||||
dynamicPortCounter++;
|
||||
|
||||
// 如果超出范围,重置到起始端口
|
||||
if (dynamicPortCounter >= MAX_DYNAMIC_PORT) {
|
||||
dynamicPortCounter = 10000;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
// 寻找可用端口
|
||||
private int findAvailablePort(int startPort) {
|
||||
int port = startPort;
|
||||
|
||||
while (port < MAX_DYNAMIC_PORT) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||
// 如果能成功创建ServerSocket,说明端口可用
|
||||
serverSocket.close();
|
||||
return port;
|
||||
} catch (IOException e) {
|
||||
// 端口被占用,尝试下一个
|
||||
port++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到可用端口,使用0让系统自动分配
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package client.service;
|
||||
|
||||
import client.view.LoginPage;
|
||||
import client.view.MainPage;
|
||||
import client.view.main.*;
|
||||
import global.global;
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
import server.serveice.Wrapper;
|
||||
import util.MsgUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* 客户端消息接收服务线程
|
||||
* <p>
|
||||
* 该类负责维护与服务端的长连接,持续监听并接收服务端推送的消息包(Wrapper)。
|
||||
* 作为客户端的“下行数据通道”,它将接收到的原始数据根据操作码(Operation Code)进行解析和分发,
|
||||
* 驱动本地数据更新(LocalData)及 UI 界面刷新(UIUpdate)。
|
||||
* <p>
|
||||
* 核心功能包括:
|
||||
* 1. 登录/注册响应处理
|
||||
* 2. 实时聊天消息接收与存储
|
||||
* 3. 群组创建、邀请及变更通知
|
||||
* 4. 异常捕获与日志记录
|
||||
*/
|
||||
public class ChatReceiver extends Thread {
|
||||
// 客户端本地Socket,用于与服务端通信
|
||||
private final Socket localSocket;
|
||||
ObjectInputStream ois;
|
||||
private static boolean isRunning;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* <p>
|
||||
* 初始化接收线程,绑定到已连接的客户端Socket。
|
||||
* 创建对象输入流,用于从Socket读取服务端消息。
|
||||
*
|
||||
* @param localSocket 已连接的客户端Socket
|
||||
* @param messageQueue 消息处理队列
|
||||
* @throws IOException 如果获取输入流失败
|
||||
*/
|
||||
public ChatReceiver(Socket localSocket, BlockingQueue<Wrapper> messageQueue) {
|
||||
this.localSocket = localSocket;
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程主循环:持续阻塞读取服务端消息并分发处理
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
ois = new ObjectInputStream(localSocket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
while (isRunning) {
|
||||
try {
|
||||
// 阻塞式读取服务端发送的 Wrapper 对象
|
||||
Wrapper message = (Wrapper) ois.readObject();
|
||||
|
||||
System.out.println("收到消息:" + message.getOperation());
|
||||
|
||||
handleMessage(message);
|
||||
}
|
||||
// 捕获 IO 异常(如连接中断)
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
// 连接中断,退出循环
|
||||
break;
|
||||
}
|
||||
// 捕获反序列化异常
|
||||
catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopRunning() {
|
||||
ChatReceiver.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息处理核心方法:根据操作码分发业务逻辑
|
||||
* 1. 登录/注册响应处理
|
||||
* 2. 实时聊天消息接收与存储
|
||||
* 3. 群组创建、邀请及变更通知
|
||||
* 4. 异常捕获与日志记录
|
||||
* 5. 服务器关闭通知
|
||||
* 6. 好友添加响应处理
|
||||
* 7. 好友删除响应处理
|
||||
* 8. 群组解散响应处理
|
||||
* 9. 好友添加请求处理
|
||||
* 10. 好友删除请求处理
|
||||
* 11. 群组解散请求处理
|
||||
* 12. 好友添加响应处理
|
||||
* 13. 好友删除响应处理
|
||||
* 14. 群组解散响应处理
|
||||
*
|
||||
* @param msg 服务端下发的消息包
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleMessage(Wrapper msg) {
|
||||
int opt = msg.getOperation();
|
||||
switch (opt) {
|
||||
case global.OPT_QUEST_WRONG:
|
||||
// 通用异常响应:直接展示服务端返回的错误信息
|
||||
System.out.println("出现未知异常");
|
||||
break;
|
||||
|
||||
case global.OPT_REGISTER_FAILED_ACC:
|
||||
LoginPage.get().showMsgDialog("账号已存在");
|
||||
break;
|
||||
|
||||
case global.OPT_REGISTER_SUCCESS:
|
||||
case global.OPT_LOGIN_SUCCESS:
|
||||
System.out.println("登录/注册成功");
|
||||
LoginPage.get().openMainPage();
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
|
||||
// 新增:请求初始化所有用户详细信息
|
||||
ChatSender.addMsg(Wrapper.simpleRequest(LocalData.get().getId(), null, global.OPT_INIT_USER_DETAIL));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_ACC:
|
||||
LoginPage.get().showMsgDialog("账号不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_PWD:
|
||||
LoginPage.get().showMsgDialog("密码错误");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_REPEATED:
|
||||
LoginPage.get().showMsgDialog("账户已登录,请勿重复登录");
|
||||
break;
|
||||
|
||||
case global.OPT_ERROR_NOT_LOGIN:
|
||||
System.out.println("未知的登录问题?");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGOUT:
|
||||
MainPage.get().openLogInPage();
|
||||
break;
|
||||
|
||||
case global.OPT_DELETE_ACCOUNT:
|
||||
MainPage.get().showMsgDialog("账号已被删除");
|
||||
MainPage.get().openLogInPage();
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_NICKNAME:
|
||||
LocalData.get().setUserName(msg.getSenderId(), (String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_PASSWORD:
|
||||
MainPage.get().showMsgDialog("密码已经更新");
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_NAME_FAILED:
|
||||
MainPage.get().showMsgDialog("昵称修改失败");
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_PASSWORD_FAILED:
|
||||
MainPage.get().showMsgDialog("密码修改失败");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_CREATE_SUCCESS:
|
||||
MainPage.get().showMsgDialog("群组创建成功");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE:
|
||||
// 群组变更(新建/被拉入):更新本地群组列表并刷新左侧导航栏
|
||||
MainPage.get().showGroupInviteRequestDialog(
|
||||
msg.getSenderId(),
|
||||
LocalData.get().getUserName(msg.getSenderId()),
|
||||
(String) msg.getData(),
|
||||
msg.getGroupId());
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE_REFUSE:
|
||||
MainPage.get().showMsgDialog("对方拒绝了你的邀请");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE_OFFLINE:
|
||||
MainPage.get().showMsgDialog("邀请的用户并不在线");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_QUIT:
|
||||
handleGroupQuit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_DISBAND:
|
||||
MainPage.get().showMsgDialog("群聊已被解散");
|
||||
break;
|
||||
|
||||
case global.OPT_CHAT:
|
||||
handleChatRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_PRIVATE_CHAT:
|
||||
handlePrivateChatRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_UPDATE_NAME:
|
||||
// 群信息变更:更新群名显示
|
||||
LocalData.get().setGroupName(msg.getGroupId(), (String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_UPDATE_OWNER:
|
||||
LocalData.get().getGroupData(msg.getGroupId()).setGroupOwner((String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT:
|
||||
MainPage.get().showMsgDialog("用户名格式错误,请重新输入");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_JOIN_FAILED:
|
||||
MainPage.get().showMsgDialog("加入群聊失败:群组不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_SUCCESS:
|
||||
handleFriendAddSuccess(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_FAILED:
|
||||
MainPage.get().showMsgDialog("添加好友失败:用户不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_USER_DETAIL:
|
||||
// 初始化所有用户详细信息
|
||||
Map<String, UserData> userDetails = (Map<String, UserData>) msg.getData();
|
||||
LocalData.get().setUserDetails(userDetails);
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_USER_DETAIL:
|
||||
// 更新单个用户详细信息
|
||||
UserData updatedUser = (UserData) msg.getData();
|
||||
LocalData.get().updateUserDetails(updatedUser);
|
||||
// 刷新界面(如果当前正显示该用户的资料)
|
||||
// 由于目前没有全局事件总线,这里简单起见,可以刷新整个 MainView 或者依赖用户重新点击
|
||||
// 实际上,如果正在查看好友资料,可能需要实时刷新。
|
||||
// 简单实现:不主动刷新UI,下次进入界面时读取最新数据即可。
|
||||
// 如果在“设置-个人信息”界面,且更新的是自己,界面通常已经由用户操作更新了,或者可以刷新一下。
|
||||
break;
|
||||
|
||||
case global.OPT_EXIT:
|
||||
// 服务器关闭通知
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(MainPage.get(), "服务器已关闭,程序将退出。", "系统通知", JOptionPane.WARNING_MESSAGE);
|
||||
System.exit(0);
|
||||
});
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD:
|
||||
handleFriendAddRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_REFUSE:
|
||||
MainPage.get().showMsgDialog("对方拒绝了你的好友请求");
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_CHAT:
|
||||
handleChatInit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_USER:
|
||||
handleUserInit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_GROUP:
|
||||
handleGroupInit(msg);
|
||||
break;
|
||||
|
||||
case global.SERVER_MESSAGE:
|
||||
MainPage.get().showMsgDialog((String) msg.getData());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的好友请求
|
||||
* 弹出对话框询问用户是否同意
|
||||
*/
|
||||
private void handleFriendAddRequest(Wrapper msg) {
|
||||
String senderId = msg.getSenderId();
|
||||
String senderName = (String) msg.getData();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
MainPage.get(),
|
||||
"收到来自 " + senderName + " (" + senderId + ") 的好友请求,是否同意?",
|
||||
"好友请求",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
ChatSender.addMsg(Wrapper.friendAddAgree(LocalData.get().getId(), senderId));
|
||||
} else {
|
||||
ChatSender.addMsg(Wrapper.friendAddRefuse(LocalData.get().getId(), senderId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理好友添加成功消息
|
||||
*
|
||||
* @param msg 好友添加成功消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleFriendAddSuccess(Wrapper msg) {
|
||||
Map<String, String> friendInfo = (Map<String, String>) msg.getData();
|
||||
if (friendInfo != null) {
|
||||
for (Map.Entry<String, String> entry : friendInfo.entrySet()) {
|
||||
LocalData.get().addFriend(entry.getKey(), entry.getValue());
|
||||
}
|
||||
// 刷新好友列表 UI
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
SecondaryOptionView.get().exchangeToFriendList();
|
||||
MainPage.get().showMsgDialog("添加好友成功");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊退出消息
|
||||
*
|
||||
* @param msg 群聊退出消息
|
||||
*/
|
||||
private void handleGroupQuit(Wrapper msg) {
|
||||
LocalData.get().removeGroupChatMsg(msg.getGroupId());
|
||||
LocalData.get().setCurrentChatId(null);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
SecondaryOptionView.get().removeGroupListItem(msg.getGroupId());
|
||||
MainPage.get().exchangeToBlankContent();
|
||||
MainPage.get().revalidate();
|
||||
MainPage.get().repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊初始化消息
|
||||
* 初始化群聊消息记录
|
||||
*
|
||||
* @param msg 群聊初始化消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleChatInit(Wrapper msg) {
|
||||
List<String> chatHistory = (List<String>) msg.getData();
|
||||
|
||||
// 初始化群聊消息记录
|
||||
LocalData.get().addChatMsg(msg.getGroupId(), chatHistory);
|
||||
|
||||
// 如果当前正好停留在该群聊界面,刷新消息
|
||||
if (msg.getGroupId().equals(LocalData.get().getCurrentChatId())) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().setChatInfo(msg.getGroupId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户初始化消息
|
||||
*
|
||||
* @param msg 用户初始化消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleUserInit(Wrapper msg) {
|
||||
Map<String, String> groupMap = (Map<String, String>) msg.getData();
|
||||
for (Map.Entry<String, String> entry : groupMap.entrySet()) {
|
||||
LocalData.get().addUserId_name(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊初始化消息
|
||||
*
|
||||
* @param msg 群聊初始化消息
|
||||
*/
|
||||
private void handleGroupInit(Wrapper msg) {
|
||||
GroupData newGroupData = (GroupData) msg.getData();
|
||||
|
||||
// 打印群聊初始化信息
|
||||
System.out.println(
|
||||
"handleGroupInit: " + newGroupData.getGroupId() + ", members: " + newGroupData.getMembers().size());
|
||||
|
||||
// 添加群聊数据到本地存储
|
||||
LocalData.get().addGroup(newGroupData.getGroupId(), newGroupData);
|
||||
|
||||
// 更新群聊列表 UI
|
||||
SecondaryOptionView.get().updateGroupList(
|
||||
newGroupData.getGroupId(),
|
||||
newGroupData.getGroupName(),
|
||||
0);
|
||||
|
||||
// 如果当前处于群聊模式,刷新群聊列表以显示新加入的群组
|
||||
SecondaryOptionView.get().refreshIfInGroupMode();
|
||||
|
||||
// 更新当前群组信息(如果当前正打开该群)
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
// SecondaryOptionView.get().updateGroupList(newGroupData.getGroupId(),
|
||||
// newGroupData.getGroupName(), 0);
|
||||
|
||||
// 检查是否需要更新当前显示的群组信息(如果当前正打开该群)
|
||||
if (newGroupData.getGroupId().equals(currentChatId)) {
|
||||
System.out.println("Updating GroupInfoView...");
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
GroupInfoView.get().updateInfo();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊请求消息
|
||||
*
|
||||
* @param msg 群聊请求消息
|
||||
*/
|
||||
private void handleChatRequest(Wrapper msg) {
|
||||
String content = (String) msg.getData();
|
||||
String[] split = MsgUtil.splitMsg(content);
|
||||
|
||||
LocalData.get().addChatMsg(msg.getGroupId(), content);
|
||||
// 如果就在当前这个面板,立即更新面板
|
||||
if (LocalData.get().getCurrentChatId().equals(msg.getGroupId())) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().addOtherUserMessage(split[1], split[2]);
|
||||
});
|
||||
} else {
|
||||
SecondaryOptionView.get().updateGroupList(
|
||||
msg.getGroupId(),
|
||||
LocalData.get().getGroupName(msg.getGroupId()),
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊请求消息
|
||||
*/
|
||||
private void handlePrivateChatRequest(Wrapper msg) {
|
||||
String senderId = msg.getSenderId();
|
||||
String text = (String) msg.getData();
|
||||
|
||||
// 获取发送者名称
|
||||
String senderName = LocalData.get().getFriends().get(senderId);
|
||||
if (senderName == null) {
|
||||
senderName = LocalData.get().getUserName(senderId);
|
||||
if (senderName == null)
|
||||
senderName = senderId;
|
||||
}
|
||||
|
||||
// 组合成标准消息格式: id|name|text
|
||||
String combinedMsg = MsgUtil.combineMsg(senderId, senderName, text);
|
||||
|
||||
// 存储消息,使用senderId作为chatId
|
||||
LocalData.get().addChatMsg(senderId, combinedMsg);
|
||||
|
||||
// 如果就在当前这个面板,立即更新面板
|
||||
if (senderId.equals(LocalData.get().getCurrentChatId())) {
|
||||
String finalSenderName = senderName;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().addOtherUserMessage(finalSenderName, text);
|
||||
});
|
||||
} else {
|
||||
// 更新消息列表
|
||||
SecondaryOptionView.get().updateMessageList(
|
||||
senderId,
|
||||
senderName,
|
||||
text,
|
||||
1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package client.service;
|
||||
|
||||
import client.view.MainPage;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class ChatSender extends Thread {
|
||||
|
||||
private static BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
public static void addMsg(Wrapper msg) {
|
||||
messageQueue.add(msg);
|
||||
}
|
||||
|
||||
private ObjectOutputStream oos;
|
||||
private final Socket clientSocket;
|
||||
|
||||
private static boolean isRunning;
|
||||
|
||||
public static void stopRunning() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public ChatSender(Socket clientSocket, BlockingQueue<Wrapper> messageQueue) {
|
||||
ChatSender.messageQueue = messageQueue;
|
||||
this.clientSocket = clientSocket;
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
oos = new ObjectOutputStream(clientSocket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Wrapper msg = null;
|
||||
while (isRunning) {
|
||||
try {
|
||||
msg = messageQueue.take();
|
||||
oos.writeObject(msg);
|
||||
oos.flush();
|
||||
System.out.println("信息已发出:" + msg.getOperation());
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println(LocalData.get().getId() + ": 消息队列被中断");
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
if (isConnectionClosed(e)) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(MainPage.get(), "服务器连接已断开,请重新登录。", "连接断开",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
System.exit(0);
|
||||
});
|
||||
System.out.println("服务器连接已断开");
|
||||
break;
|
||||
} else {
|
||||
System.out.println("发送消息时发生IO异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnectionClosed(IOException e) {
|
||||
// 根据异常类型判断连接是否断开
|
||||
return e instanceof SocketException ||
|
||||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
|
||||
e.getMessage().contains("Broken pipe") ||
|
||||
e.getMessage().contains("Connection refused") ||
|
||||
e.getMessage().contains("Software caused connection abort"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package client.service;
|
||||
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
// 这个类用于存储本地的数据,便于之后的UI更新操作。
|
||||
public class LocalData {
|
||||
private static final LocalData INSTANCE = new LocalData();
|
||||
|
||||
public static LocalData get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private String id;
|
||||
|
||||
// 当前的所在的群聊id,辅助UI更新
|
||||
private String currentChatId;
|
||||
|
||||
// 使用 ConcurrentHashMap 替代 HashMap
|
||||
private ConcurrentMap<String, String> userIdNameMap;
|
||||
private ConcurrentMap<String, GroupData> groupDataMap;
|
||||
private ConcurrentMap<String, List<String>> groupChatMap;
|
||||
|
||||
// 好友列表
|
||||
private ConcurrentMap<String, String> friendMap;
|
||||
|
||||
// 存储所有用户的详细信息
|
||||
private ConcurrentMap<String, UserData> userDetailsMap;
|
||||
|
||||
private LocalData() {
|
||||
userIdNameMap = new ConcurrentHashMap<>();
|
||||
groupDataMap = new ConcurrentHashMap<>();
|
||||
groupChatMap = new ConcurrentHashMap<>();
|
||||
friendMap = new ConcurrentHashMap<>();
|
||||
userDetailsMap = new ConcurrentHashMap<>();
|
||||
|
||||
id = "";
|
||||
currentChatId = "";
|
||||
}
|
||||
|
||||
public Map<String, String> getFriends() {
|
||||
return friendMap;
|
||||
}
|
||||
|
||||
public void addFriend(String id, String name) {
|
||||
friendMap.put(id, name);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCurrentChatName() {
|
||||
return groupDataMap.get(currentChatId).getGroupName();
|
||||
}
|
||||
|
||||
public synchronized void setCurrentChatId(String id) {
|
||||
this.currentChatId = id;
|
||||
}
|
||||
|
||||
public synchronized String getCurrentChatId() {
|
||||
return currentChatId;
|
||||
}
|
||||
|
||||
public void addUserId_name(String id, String name) {
|
||||
userIdNameMap.put(id, name);
|
||||
}
|
||||
|
||||
public String getGroupName(String groupId) {
|
||||
return groupDataMap.get(groupId).getGroupName();
|
||||
}
|
||||
|
||||
public void addChatMsg(String groupId, List<String> messages) {
|
||||
// 覆盖模式:用于初始化加载历史消息,避免重复添加
|
||||
groupChatMap.put(groupId, new ArrayList<>(messages));
|
||||
}
|
||||
|
||||
public void addChatMsg(String groupId, String message) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
groupChatMap.get(groupId).add(message);
|
||||
} else {
|
||||
groupChatMap.put(groupId, new ArrayList<>());
|
||||
groupChatMap.get(groupId).add(message);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getChatMsg(String groupId) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
return groupChatMap.get(groupId);
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void setUserDetails(Map<String, UserData> userDetails) {
|
||||
userDetailsMap.clear();
|
||||
userDetailsMap.putAll(userDetails);
|
||||
// 同时更新 userIdNameMap,确保一致性
|
||||
userDetails.forEach((id, userData) -> {
|
||||
userIdNameMap.put(id, userData.getNickname());
|
||||
});
|
||||
}
|
||||
|
||||
public void updateUserDetails(UserData userData) {
|
||||
userDetailsMap.put(userData.getUserId(), userData);
|
||||
userIdNameMap.put(userData.getUserId(), userData.getNickname());
|
||||
}
|
||||
|
||||
public UserData getUserDetail(String userId) {
|
||||
return userDetailsMap.get(userId);
|
||||
}
|
||||
|
||||
public void removeGroupChatMsg(String groupId) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
groupChatMap.remove(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserName(String userId) {
|
||||
if (userIdNameMap.containsKey(userId))
|
||||
return userIdNameMap.get(userId);
|
||||
else
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserName(String suerId, String name) {
|
||||
userIdNameMap.put(suerId, name);
|
||||
}
|
||||
|
||||
public synchronized void addGroup(String groupId, GroupData groupData) {
|
||||
groupDataMap.put(groupId, groupData); // HashMap的put方法会自动替换已有的键值对
|
||||
}
|
||||
|
||||
public void setGroupName(String groupId, String name) {
|
||||
groupDataMap.get(groupId).setGroupName(name);
|
||||
}
|
||||
|
||||
public GroupData getGroupData(String groupId) {
|
||||
return groupDataMap.get(groupId);
|
||||
}
|
||||
|
||||
public List<GroupData> getAllGroups() {
|
||||
return new ArrayList<>(groupDataMap.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package client.view;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.login.*;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* 登录页面,用于完成用户账户登录的功能,作为一个独立的页面,登录完成之后将自动关闭,并开启主界面
|
||||
*/
|
||||
public class LoginPage extends JFrame {
|
||||
private static volatile LoginPage INSTANCE;
|
||||
|
||||
// 获取登录页面实例,使用单例模式确保全局唯一性
|
||||
public static LoginPage get() {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (LoginPage.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new LoginPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// 交替展示两个窗口,分别用于进行注册和登录操作。
|
||||
private SignInView signInView;
|
||||
private SignUpView signUpView;
|
||||
|
||||
private LoginPage() {
|
||||
setTitle("欢迎来到本地网聊天室!");
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
// 如果链接上了,发出退出信息
|
||||
if (Client.isConnected()) {
|
||||
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
|
||||
}
|
||||
super.windowClosing(e);
|
||||
// 如果未登录,则直接退出。
|
||||
if (LocalData.get().getId().length() == 0) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
signInView = new SignInView(this);
|
||||
signUpView = new SignUpView(this);
|
||||
|
||||
// 默认为登录
|
||||
exchangeToSignInView();
|
||||
}
|
||||
|
||||
// 更换到注册界面
|
||||
public void exchangeToSignUpView() {
|
||||
this.remove(signInView);
|
||||
this.add(signUpView, BorderLayout.CENTER);
|
||||
this.validate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
// 更换到登录界面
|
||||
public void exchangeToSignInView() {
|
||||
this.remove(signUpView);
|
||||
this.add(signInView, BorderLayout.CENTER);
|
||||
this.validate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
// 打开主界面,并关闭本界面
|
||||
public void openMainPage() {
|
||||
// 创建新窗口
|
||||
MainPage.get().setVisible(true);
|
||||
|
||||
// 关闭当前窗口
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public void showMsgDialog(String text) {
|
||||
JDialog inviteDialog = new JDialog(this, "信息", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(this);
|
||||
|
||||
// 设置对话框内容
|
||||
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
|
||||
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
|
||||
JButton closeBtn = new JButton("确定");
|
||||
|
||||
closeBtn.addActionListener(e -> inviteDialog.dispose());
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(closeBtn, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
package client.view;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.main.*;
|
||||
import global.global;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
/**
|
||||
* 主界面。
|
||||
*/
|
||||
public class MainPage extends JFrame {
|
||||
private volatile static MainPage instance;
|
||||
|
||||
// 获取主界面
|
||||
public static MainPage get() {
|
||||
if (instance == null) {
|
||||
synchronized (LoginPage.class) {
|
||||
if (instance == null) {
|
||||
instance = new MainPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 可以左右的移动大小的分割界面
|
||||
private final JSplitPane splitPane;
|
||||
private final SideOptionView sideOptionView;
|
||||
private final SecondaryOptionView secondaryOptionView;
|
||||
private final ContentView contentView;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 初始化主界面组件,包括侧边栏、二级选项栏、详细内容区域等。
|
||||
* 设置主界面的标题、关闭操作、大小、位置等属性。
|
||||
* 同时添加窗口关闭监听器,在窗口关闭时发送登出请求。
|
||||
*/
|
||||
private MainPage() {
|
||||
Dimension size = new Dimension(WINDOW_ORI_WIDTH, WINDOW_ORI_HEIGHT);
|
||||
|
||||
setTitle("本地网络聊天室");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(size);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
// 侧边栏
|
||||
sideOptionView = new SideOptionView();
|
||||
sideOptionView.setMinimumSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
|
||||
sideOptionView.setMaximumSize(new Dimension(SIDE_PANEL_WIDTH, Integer.MAX_VALUE));
|
||||
sideOptionView.setPreferredSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
|
||||
|
||||
// 二级选项栏
|
||||
secondaryOptionView = SecondaryOptionView.get();
|
||||
// 详细内容
|
||||
contentView = new ContentView();
|
||||
|
||||
splitPane = new JSplitPane(
|
||||
JSplitPane.HORIZONTAL_SPLIT,
|
||||
secondaryOptionView,
|
||||
contentView);
|
||||
|
||||
splitPane.setDividerSize(2);
|
||||
splitPane.setOneTouchExpandable(true);
|
||||
splitPane.setResizeWeight(0.5);
|
||||
splitPane.setContinuousLayout(true);
|
||||
splitPane.setMinimumSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
|
||||
splitPane.setPreferredSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
|
||||
|
||||
addDividerConstraintListener();
|
||||
|
||||
this.add(sideOptionView, BorderLayout.WEST);
|
||||
this.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
// 如果没有连接上,则直接退出
|
||||
if (Client.isConnected()) {
|
||||
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
|
||||
}
|
||||
// 否则,窗口关闭的时候发送登出信息
|
||||
super.windowClosing(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送初始化请求
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分隔条约束监听器
|
||||
* 监听分隔条位置变化事件,确保分隔条不会超出最小和最大允许位置范围。
|
||||
* 当窗口大小改变或分隔条位置改变时,调用constrainDividerLocation方法重新限制分隔条位置。
|
||||
*/
|
||||
private void addDividerConstraintListener() {
|
||||
splitPane.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
// 窗口大小改变时重新计算限制
|
||||
constrainDividerLocation();
|
||||
}
|
||||
});
|
||||
|
||||
splitPane.getLeftComponent().addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
constrainDividerLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制分隔条位置
|
||||
* 确保分隔条不会超出最小和最大允许位置范围。
|
||||
* 如果当前位置小于最小位置,将分隔条位置设置为最小位置。
|
||||
* 如果当前位置大于最大位置,将分隔条位置设置为最大位置。
|
||||
*/
|
||||
private void constrainDividerLocation() {
|
||||
int totalWidth = splitPane.getWidth();
|
||||
int dividerSize = splitPane.getDividerSize();
|
||||
int currentLocation = splitPane.getDividerLocation();
|
||||
|
||||
// 计算有效位置范围
|
||||
int minLocation = SECONDARY_PANEL_WIDTH_MIN;
|
||||
int maxLocation = totalWidth - dividerSize - SECONDARY_PANEL_WIDTH_MIN;
|
||||
|
||||
// 限制分隔条位置
|
||||
if (currentLocation < minLocation) {
|
||||
splitPane.setDividerLocation(minLocation);
|
||||
} else if (currentLocation > maxLocation) {
|
||||
splitPane.setDividerLocation(maxLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置界面,当前还没做具体实现
|
||||
*/
|
||||
public void exchangeToSettingPage() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示消息对话框
|
||||
* 创建一个对话框,显示text内容。
|
||||
* 对话框标题为"信息",大小为300x200,居中显示在主窗口上。
|
||||
* 对话框内容为text,居中对齐,宽度为210px, padding为10px。
|
||||
* 对话框包含一个确定按钮,点击后关闭对话框。
|
||||
*/
|
||||
public void showMsgDialog(String text) {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "信息", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 设置对话框内容
|
||||
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
|
||||
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
|
||||
JButton closeBtn = new JButton("确定");
|
||||
|
||||
closeBtn.addActionListener(e -> inviteDialog.dispose());
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(closeBtn, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到登录界面
|
||||
* 隐藏当前主窗口,显示登录界面。
|
||||
*/
|
||||
public void openLogInPage() {
|
||||
LoginPage.get().setVisible(true);
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到空白内容界面
|
||||
* 清空当前内容区域,显示一个空白界面。
|
||||
*/
|
||||
public void exchangeToBlankContent() {
|
||||
contentView.exchangeToBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到聊天房间界面
|
||||
* 清空当前内容区域,显示聊天房间界面。
|
||||
* 聊天房间界面显示与groupId相关的聊天内容。
|
||||
*/
|
||||
public void exchangeToChatRoom(String groupId) {
|
||||
contentView.exchangeToChatRoom(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到好友个人信息界面
|
||||
* 清空当前内容区域,显示好友个人信息界面。
|
||||
* 好友个人信息界面显示与userId相关的好友信息。
|
||||
*/
|
||||
public void exchangeToFriendProfile(String userId, String userName) {
|
||||
contentView.exchangeToFriendProfile(userId, userName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置界面
|
||||
* 清空当前内容区域,显示设置界面。
|
||||
* 设置界面根据type显示不同的设置选项。
|
||||
*/
|
||||
public void exchangeToSettings(String type) {
|
||||
contentView.exchangeToSettings(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到群聊邀请请求界面
|
||||
* 清空当前内容区域,显示群聊邀请请求界面。
|
||||
* 群聊邀请请求界面显示与inviterId相关的群聊邀请请求,包括邀请者姓名、群聊名称等。
|
||||
*/
|
||||
public void showGroupInviteRequestDialog(String inviterId, String inviterName, String groupName, String groupId) {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "群聊邀请", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 设置对话框内容
|
||||
JLabel label = new JLabel(
|
||||
"用户" + inviterName + "邀请你加入:" + groupName,
|
||||
SwingConstants.CENTER);
|
||||
JButton confirmBtn = new JButton("接受");
|
||||
JButton closeBtn = new JButton("拒绝");
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
JPanel bottomPanel = new JPanel();
|
||||
bottomPanel.add(confirmBtn);
|
||||
bottomPanel.add(closeBtn);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
confirmBtn.addActionListener(
|
||||
e -> {
|
||||
ChatSender.addMsg(
|
||||
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_AGREE));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
closeBtn.addActionListener(
|
||||
e -> {
|
||||
ChatSender.addMsg(
|
||||
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_REFUSE));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到邀请好友界面
|
||||
* 清空当前内容区域,显示邀请好友界面。
|
||||
* 邀请好友界面允许用户输入好友ID,邀请好友加入当前聊天房间。
|
||||
*/
|
||||
public void showGroupInviteDialog() {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "邀请好友", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("好友ID :"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String userId = idField.getText().trim();
|
||||
|
||||
if (userId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "好友不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里添加创建群聊的逻辑
|
||||
ChatSender.addMsg(Wrapper.groupInviteRequest(
|
||||
userId,
|
||||
LocalData.get().getId(),
|
||||
LocalData.get().getCurrentChatId()));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
inviteDialog.add(mainPanel);
|
||||
inviteDialog.getRootPane().setDefaultButton(confirmButton);
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到加入群聊界面
|
||||
* 清空当前内容区域,显示加入群聊界面。
|
||||
* 加入群聊界面允许用户输入群聊ID,申请加入群聊。
|
||||
*/
|
||||
public void showJoinGroupDialog() {
|
||||
JDialog dialog = new JDialog(MainPage.get(), "加入群聊", true);
|
||||
dialog.setSize(300, 200);
|
||||
dialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("群聊ID:"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String groupId = idField.getText().trim();
|
||||
|
||||
if (groupId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "群聊ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送加入群聊请求
|
||||
ChatSender.addMsg(new Wrapper(null, LocalData.get().getId(), groupId, global.OPT_GROUP_JOIN));
|
||||
dialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
dialog.add(mainPanel);
|
||||
dialog.getRootPane().setDefaultButton(confirmButton);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到添加好友界面
|
||||
* 清空当前内容区域,显示添加好友界面。
|
||||
* 添加好友界面允许用户输入好友ID,申请添加好友。
|
||||
*/
|
||||
public void showAddFriendDialog() {
|
||||
JDialog dialog = new JDialog(MainPage.get(), "添加好友", true);
|
||||
dialog.setSize(300, 200);
|
||||
dialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("好友ID:"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String friendId = idField.getText().trim();
|
||||
|
||||
if (friendId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "好友ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (friendId.equals(LocalData.get().getId())) {
|
||||
JOptionPane.showMessageDialog(dialog, "不能添加自己为好友", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送添加好友请求
|
||||
ChatSender.addMsg(new Wrapper(friendId, LocalData.get().getId(), null, global.OPT_FRIEND_ADD));
|
||||
dialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
dialog.add(mainPanel);
|
||||
dialog.getRootPane().setDefaultButton(confirmButton);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到创建群聊界面
|
||||
* 清空当前内容区域,显示创建群聊界面。
|
||||
* 创建群聊界面允许用户输入群聊ID和名称,创建一个新的群聊房间。
|
||||
*/
|
||||
public void showGroupCreateDialog() {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "创建群聊", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("群聊ID :"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 创建名称面板
|
||||
JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
namePanel.add(new JLabel("群聊名称:"));
|
||||
JTextField nameField = new JTextField(15);
|
||||
namePanel.add(nameField);
|
||||
mainPanel.add(namePanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String groupId = idField.getText().trim();
|
||||
String groupName = nameField.getText().trim();
|
||||
|
||||
if (groupId.isEmpty() || groupName.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊ID和名称不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupId.length() < 6 || groupId.length() > 10 || !groupId.matches("[a-zA-Z0-9_]+")) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊ID只能包含字母大小写和数字下划线,且长度不得小于6,大于10", "警告",
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName.contains(" ")) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊名称不能包含空格", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加创建群聊的逻辑
|
||||
ChatSender.addMsg(Wrapper.createGroupRequest(LocalData.get().getId(), groupName, groupId));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
inviteDialog.add(mainPanel);
|
||||
inviteDialog.getRootPane().setDefaultButton(confirmButton);
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package client.view.login;
|
||||
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.LoginPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
// 登录界面
|
||||
public class SignInView extends JPanel {
|
||||
private LoginPage loginPage;
|
||||
|
||||
JTextField account;
|
||||
JTextField password;
|
||||
|
||||
/**
|
||||
* 生成一个登录界面
|
||||
* 按照原型图中设计,创建两个文本输入框,用于让用户输入id和密码。
|
||||
* 创建一个按钮用于登录,为按钮添加SignIncheck的按下事件
|
||||
* 创建一个注册选项按钮,按下后该界面更换为注册界面。
|
||||
*/
|
||||
public SignInView(LoginPage loginPage) {
|
||||
this.loginPage = loginPage;
|
||||
|
||||
//面板
|
||||
this.setLayout(null);
|
||||
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
this.setVisible(true);
|
||||
|
||||
//文本展示
|
||||
JLabel ja1 = new JLabel("登录");
|
||||
ja1.setBounds(165, 10, 80, 80);
|
||||
this.add(ja1);
|
||||
|
||||
|
||||
JLabel ja2 = new JLabel("账户:");
|
||||
ja2.setBounds(50, 80, 100, 30);
|
||||
this.add(ja2);
|
||||
|
||||
|
||||
JLabel ja3 = new JLabel("密码:");
|
||||
ja3.setBounds(50, 120, 100, 30);
|
||||
this.add(ja3);
|
||||
|
||||
|
||||
//按钮设计
|
||||
JButton jb1 = new JButton("注册");
|
||||
jb1.setBounds(230, 190, 59, 20);
|
||||
jb1.setBorderPainted(false);
|
||||
jb1.addActionListener(e -> {
|
||||
this.loginPage.exchangeToSignUpView();
|
||||
});
|
||||
this.add(jb1);
|
||||
|
||||
// JButton jb2=new JButton("忘记密码");
|
||||
// jb2.setBounds(290,190,90,20);
|
||||
// jb2.setBorderPainted(false);
|
||||
// jb2.addActionListener(e -> {
|
||||
//
|
||||
// });
|
||||
// add(jb2);
|
||||
|
||||
|
||||
JButton jb3 = new JButton("登录");
|
||||
jb3.setBounds(150, 160, 59, 50);
|
||||
jb3.addActionListener(e -> {
|
||||
signIncheck();
|
||||
});
|
||||
this.add(jb3);
|
||||
|
||||
//建立文本域
|
||||
account = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
account.setBounds(80, 80, 220, 30);
|
||||
this.add(account);
|
||||
|
||||
password = new JPasswordField(DesignToken.MAX_FONT_SIZE);
|
||||
password.setBounds(80, 120, 220, 30);
|
||||
this.add(password);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面的用户id和密码
|
||||
* 如果当前服务器未在线,则直接向用户说明
|
||||
* 否则,发出登录请求
|
||||
*/
|
||||
private void signIncheck() {
|
||||
if (!Client.isConnected()) {
|
||||
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
LocalData.get().setId(account.getText());
|
||||
ChatSender.addMsg(
|
||||
Wrapper.loginRequest(LocalData.get().getId(), password.getText())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package client.view.login;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.LoginPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class SignUpView extends JPanel {
|
||||
private LoginPage loginPage;
|
||||
|
||||
private JTextField account;
|
||||
private JTextField password;
|
||||
private JTextField name;
|
||||
|
||||
/**
|
||||
* 生成一个注册界面
|
||||
* 按照原型图中设计,创建三个文本输入框,用于让用户输入id,密码,用户名
|
||||
* 创建一个按钮用于注册
|
||||
* 创建一个登录选项按钮,按下后该界面更换为登录界面。
|
||||
*/
|
||||
public SignUpView(LoginPage loginPage) {
|
||||
this.loginPage = loginPage;
|
||||
|
||||
//功能面板的建立
|
||||
this.setLayout(null);
|
||||
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
this.setVisible(true);
|
||||
|
||||
//文本
|
||||
JLabel ja1 = new JLabel("注册");
|
||||
ja1.setBounds(165, 10, 80, 80);
|
||||
this.add(ja1);
|
||||
|
||||
JLabel ja2 = new JLabel("账户:");
|
||||
ja2.setBounds(50, 80, 100, 30);
|
||||
this.add(ja2);
|
||||
|
||||
JLabel ja3 = new JLabel("密码:");
|
||||
ja3.setBounds(50, 120, 100, 30);
|
||||
this.add(ja3);
|
||||
|
||||
JLabel ja4 = new JLabel("名字:");
|
||||
ja4.setBounds(50, 160, 100, 30);
|
||||
this.add(ja4);
|
||||
|
||||
|
||||
//监听按钮
|
||||
JButton jb1 = new JButton("注册");
|
||||
jb1.setBounds(150, 200, 59, 50);
|
||||
this.add(jb1);
|
||||
jb1.addActionListener(e -> {
|
||||
this.SignUpcheck();
|
||||
});
|
||||
|
||||
JButton jb2 = new JButton("已有账号?去登录");
|
||||
jb2.setBounds(220, 230, 150, 20);
|
||||
jb2.setBorderPainted(false);
|
||||
jb2.addActionListener(e -> {
|
||||
this.loginPage.exchangeToSignInView();
|
||||
});
|
||||
this.add(jb2);
|
||||
|
||||
//文本域
|
||||
account = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
account.setBounds(80, 80, 220, 30);
|
||||
this.add(account);
|
||||
|
||||
password = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
password.setBounds(80, 120, 220, 30);
|
||||
this.add(password);
|
||||
|
||||
name = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
name.setBounds(80, 160, 220, 30);
|
||||
this.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前界面的文本的内容
|
||||
* 检查服务器是否链接
|
||||
* 检查注册的id,name,password是否合法
|
||||
* 都合适的话,发送注册请求。
|
||||
*/
|
||||
private void SignUpcheck() {
|
||||
if (!Client.isConnected()) {
|
||||
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = account.getText();
|
||||
String userName = name.getText();
|
||||
String userPassword = password.getText();
|
||||
|
||||
if (userId.length() < 6 || userId.length() > 10 || !userId.matches("[a-zA-Z0-9_]+")) {
|
||||
loginPage.showMsgDialog("用户id只能包含字母大小写和数字下划线,且长度不得小于6,大于10");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userName.contains(" ")) {
|
||||
loginPage.showMsgDialog("用户名不能包含空格");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userPassword.contains(" ")) {
|
||||
loginPage.showMsgDialog("密码不能包含空格");
|
||||
return;
|
||||
}
|
||||
|
||||
LocalData.get().setId(userId);
|
||||
LocalData.get().setUserName(userId, userName);
|
||||
|
||||
ChatSender.addMsg(Wrapper.registerRequest(
|
||||
LocalData.get().getId(),
|
||||
password.getText(),
|
||||
name.getText()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
import util.MsgUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.AncestorEvent;
|
||||
import javax.swing.event.AncestorListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
// 群聊信息组件,包含底部的打字框和滚动的信息聊天信息
|
||||
public class ChatInfoView extends JPanel {
|
||||
|
||||
private static volatile ChatInfoView instance;
|
||||
|
||||
public static ChatInfoView get() {
|
||||
if (instance == null) {
|
||||
synchronized (ChatInfoView.class) {
|
||||
if (instance == null) {
|
||||
instance = new ChatInfoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 界面组件
|
||||
private JPanel messagePanel; // 使用JPanel来承载消息,可以自定义布局
|
||||
private List<JPanel> bubbles;
|
||||
private JScrollPane messageScrollPane;
|
||||
private JTextArea inputArea;
|
||||
private JScrollPane inputScrollPane;
|
||||
private JButton sendButton;
|
||||
private JPanel inputPanel;
|
||||
private JPanel buttonPanel;
|
||||
|
||||
// 样式相关
|
||||
private SimpleDateFormat timeFormat;
|
||||
private Color userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN); // 用户消息气泡颜色
|
||||
private Color otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE); // 他人消息气泡颜色(白色)
|
||||
private Color systemColor = Color.decode(DesignToken.BACKGROUND_COLOR); // 系统消息背景色
|
||||
|
||||
// 头像颜色数组,用于不同用户的头像显示
|
||||
private final Color[] avatarColors = {
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_BLUE),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_GRAY),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_RED),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_YELLOW),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_WHITE),
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 初始化界面组件和布局
|
||||
*/
|
||||
private ChatInfoView() {
|
||||
setLayout(new BorderLayout(0, 0));
|
||||
|
||||
// 初始化时间格式
|
||||
timeFormat = new SimpleDateFormat("HH:mm");
|
||||
|
||||
// 初始化消息显示区域
|
||||
initMessagePanel();
|
||||
|
||||
// 创建输入区域
|
||||
initInputPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化消息面板
|
||||
* 包含滚动条和消息气泡容器
|
||||
*/
|
||||
private void initMessagePanel() {
|
||||
bubbles = new ArrayList<>();
|
||||
|
||||
// 创建消息面板,使用垂直箱式布局
|
||||
messagePanel = new JPanel();
|
||||
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
|
||||
messagePanel.setBackground(systemColor);
|
||||
messagePanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// 添加一个弹性空间,让新消息从底部开始
|
||||
messagePanel.add(Box.createVerticalGlue());
|
||||
|
||||
// 添加滚动条
|
||||
messageScrollPane = new JScrollPane(messagePanel);
|
||||
messageScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||
messageScrollPane.getVerticalScrollBar().setUnitIncrement(16);
|
||||
messageScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
// 设置视口的背景色
|
||||
JViewport viewport = messageScrollPane.getViewport();
|
||||
viewport.setBackground(systemColor);
|
||||
|
||||
// 添加到主面板
|
||||
this.add(messageScrollPane, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化输入面板
|
||||
* 包含输入文本框和发送按钮
|
||||
*/
|
||||
private void initInputPanel() {
|
||||
// 创建输入面板
|
||||
inputPanel = new JPanel(new BorderLayout(5, 5));
|
||||
inputPanel.setBackground(systemColor);
|
||||
inputPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// 创建输入文本框
|
||||
inputArea = new JTextArea(3, 20);
|
||||
inputArea.setFont(new Font(DEFAULT_FONT, Font.PLAIN, DesignToken.FONT_SIZE));
|
||||
inputArea.setLineWrap(true);
|
||||
inputArea.setWrapStyleWord(true);
|
||||
inputArea.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
|
||||
|
||||
// 设置提示文本
|
||||
inputArea.setToolTipText("输入消息,按Enter发送,Ctrl+Enter换行");
|
||||
|
||||
// 添加滚动条到输入框
|
||||
inputScrollPane = new JScrollPane(inputArea);
|
||||
inputScrollPane.setBorder(null);
|
||||
|
||||
// 创建发送按钮
|
||||
sendButton = createSendButton();
|
||||
|
||||
// 创建按钮面板
|
||||
buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 5));
|
||||
buttonPanel.setBackground(systemColor);
|
||||
buttonPanel.add(sendButton);
|
||||
setupListeners();
|
||||
|
||||
// 添加组件到输入面板
|
||||
inputPanel.add(inputScrollPane, BorderLayout.CENTER);
|
||||
inputPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
// 添加到主面板
|
||||
this.add(inputPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
* 当主题变化时调用,用于更新所有组件的颜色
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 更新颜色变量
|
||||
userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN);
|
||||
otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE);
|
||||
systemColor = Color.decode(DesignToken.BACKGROUND_COLOR);
|
||||
|
||||
// 更新组件背景
|
||||
if (messagePanel != null)
|
||||
messagePanel.setBackground(systemColor);
|
||||
if (messageScrollPane != null && messageScrollPane.getViewport() != null) {
|
||||
messageScrollPane.getViewport().setBackground(systemColor);
|
||||
}
|
||||
if (inputPanel != null)
|
||||
inputPanel.setBackground(systemColor);
|
||||
if (buttonPanel != null)
|
||||
buttonPanel.setBackground(systemColor);
|
||||
if (sendButton != null)
|
||||
sendButton.setBackground(userColor);
|
||||
if (inputArea != null) {
|
||||
inputArea.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
|
||||
}
|
||||
|
||||
// 刷新当前聊天记录
|
||||
if (LocalData.get().getCurrentChatId() != null) {
|
||||
init(LocalData.get().getCurrentChatId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发送按钮
|
||||
* 按钮文本为“发送”,字体为加粗,大小为 DesignToken.FONT_SIZE
|
||||
* 背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,前景颜色为黑色
|
||||
* 点击时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,松开时恢复为 DesignToken.BUBBLE_COLOR_GREEN
|
||||
* 鼠标悬停时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN
|
||||
*
|
||||
* @return 发送按钮
|
||||
*/
|
||||
private JButton createSendButton() {
|
||||
JButton button = new JButton("发送");
|
||||
button.setFont(new Font(DEFAULT_FONT, Font.BOLD, DesignToken.FONT_SIZE));
|
||||
button.setBackground(userColor);
|
||||
button.setForeground(Color.BLACK); // 强制设置字体颜色为黑色,确保在绿色背景下清晰可见
|
||||
button.setFocusPainted(false);
|
||||
button.setBorder(BorderFactory.createEmptyBorder(10, 25, 10, 25));
|
||||
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
|
||||
// 鼠标悬停效果
|
||||
button.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
public void mouseEntered(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(Color.decode(DesignToken.BUBBLE_COLOR_GREEN));
|
||||
}
|
||||
|
||||
public void mouseExited(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
|
||||
public void mousePressed(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
|
||||
public void mouseReleased(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送事件
|
||||
* 点击发送按钮或按下Enter键发送消息
|
||||
*/
|
||||
private void setupListeners() {
|
||||
// 发送按钮点击事件
|
||||
sendButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 回车发送消息,Ctrl+Enter换行
|
||||
inputArea.addKeyListener(new java.awt.event.KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(java.awt.event.KeyEvent e) {
|
||||
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) {
|
||||
if (e.isControlDown()) {
|
||||
// Ctrl+Enter 换行
|
||||
inputArea.append("\n");
|
||||
} else {
|
||||
// Enter 发送消息
|
||||
e.consume(); // 防止默认的换行行为
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口显示时自动聚焦到输入框
|
||||
addAncestorListener(new AncestorListener() {
|
||||
@Override
|
||||
public void ancestorAdded(AncestorEvent e) {
|
||||
inputArea.requestFocusInWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ancestorRemoved(AncestorEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ancestorMoved(AncestorEvent event) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息
|
||||
* 先调用使用DataManager进行信息发送操作
|
||||
* 如果发送成功,则更新界面,添加信息到消息历史上
|
||||
* 如果发送未成功,则忽略这个操作(发射未成功表示程序出现了问题,在控制态输出问题)
|
||||
*/
|
||||
private void sendMessage() {
|
||||
String text = inputArea.getText().trim();
|
||||
if (!text.isEmpty()) {
|
||||
|
||||
String id = LocalData.get().getId();
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
String message = MsgUtil.combineMsg(id, LocalData.get().getUserName(id), text);
|
||||
|
||||
// 检查是群聊还是私聊
|
||||
if (LocalData.get().getGroupData(currentChatId) != null) {
|
||||
// 群聊
|
||||
ChatSender.addMsg(Wrapper.groupChat(message, id, currentChatId));
|
||||
} else {
|
||||
// 私聊:发送纯文本
|
||||
ChatSender.addMsg(Wrapper.privateChat(text, id, currentChatId));
|
||||
}
|
||||
|
||||
// 暂时保存消息
|
||||
LocalData.get().addChatMsg(
|
||||
LocalData.get().getCurrentChatId(),
|
||||
message);
|
||||
|
||||
// 添加用户消息
|
||||
addUserMessage(text);
|
||||
|
||||
// 清空输入框
|
||||
inputArea.setText("");
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 聚焦回输入框
|
||||
inputArea.requestFocusInWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加用户消息
|
||||
* 用户消息右对齐
|
||||
*
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addUserMessage(String content) {
|
||||
// 用户消息右对齐
|
||||
addMessageBubble(true, "我", content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加他人消息
|
||||
* 他人消息左对齐
|
||||
*
|
||||
* @param senderName 发送者名称
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addOtherUserMessage(String senderName, String content) {
|
||||
// 他人消息左对齐
|
||||
addMessageBubble(false, senderName, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前聊天信息
|
||||
* 从本地数据中获取聊天记录,根据发送者ID判断是用户消息还是他人消息
|
||||
*
|
||||
* @param chatId 聊天ID
|
||||
*/
|
||||
public void setChatInfo(String chatId) {
|
||||
// 清空当前消息
|
||||
messagePanel.removeAll();
|
||||
messagePanel.add(Box.createVerticalGlue());
|
||||
bubbles.clear();
|
||||
|
||||
// 从本地数据中获取聊天记录
|
||||
List<String> messages = LocalData.get().getChatMsg(chatId);
|
||||
if (messages != null) {
|
||||
String myId = LocalData.get().getId();
|
||||
for (String msg : messages) {
|
||||
String[] split = MsgUtil.splitMsg(msg);
|
||||
// split[0] = senderId, split[1] = senderName, split[2] = content
|
||||
if (split.length >= 3) {
|
||||
if (split[0].equals(myId)) {
|
||||
addUserMessage(split[2]);
|
||||
} else {
|
||||
addOtherUserMessage(split[1], split[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新消息面板
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加系统消息
|
||||
* 系统消息居中显示,字体为斜体,颜色为灰色
|
||||
*
|
||||
* @param content 系统消息内容
|
||||
*/
|
||||
public void addSystemMessage(String content) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 系统消息
|
||||
JPanel systemPanel = new JPanel();
|
||||
systemPanel.setLayout(new BorderLayout());
|
||||
systemPanel.setBackground(new Color(236, 236, 236));
|
||||
systemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40));
|
||||
|
||||
JLabel systemLabel = new JLabel(content);
|
||||
systemLabel.setFont(new Font(DEFAULT_FONT, Font.ITALIC, DesignToken.FONT_SIZE_SMALL));
|
||||
systemLabel.setForeground(Color.GRAY);
|
||||
systemLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
systemPanel.add(systemLabel, BorderLayout.CENTER);
|
||||
|
||||
// 添加到消息面板顶部
|
||||
messagePanel.add(systemPanel, 0);
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息气泡
|
||||
* 根据是否是用户消息,创建不同的消息气泡样式
|
||||
*
|
||||
* @param isSelf 是否是用户消息
|
||||
* @param senderName 发送者名称
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addMessageBubble(boolean isSelf, String senderName, String content) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 创建消息气泡面板
|
||||
JPanel messageBubblePanel = new JPanel();
|
||||
messageBubblePanel.setLayout(new BorderLayout(8, 0));
|
||||
messageBubblePanel.setBackground(systemColor);
|
||||
messageBubblePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 200));
|
||||
|
||||
// 添加头像
|
||||
JLabel avatarPanel = new JLabel(new CircleCharIcon2(
|
||||
avatarColors[Math.abs(senderName.hashCode()) % avatarColors.length],
|
||||
Color.WHITE,
|
||||
senderName.substring(0, 1).toUpperCase(),
|
||||
40));
|
||||
|
||||
// 创建消息内容面板
|
||||
JPanel contentPanel = new JPanel();
|
||||
contentPanel.setLayout(new BorderLayout(0, 5));
|
||||
contentPanel.setOpaque(false);
|
||||
|
||||
// 创建发送者标签和时间标签
|
||||
String time = timeFormat.format(new Date());
|
||||
JLabel infoLabel = new JLabel(senderName + " " + time);
|
||||
infoLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE_SMALL));
|
||||
infoLabel.setForeground(Color.GRAY);
|
||||
|
||||
// 创建消息气泡
|
||||
JTextArea messageLabel = new JTextArea(content);
|
||||
messageLabel.setEditable(false);
|
||||
messageLabel.setLineWrap(true);
|
||||
messageLabel.setWrapStyleWord(true);
|
||||
messageLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE));
|
||||
messageLabel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15));
|
||||
|
||||
// 设置气泡颜色和样式
|
||||
if (isSelf) {
|
||||
// 用户消息:右对齐,绿色气泡
|
||||
messageLabel.setBackground(userColor);
|
||||
messageLabel.setForeground(Color.WHITE);
|
||||
infoLabel.setHorizontalAlignment(SwingConstants.RIGHT);
|
||||
contentPanel.add(infoLabel, BorderLayout.NORTH);
|
||||
contentPanel.add(messageLabel, BorderLayout.CENTER);
|
||||
|
||||
// 右对齐布局
|
||||
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
|
||||
messageBubblePanel.add(avatarPanel, BorderLayout.EAST);
|
||||
} else {
|
||||
// 他人消息:左对齐,白色气泡
|
||||
messageLabel.setBackground(otherColor);
|
||||
messageLabel.setForeground(Color.decode(DesignToken.COLOR_FONT_BLACK));
|
||||
messageLabel.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(10, 15, 10, 15)));
|
||||
infoLabel.setHorizontalAlignment(SwingConstants.LEFT);
|
||||
contentPanel.add(infoLabel, BorderLayout.NORTH);
|
||||
contentPanel.add(messageLabel, BorderLayout.CENTER);
|
||||
|
||||
// 左对齐布局
|
||||
messageBubblePanel.add(avatarPanel, BorderLayout.WEST);
|
||||
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// 设置消息气泡的最大宽度(防止过宽)
|
||||
int maxBubbleWidth = 350;
|
||||
messageLabel.setSize(new Dimension(maxBubbleWidth, Integer.MAX_VALUE));
|
||||
int preferredHeight = messageLabel.getPreferredSize().height;
|
||||
messageLabel.setPreferredSize(new Dimension(maxBubbleWidth, preferredHeight));
|
||||
|
||||
// 将其添加到消息列表当中
|
||||
bubbles.add(messageBubblePanel);
|
||||
|
||||
// 添加到消息面板顶部(新消息在顶部显示)
|
||||
messagePanel.add(messageBubblePanel);
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有消息气泡(bubbles)
|
||||
* 清空输入栏的内容
|
||||
*/
|
||||
public void removeAllMessageBubble() {
|
||||
for (JPanel panel : bubbles) {
|
||||
messagePanel.remove(panel);
|
||||
}
|
||||
bubbles.clear();
|
||||
inputArea.setText("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据群聊id在DataManager中选择并加载群聊信息
|
||||
*
|
||||
* @param groupId 群聊id
|
||||
*/
|
||||
public void init(String groupId) {
|
||||
this.removeAllMessageBubble();
|
||||
List<String> messages = LocalData.get().getChatMsg(groupId);
|
||||
for (String text : messages) {
|
||||
String[] msgs = MsgUtil.splitMsg(text);
|
||||
|
||||
if (msgs[0].equals(LocalData.get().getId())) {
|
||||
addUserMessage(msgs[2]);
|
||||
} else {
|
||||
addMessageBubble(msgs[0].equals(LocalData.get().getId()), msgs[1], msgs[2]);
|
||||
}
|
||||
}
|
||||
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
}
|
||||
|
||||
private void scrollToBottom() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JScrollBar vertical = messageScrollPane.getVerticalScrollBar();
|
||||
vertical.setValue(vertical.getMaximum());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.util.DesignToken;
|
||||
import client.view.util.LimitSizePanel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
/**
|
||||
* 内容界面组件
|
||||
* 用于显示聊天、群聊、好友个人信息等内容。
|
||||
* 点击不同选项可以切换到对应的功能界面。
|
||||
*/
|
||||
public class ContentView extends LimitSizePanel {
|
||||
private ChatInfoView chatInfoView;
|
||||
private GroupInfoView groupInfoView;
|
||||
|
||||
public ContentView() {
|
||||
super(CONTENT_PANEL_WIDTH_MIN);
|
||||
this.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
||||
this.setLayout(new BorderLayout());
|
||||
|
||||
chatInfoView = ChatInfoView.get();
|
||||
chatInfoView.setPreferredSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
|
||||
chatInfoView.setMinimumSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
|
||||
|
||||
groupInfoView = GroupInfoView.get();
|
||||
groupInfoView.setPreferredSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, this.getHeight()));
|
||||
|
||||
exchangeToBlank();
|
||||
}
|
||||
|
||||
public void exchangeToBlank() {
|
||||
this.removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为群聊组件
|
||||
* 依据groupId来从DataManager中获取群聊信息
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToChatRoom(String groupId) {
|
||||
this.removeAll();
|
||||
chatInfoView.init(groupId);
|
||||
this.add(chatInfoView, BorderLayout.CENTER);
|
||||
|
||||
if (LocalData.get().getGroupData(groupId) != null) {
|
||||
groupInfoView.updateInfo();
|
||||
this.add(groupInfoView, BorderLayout.EAST);
|
||||
}
|
||||
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为好友个人信息组件
|
||||
* 依据userId和userName来创建FriendProfileView组件
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToFriendProfile(String userId, String userName) {
|
||||
this.removeAll();
|
||||
FriendProfileView profileView = new FriendProfileView(userId, userName);
|
||||
this.add(profileView, BorderLayout.CENTER);
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为设置组件
|
||||
* 依据type来创建SettingsView组件
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToSettings(String type) {
|
||||
this.removeAll();
|
||||
SettingsView settingsView = new SettingsView(type);
|
||||
this.add(settingsView, BorderLayout.CENTER);
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.view.MainPage;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* 好友列表项组件
|
||||
* 用于显示好友列表中的每个好友项,包括好友头像、好友名称等。
|
||||
* 点击好友项可以进入好友个人信息界面。
|
||||
*/
|
||||
public class FriendListItem extends JPanel {
|
||||
private final String userId;
|
||||
private final String userName;
|
||||
|
||||
JLabel icon;
|
||||
JPanel centerPanel;
|
||||
JLabel titleLabel;
|
||||
|
||||
/**
|
||||
* 构造好友列表项组件
|
||||
*
|
||||
* @param userId 好友用户ID
|
||||
* @param userName 好友用户名
|
||||
*/
|
||||
public FriendListItem(String userId, String userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
|
||||
// 设置布局管理器
|
||||
setLayout(new BorderLayout(10, 0));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
|
||||
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
|
||||
setPreferredSize(new Dimension(200, 70));
|
||||
|
||||
// 左侧图标
|
||||
icon = new JLabel(new CircleCharIcon2(Color.LIGHT_GRAY, Color.WHITE,
|
||||
userName.substring(0, 1).toUpperCase(), 40));
|
||||
icon.setPreferredSize(new Dimension(40, 40));
|
||||
|
||||
// 中间区域 - 好友名称
|
||||
centerPanel = new JPanel(new GridLayout(1, 1));
|
||||
titleLabel = new JLabel(userName);
|
||||
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
centerPanel.add(titleLabel);
|
||||
|
||||
// 组装
|
||||
this.add(icon, BorderLayout.WEST);
|
||||
this.add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
enterFriendProfile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("List.selectionBackground")); // 悬停效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("List.selectionInactiveBackground")); // 点击效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (contains(e.getPoint())) {
|
||||
setBackground(UIManager.getColor("List.selectionBackground"));
|
||||
} else {
|
||||
setBackground(UIManager.getColor("Panel.background"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入好友个人信息界面
|
||||
* 点击好友项时,切换到好友个人信息界面,显示与该好友相关的个人信息。
|
||||
*/
|
||||
public void enterFriendProfile() {
|
||||
MainPage.get().exchangeToFriendProfile(userId, userName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.UserData;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 好友个人信息视图
|
||||
* 显示好友的头像、名称和ID,以及操作按钮(发送消息、删除好友)
|
||||
* 好友头像:显示好友的圆形头像
|
||||
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
|
||||
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
|
||||
* 发送消息按钮:点击后可以发送消息给好友
|
||||
* 删除好友按钮:点击后可以删除好友关系
|
||||
*/
|
||||
public class FriendProfileView extends JPanel {
|
||||
private String userId;
|
||||
private String userName;
|
||||
|
||||
public FriendProfileView(String userId, String userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
initUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化UI组件
|
||||
* 设置布局为网格Bag布局,背景颜色为白色
|
||||
* 好友头像:显示好友的圆形头像,大小为80x80
|
||||
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
|
||||
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
|
||||
* 发送消息按钮:点击后可以发送消息给好友,大小为120x40,背景颜色为蓝色,文字颜色为白色
|
||||
* 删除好友按钮:点击后可以删除好友关系,大小为120x40,背景颜色为红色,文字颜色为白色
|
||||
*/
|
||||
private void initUI() {
|
||||
setLayout(new GridBagLayout());
|
||||
setBackground(Color.WHITE);
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.CENTER;
|
||||
|
||||
// 好友头像
|
||||
JLabel icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
userName.substring(0, 1).toUpperCase(), 80));
|
||||
add(icon, gbc);
|
||||
|
||||
// 好友名称
|
||||
gbc.gridy++;
|
||||
JLabel nameLabel = new JLabel(userName);
|
||||
nameLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
add(nameLabel, gbc);
|
||||
|
||||
// 用户ID
|
||||
gbc.gridy++;
|
||||
JLabel idLabel = new JLabel("ID: " + userId);
|
||||
idLabel.setForeground(Color.GRAY);
|
||||
add(idLabel, gbc);
|
||||
|
||||
// 获取并显示详细信息
|
||||
UserData friendData = LocalData.get().getUserDetail(userId);
|
||||
if (friendData != null) {
|
||||
addInfoLabel(friendData.getEmail(), gbc);
|
||||
addInfoLabel(friendData.getBirthday(), gbc);
|
||||
addInfoLabel(friendData.getAddress(), gbc);
|
||||
addInfoLabel(friendData.getSignature(), gbc);
|
||||
}
|
||||
|
||||
// 发送消息按钮
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(30, 10, 10, 10);
|
||||
JButton sendBtn = new JButton("发送消息");
|
||||
sendBtn.setPreferredSize(new Dimension(120, 40));
|
||||
sendBtn.setBackground(new Color(0, 122, 255));
|
||||
sendBtn.setForeground(Color.WHITE);
|
||||
sendBtn.setFocusPainted(false);
|
||||
sendBtn.addActionListener(e -> {
|
||||
// 更新消息列表
|
||||
SecondaryOptionView.get().updateMessageList(userId, userName, "", 0);
|
||||
|
||||
// 更新当前聊天ID
|
||||
client.service.LocalData.get().setCurrentChatId(userId);
|
||||
|
||||
// 切换到聊天房间
|
||||
client.view.MainPage.get().exchangeToChatRoom(userId);
|
||||
});
|
||||
add(sendBtn, gbc);
|
||||
|
||||
// 好友操作按钮
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
JButton deleteBtn = new JButton("删除好友");
|
||||
deleteBtn.setPreferredSize(new Dimension(120, 40));
|
||||
deleteBtn.setBackground(new Color(220, 53, 69));
|
||||
deleteBtn.setForeground(Color.WHITE);
|
||||
deleteBtn.setFocusPainted(false);
|
||||
deleteBtn.addActionListener(e -> {
|
||||
JOptionPane.showMessageDialog(this, "删除好友功能开发中...");
|
||||
});
|
||||
add(deleteBtn, gbc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加详细信息标签
|
||||
* 检查文本是否为空,如果不为空则添加到面板中
|
||||
* 标签字体为默认字体,大小为14号,颜色为深灰色
|
||||
*
|
||||
* @param text 要添加的详细信息文本
|
||||
* @param gbc 网格BagConstraints对象,用于布局
|
||||
*/
|
||||
private void addInfoLabel(String text, GridBagConstraints gbc) {
|
||||
if (text != null && !text.isEmpty()) {
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(2, 10, 2, 10);
|
||||
JLabel label = new JLabel(text);
|
||||
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
label.setForeground(Color.DARK_GRAY);
|
||||
add(label, gbc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.GroupData;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 聊天信息组件,用于展示当前聊天室的信息
|
||||
*/
|
||||
public class GroupInfoView extends JScrollPane {
|
||||
private static volatile GroupInfoView instance;
|
||||
|
||||
public static GroupInfoView get() {
|
||||
if (instance == null) {
|
||||
synchronized (GroupInfoView.class) {
|
||||
if (instance == null) {
|
||||
instance = new GroupInfoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final JPanel mainPanel;
|
||||
private final JPanel groupInfoPanel;
|
||||
private final JPanel groupMemberPanel;
|
||||
|
||||
public GroupInfoView() {
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); // 垂直布局
|
||||
|
||||
setPreferredSize(new Dimension(DesignToken.INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
|
||||
setBackground(Color.GRAY);
|
||||
|
||||
// 设置滚动策略
|
||||
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
groupInfoPanel = createGroupInfoPanel();
|
||||
groupMemberPanel = createGroupMemberPanel();
|
||||
|
||||
mainPanel.add(groupInfoPanel);
|
||||
|
||||
mainPanel.add(groupInfoPanel);
|
||||
mainPanel.add(createInviteButton());
|
||||
mainPanel.add(createExitButton());
|
||||
mainPanel.add(groupMemberPanel);
|
||||
|
||||
mainPanel.add(groupMemberPanel);
|
||||
|
||||
mainPanel.setMinimumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
mainPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
|
||||
|
||||
this.setViewportView(mainPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建聊天成员信息面板
|
||||
*/
|
||||
public JPanel createGroupMemberPanel() {
|
||||
JPanel memberInfoPanel = new JPanel();
|
||||
memberInfoPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
|
||||
|
||||
memberInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
|
||||
memberInfoPanel.setLayout(new BoxLayout(memberInfoPanel, BoxLayout.Y_AXIS));
|
||||
|
||||
// 添加成员标题
|
||||
JLabel memberTitle = new JLabel("群成员");
|
||||
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
memberTitle.setForeground(Color.WHITE);
|
||||
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
|
||||
memberInfoPanel.add(memberTitle);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.DARK_GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
|
||||
|
||||
memberInfoPanel.add(separator);
|
||||
|
||||
return memberInfoPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群信息组件
|
||||
*/
|
||||
public JPanel createGroupInfoPanel() {
|
||||
JPanel groupInfoPanel = new JPanel();
|
||||
groupInfoPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
|
||||
groupInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
groupInfoPanel.setLayout(new BoxLayout(groupInfoPanel, BoxLayout.Y_AXIS));
|
||||
|
||||
// 添加标题
|
||||
JLabel titleLabel = new JLabel("群聊信息");
|
||||
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
|
||||
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
|
||||
groupInfoPanel.add(titleLabel);
|
||||
|
||||
return groupInfoPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个邀请成员的按钮
|
||||
*/
|
||||
public JButton createInviteButton() {
|
||||
JButton inviteButton = new JButton("邀请");
|
||||
inviteButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
|
||||
inviteButton.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
inviteButton.setMaximumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
inviteButton.setMargin(
|
||||
new Insets(5, 10, 5, 10));
|
||||
|
||||
inviteButton.addActionListener(e -> MainPage.get().showGroupInviteDialog());
|
||||
return inviteButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个退出群聊的按钮
|
||||
*/
|
||||
public JButton createExitButton() {
|
||||
JButton exitButton = new JButton("退出");
|
||||
exitButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
|
||||
exitButton.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
exitButton.setMaximumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
exitButton.setMargin(
|
||||
new Insets(5, 10, 5, 10));
|
||||
|
||||
exitButton.addActionListener(e -> {
|
||||
ChatSender.addMsg(Wrapper.groupQuitRequest(
|
||||
LocalData.get().getId(),
|
||||
LocalData.get().getCurrentChatId()));
|
||||
});
|
||||
return exitButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 groupInfoPanel 和 groupMemberPanel 这两个组件
|
||||
*/
|
||||
public void updateInfo() {
|
||||
groupInfoPanel.removeAll();
|
||||
groupMemberPanel.removeAll();
|
||||
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
if (currentChatId == null || currentChatId.isEmpty()) {
|
||||
// 如果没有当前群聊,显示提示信息
|
||||
JLabel noGroupLabel = new JLabel("未选择群聊");
|
||||
noGroupLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
noGroupLabel.setForeground(Color.GRAY);
|
||||
groupInfoPanel.add(noGroupLabel);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
GroupData groupData = LocalData.get().getGroupData(currentChatId);
|
||||
if (groupData == null) {
|
||||
JLabel errorLabel = new JLabel("群聊数据不存在");
|
||||
errorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
errorLabel.setForeground(Color.RED);
|
||||
groupInfoPanel.add(errorLabel);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新群聊信息面板
|
||||
updateGroupInfoPanel(groupData);
|
||||
|
||||
// 更新群成员面板
|
||||
updateGroupMemberPanel(groupData);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群聊信息面板内容
|
||||
*/
|
||||
private void updateGroupInfoPanel(GroupData groupData) {
|
||||
// 添加标题
|
||||
JLabel titleLabel = new JLabel("群聊信息");
|
||||
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
|
||||
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
|
||||
groupInfoPanel.add(titleLabel);
|
||||
|
||||
// 群聊名称
|
||||
JLabel nameLabel = new JLabel("群名: " + groupData.getGroupName());
|
||||
nameLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
||||
nameLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
|
||||
groupInfoPanel.add(nameLabel);
|
||||
|
||||
// 群聊ID
|
||||
JLabel idLabel = new JLabel("群ID: " + groupData.getGroupId());
|
||||
idLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
idLabel.setForeground(Color.DARK_GRAY);
|
||||
idLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
|
||||
groupInfoPanel.add(idLabel);
|
||||
|
||||
// 成员数量
|
||||
JLabel memberCountLabel = new JLabel("成员: " + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + "人");
|
||||
memberCountLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
memberCountLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
groupInfoPanel.add(memberCountLabel);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 30, 1));
|
||||
groupInfoPanel.add(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群成员面板内容
|
||||
*/
|
||||
private void updateGroupMemberPanel(GroupData groupData) {
|
||||
// 添加成员标题
|
||||
JLabel memberTitle = new JLabel("群成员 (" + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + ")");
|
||||
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
memberTitle.setForeground(Color.WHITE);
|
||||
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
|
||||
groupMemberPanel.add(memberTitle);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.DARK_GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
|
||||
groupMemberPanel.add(separator);
|
||||
|
||||
if (groupData.getMembers() == null || groupData.getMembers().isEmpty()) {
|
||||
JLabel noMemberLabel = new JLabel("暂无成员");
|
||||
noMemberLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
noMemberLabel.setForeground(Color.LIGHT_GRAY);
|
||||
noMemberLabel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0));
|
||||
groupMemberPanel.add(noMemberLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加成员列表
|
||||
for (GroupData.GroupMember memberId : groupData.getMembers()) {
|
||||
JPanel memberItemPanel =
|
||||
createMemberItemPanel(memberId.id);
|
||||
groupMemberPanel.add(memberItemPanel);
|
||||
}
|
||||
|
||||
// 添加底部空白,确保内容居中
|
||||
groupMemberPanel.add(Box.createVerticalGlue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个成员信息面板
|
||||
*/
|
||||
private JPanel createMemberItemPanel(String memberId) {
|
||||
JPanel memberItemPanel = new JPanel();
|
||||
memberItemPanel.setLayout(new BoxLayout(memberItemPanel, BoxLayout.X_AXIS));
|
||||
memberItemPanel.setBackground(new Color(40, 40, 40));
|
||||
memberItemPanel.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15));
|
||||
memberItemPanel.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 50));
|
||||
memberItemPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
|
||||
// 成员头像(使用圆形标签模拟)
|
||||
JLabel avatarLabel = new JLabel();
|
||||
avatarLabel.setOpaque(true);
|
||||
avatarLabel.setBackground(getMemberColor(memberId));
|
||||
avatarLabel.setPreferredSize(new Dimension(30, 30));
|
||||
avatarLabel.setMinimumSize(new Dimension(30, 30));
|
||||
avatarLabel.setMaximumSize(new Dimension(30, 30));
|
||||
avatarLabel.setBorder(BorderFactory.createLineBorder(Color.WHITE, 1));
|
||||
|
||||
// 设置圆形头像
|
||||
avatarLabel.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.WHITE, 1),
|
||||
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||
));
|
||||
|
||||
// 成员信息
|
||||
JPanel infoPanel = new JPanel();
|
||||
infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS));
|
||||
infoPanel.setBackground(new Color(40, 40, 40));
|
||||
infoPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
|
||||
|
||||
// 成员名称
|
||||
String memberName = LocalData.get().getUserName(memberId);
|
||||
if (memberName == null) {
|
||||
memberName = "用户" + memberId.substring(0, Math.min(6, memberId.length()));
|
||||
}
|
||||
|
||||
JLabel nameLabel = new JLabel(memberName);
|
||||
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
||||
nameLabel.setForeground(Color.WHITE);
|
||||
nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 成员ID
|
||||
JLabel idLabel = new JLabel("ID: " + memberId.substring(0, Math.min(10, memberId.length())));
|
||||
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 10));
|
||||
idLabel.setForeground(Color.LIGHT_GRAY);
|
||||
idLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
infoPanel.add(nameLabel);
|
||||
infoPanel.add(idLabel);
|
||||
|
||||
memberItemPanel.add(avatarLabel);
|
||||
memberItemPanel.add(infoPanel);
|
||||
memberItemPanel.add(Box.createHorizontalGlue());
|
||||
|
||||
// 如果是当前用户,添加标识
|
||||
if (memberId.equals(LocalData.get().getId())) {
|
||||
JLabel meLabel = new JLabel("(我)");
|
||||
meLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
|
||||
meLabel.setForeground(new Color(100, 150, 255));
|
||||
memberItemPanel.add(meLabel);
|
||||
}
|
||||
|
||||
return memberItemPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID生成固定颜色(用于头像背景)
|
||||
*/
|
||||
private Color getMemberColor(String memberId) {
|
||||
// 简单的哈希算法生成固定颜色
|
||||
int hash = memberId.hashCode();
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
|
||||
// 确保颜色不太暗
|
||||
r = Math.max(r, 50);
|
||||
g = Math.max(g, 50);
|
||||
b = Math.max(b, 50);
|
||||
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import client.view.util.RoundedRectCharIcon;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
// 群聊列表项组件
|
||||
public class GroupListItem extends JPanel implements Comparable<String> {
|
||||
private int unread;
|
||||
private final String groupId;
|
||||
|
||||
JLabel icon;
|
||||
JLabel badge;
|
||||
JPanel centerPanel;
|
||||
JLabel titleLabel;
|
||||
JPanel rightPanel; // 用于放置徽章
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param groupId 群聊ID
|
||||
* @param title 群聊标题
|
||||
* @param unread 未读消息数量
|
||||
*/
|
||||
public GroupListItem(String groupId, String title, int unread) {
|
||||
this.groupId = groupId;
|
||||
this.unread = unread;
|
||||
|
||||
// 设置布局管理器
|
||||
setLayout(new BorderLayout(10, 0));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
|
||||
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
|
||||
setPreferredSize(new Dimension(200, 70));
|
||||
|
||||
// 左侧图标
|
||||
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
|
||||
if (isGroup) {
|
||||
icon = new JLabel(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
|
||||
title.substring(0, 1).toUpperCase(), 40));
|
||||
} else {
|
||||
icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
title.substring(0, 1).toUpperCase(), 40));
|
||||
}
|
||||
icon.setPreferredSize(new Dimension(40, 40));
|
||||
|
||||
// 中间区域 - 群聊标题
|
||||
centerPanel = new JPanel(new GridLayout(1, 1));
|
||||
titleLabel = new JLabel(title);
|
||||
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
centerPanel.add(titleLabel);
|
||||
|
||||
// 右侧区域 - 未读消息徽章
|
||||
rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
|
||||
rightPanel.setOpaque(false);
|
||||
|
||||
badge = new JLabel();
|
||||
badge.setForeground(Color.WHITE);
|
||||
badge.setBackground(Color.RED);
|
||||
badge.setOpaque(true);
|
||||
badge.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
badge.setFont(new Font("Microsoft YaHei", Font.BOLD, 10));
|
||||
badge.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
|
||||
badge.setPreferredSize(new Dimension(20, 20));
|
||||
|
||||
updateBadge(); // 初始化徽章显示状态
|
||||
rightPanel.add(badge);
|
||||
|
||||
// 组装
|
||||
this.add(icon, BorderLayout.WEST);
|
||||
this.add(centerPanel, BorderLayout.CENTER);
|
||||
this.add(rightPanel, BorderLayout.EAST);
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
enterGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
setBackground(new Color(240, 240, 240)); // 悬停效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
setBackground(new Color(220, 220, 220)); // 点击效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (contains(e.getPoint())) {
|
||||
setBackground(new Color(240, 240, 240));
|
||||
} else {
|
||||
setBackground(UIManager.getColor("Panel.background"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新徽章显示
|
||||
*/
|
||||
private void updateBadge() {
|
||||
if (unread > 0) {
|
||||
String badgeText = unread > 99 ? "99+" : String.valueOf(unread);
|
||||
badge.setText(badgeText);
|
||||
badge.setVisible(true);
|
||||
|
||||
// 根据文本长度调整徽章大小
|
||||
FontMetrics fm = badge.getFontMetrics(badge.getFont());
|
||||
int width = fm.stringWidth(badgeText) + 12;
|
||||
badge.setPreferredSize(new Dimension(width, 20));
|
||||
} else {
|
||||
badge.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件
|
||||
* 将组件上对应的信息修改。
|
||||
* newUnread 是加在原有的unread上的
|
||||
*/
|
||||
public void updateUI(String name, int newUnread) {
|
||||
titleLabel.setText(name);
|
||||
|
||||
// 更新图标的首字母显示
|
||||
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
|
||||
if (isGroup) {
|
||||
icon.setIcon(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
|
||||
name.substring(0, 1).toUpperCase(), 40));
|
||||
} else {
|
||||
icon.setIcon(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
name.substring(0, 1).toUpperCase(), 40));
|
||||
}
|
||||
|
||||
unread += newUnread;
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件
|
||||
* 当点击之后,使用UIUpdater来更新ContentView的UI,
|
||||
* 使得其加载新的群聊信息
|
||||
* 清空unread为0
|
||||
* UIUpdater在ContentView展示当前群聊信息的时候不会更新这个群聊的未读信息数量。
|
||||
*/
|
||||
public void enterGroup() {
|
||||
unread = 0;
|
||||
LocalData.get().setCurrentChatId(groupId);
|
||||
|
||||
updateBadge(); // 更新徽章显示
|
||||
|
||||
String name;
|
||||
if (LocalData.get().getFriends().containsKey(groupId)) {
|
||||
name = LocalData.get().getFriends().get(groupId);
|
||||
} else {
|
||||
name = LocalData.get().getGroupName(groupId);
|
||||
}
|
||||
|
||||
updateUI(name, 0);
|
||||
|
||||
MainPage.get().exchangeToChatRoom(groupId);
|
||||
|
||||
if (LocalData.get().getGroupData(groupId) != null) {
|
||||
System.out.println(
|
||||
"进入群聊:" + groupId +
|
||||
",人数:" + LocalData.get().getGroupData(groupId).getMemberCount() +
|
||||
", 信息数量: " + LocalData.get().getChatMsg(groupId).size());
|
||||
} else {
|
||||
System.out.println("进入私聊:" + groupId);
|
||||
}
|
||||
|
||||
MainPage.get().revalidate(); // 重新计算布局
|
||||
MainPage.get().repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
*/
|
||||
public int getUnread() {
|
||||
return unread;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组ID
|
||||
*/
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现Comparable接口,用于排序
|
||||
* 按照群组ID进行排序
|
||||
*
|
||||
* @param o 要比较的对象
|
||||
* @return 比较结果
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(String o) {
|
||||
return this.groupId.compareTo(o);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.view.MainPage;
|
||||
import client.view.util.LimitSizePanel;
|
||||
|
||||
import client.service.LocalData;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static client.view.util.DesignToken.SECONDARY_PANEL_WIDTH_MIN;
|
||||
|
||||
/**
|
||||
* 二级菜单栏组件
|
||||
* 用于显示聊天、好友、设置等二级选项。
|
||||
* 点击不同选项可以切换到对应的功能界面。
|
||||
*/
|
||||
public class SecondaryOptionView extends LimitSizePanel {
|
||||
private static volatile SecondaryOptionView instance;
|
||||
|
||||
public static SecondaryOptionView get() {
|
||||
if (instance == null) {
|
||||
synchronized (SecondaryOptionView.class) {
|
||||
if (instance == null) {
|
||||
instance = new SecondaryOptionView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 二级选项模式
|
||||
private enum Mode {
|
||||
MESSAGE, FRIEND, GROUP, SETTING
|
||||
}
|
||||
|
||||
// 当前二级选项模式
|
||||
private Mode currentMode = Mode.MESSAGE;
|
||||
// 这是一个群聊ID,组件的映射
|
||||
private Map<String, GroupListItem> listItems;
|
||||
// 关于好友ID,组件的映射
|
||||
private JPanel chatContainer;
|
||||
// 好友列表滚动面板
|
||||
private JScrollPane scrollPane;
|
||||
// 创建群聊按钮
|
||||
private JButton createGroupButton;
|
||||
// 二级选项标题标签
|
||||
private JLabel titleLabel;
|
||||
|
||||
private SecondaryOptionView() {
|
||||
super(SECONDARY_PANEL_WIDTH_MIN);
|
||||
|
||||
init();
|
||||
|
||||
listItems = new LinkedHashMap<>();
|
||||
|
||||
// 添加右侧边框分割线
|
||||
this.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")));
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// 创建顶部面板
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // 增加内边距
|
||||
|
||||
titleLabel = new JLabel("消息"); // 或者 "群聊"/"好友",根据当前视图动态变化更好,这里先用通用标题
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
|
||||
// 调整“+”按钮样式,使其更像一个功能图标
|
||||
createGroupButton.setMargin(new Insets(2, 6, 2, 6));
|
||||
createGroupButton.setFocusPainted(false);
|
||||
|
||||
topPanel.add(titleLabel, BorderLayout.WEST);
|
||||
topPanel.add(createGroupButton, BorderLayout.EAST);
|
||||
|
||||
// 创建群聊容器
|
||||
chatContainer = new JPanel();
|
||||
chatContainer.setLayout(new BoxLayout(chatContainer, BoxLayout.Y_AXIS));
|
||||
|
||||
// 创建滚动面板
|
||||
scrollPane = new JScrollPane(chatContainer);
|
||||
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder()); // 移除默认边框
|
||||
|
||||
// 自定义滚动条UI
|
||||
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
|
||||
verticalScrollBar.setUnitIncrement(16);
|
||||
verticalScrollBar.setPreferredSize(new Dimension(10, 0));
|
||||
|
||||
// 组装界面
|
||||
JPanel headerContainer = new JPanel(new BorderLayout());
|
||||
headerContainer.add(topPanel, BorderLayout.CENTER);
|
||||
// 添加底部分割线,同时为了更好的层次感,也可以考虑添加顶部分割线(如果需要与标题栏分隔)
|
||||
// 这里我们给上下都添加分割线,确保 headerContainer 与上面的 Window Title 和下面的 List 都有分隔线
|
||||
headerContainer
|
||||
.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, UIManager.getColor("Component.borderColor")));
|
||||
|
||||
this.add(headerContainer, BorderLayout.NORTH);
|
||||
this.add(scrollPane, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个用于创建群聊的按钮组件
|
||||
* 按下这个组件后弹出一个创建群聊的对话框,填写完成后进行创建群聊的操作(调用DataManager的createGroupChat方法)
|
||||
* 如果失败,则放弃创建群聊。
|
||||
*
|
||||
* @return 群聊创建按钮组件
|
||||
*/
|
||||
private JButton createGroupCreateBtn() {
|
||||
JButton groupCreateBtn = new JButton("+");
|
||||
return groupCreateBtn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有组件
|
||||
* 读取DataManager的信息,而后群聊信息创建为GroupListItem,加入到类中对应的列表中
|
||||
* 并将其添加到chatContainer中
|
||||
* 同样,好友信息创建为FriendListItem,加入到类中对应的列表中
|
||||
* 并添加到friendContainer中
|
||||
* 最后使用exchangeToGroupChat将群聊列表加入到聊天项容器中
|
||||
* 使用createGroupCreateBtn创建群聊创建按钮,并添加到chatContainer中
|
||||
*/
|
||||
public void init() {
|
||||
createGroupButton = createGroupCreateBtn();
|
||||
createGroupButton.addActionListener(e -> {
|
||||
showAddMenu(createGroupButton);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加菜单
|
||||
* 根据当前模式(群聊/好友/设置),显示不同的添加选项
|
||||
* 例如,在群聊模式下,显示创建群聊和加入群聊选项
|
||||
* 在好友模式下,显示添加好友选项
|
||||
* 在设置模式下,显示不同的设置选项
|
||||
*/
|
||||
private void showAddMenu(Component invoker) {
|
||||
JPopupMenu popupMenu = new JPopupMenu();
|
||||
|
||||
if (currentMode == Mode.GROUP) {
|
||||
JMenuItem createGroupItem = new JMenuItem("创建群聊");
|
||||
createGroupItem.addActionListener(e -> MainPage.get().showGroupCreateDialog());
|
||||
popupMenu.add(createGroupItem);
|
||||
|
||||
JMenuItem joinGroupItem = new JMenuItem("加入群聊");
|
||||
joinGroupItem.addActionListener(e -> {
|
||||
MainPage.get().showJoinGroupDialog();
|
||||
});
|
||||
popupMenu.add(joinGroupItem);
|
||||
|
||||
} else if (currentMode == Mode.FRIEND) {
|
||||
JMenuItem addFriendItem = new JMenuItem("添加好友");
|
||||
addFriendItem.addActionListener(e -> {
|
||||
MainPage.get().showAddFriendDialog();
|
||||
});
|
||||
popupMenu.add(addFriendItem);
|
||||
}
|
||||
|
||||
popupMenu.show(invoker, 0, invoker.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息列表的UI
|
||||
* 根据传入的groupId,更新对应groupListItems的群聊列表项的UI
|
||||
* 这里需要先删除createGroupButton按钮,而后进行更新操作后再添加回来。
|
||||
* 如果没有对应的id,则创建新的GroupListItem,并添加到groupListItems中
|
||||
*/
|
||||
public void updateGroupList(String groupId, String title, int unreadCount) {
|
||||
if (listItems.containsKey(groupId)) {
|
||||
listItems.get(groupId).updateUI(title, unreadCount);
|
||||
} else {
|
||||
GroupListItem item = new GroupListItem(groupId, title, 0);
|
||||
listItems.put(groupId, item);
|
||||
}
|
||||
|
||||
// 只有在 MESSAGE 模式下才更新 UI 容器
|
||||
if (currentMode == Mode.MESSAGE) {
|
||||
// 简单处理:重新加载所有 item 保证顺序,或者只添加新的
|
||||
// 为了简单,如果它不在容器里,加进去
|
||||
GroupListItem item = listItems.get(groupId);
|
||||
boolean alreadyIn = false;
|
||||
for (Component c : chatContainer.getComponents()) {
|
||||
if (c == item) {
|
||||
alreadyIn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyIn) {
|
||||
chatContainer.add(item);
|
||||
}
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息列表(兼容群聊和私聊)
|
||||
*/
|
||||
public void updateMessageList(String id, String name, String content, int unread) {
|
||||
updateGroupList(id, name, unread);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果当前处于群聊模式,刷新群聊列表
|
||||
* 用于处理新加入群聊时的列表更新
|
||||
*/
|
||||
public void refreshIfInGroupMode() {
|
||||
if (currentMode == Mode.GROUP) {
|
||||
exchangeToGroupList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定的群聊列表
|
||||
* 如果当前模式是CHAT,且groupId存在于listItems中,
|
||||
* 则从chatContainer中移除对应的GroupListItem组件,
|
||||
* 并从listItems中删除该条目。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void removeGroupListItem(String groupId) {
|
||||
if (listItems.containsKey(groupId)) {
|
||||
GroupListItem item = listItems.get(groupId);
|
||||
listItems.remove(groupId);
|
||||
if (currentMode == Mode.MESSAGE || currentMode == Mode.GROUP) {
|
||||
chatContainer.remove(item);
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置列表模式
|
||||
* 将当前模式设置为SETTING,更新标题为"设置",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后添加个人信息和关于软件的设置项按钮到聊天项容器中。
|
||||
* 最后调用revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToSettingList() {
|
||||
currentMode = Mode.SETTING;
|
||||
titleLabel.setText("设置");
|
||||
createGroupButton.setVisible(false);
|
||||
chatContainer.removeAll();
|
||||
|
||||
// 添加设置项
|
||||
chatContainer.add(createSettingItem("个人信息", "info"));
|
||||
chatContainer.add(createSettingItem("关于软件", "about"));
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
|
||||
MainPage.get().exchangeToBlankContent(); // 右侧清空或显示默认页
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个设置项按钮
|
||||
* 按钮的文本为text,类型为type。
|
||||
* 按钮的对齐方式为居中对齐,最大宽度为Integer.MAX_VALUE,高度为50。
|
||||
* 点击按钮时,调用MainPage的exchangeToSettings方法,传入type参数。
|
||||
*
|
||||
* @param text 按钮的文本
|
||||
* @param type 按钮的类型
|
||||
* @return 一个设置项按钮组件
|
||||
*/
|
||||
private JButton createSettingItem(String text, String type) {
|
||||
JButton btn = new JButton(text);
|
||||
btn.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
btn.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50));
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBackground(Color.WHITE);
|
||||
btn.addActionListener(e -> MainPage.get().exchangeToSettings(type));
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到消息列表模式
|
||||
* 将当前模式设置为MESSAGE,更新标题为"消息",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历listItems中的所有GroupListItem组件,添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToMessageList() {
|
||||
currentMode = Mode.MESSAGE;
|
||||
titleLabel.setText("消息");
|
||||
createGroupButton.setVisible(false);
|
||||
chatContainer.removeAll();
|
||||
|
||||
// 恢复消息列表
|
||||
for (GroupListItem item : listItems.values()) {
|
||||
chatContainer.add(item);
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到群聊列表模式
|
||||
* 将当前模式设置为GROUP,更新标题为"群聊",显示创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历LocalData中的所有群聊,创建GroupListItem组件并添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToGroupList() {
|
||||
currentMode = Mode.GROUP;
|
||||
titleLabel.setText("群聊");
|
||||
createGroupButton.setVisible(true);
|
||||
chatContainer.removeAll();
|
||||
|
||||
java.util.List<server.data.GroupData> groups = LocalData.get().getAllGroups();
|
||||
if (groups != null) {
|
||||
for (server.data.GroupData group : groups) {
|
||||
// 这里我们复用GroupListItem,未读数设为0
|
||||
GroupListItem item = new GroupListItem(group.getGroupId(), group.getGroupName(), 0);
|
||||
chatContainer.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到好友列表模式
|
||||
* 将当前模式设置为FRIEND,更新标题为"好友",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历LocalData中的好友列表,创建FriendListItem组件并添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToFriendList() {
|
||||
currentMode = Mode.FRIEND;
|
||||
titleLabel.setText("好友");
|
||||
createGroupButton.setVisible(true); // 显示加号按钮
|
||||
chatContainer.removeAll();
|
||||
|
||||
Map<String, String> friends = LocalData.get().getFriends();
|
||||
if (friends != null) {
|
||||
for (Map.Entry<String, String> entry : friends.entrySet()) {
|
||||
FriendListItem item = new FriendListItem(entry.getKey(), entry.getValue());
|
||||
chatContainer.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
|
||||
MainPage.get().exchangeToBlankContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.UserData;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 设置界面组件
|
||||
* 用于显示用户个人信息和关于 LocalChatApp 的设置选项。
|
||||
* 包括用户ID、用户名、退出登录等功能。
|
||||
*/
|
||||
public class SettingsView extends JPanel {
|
||||
|
||||
public SettingsView(String type) {
|
||||
initUI(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化设置界面组件
|
||||
* 用于根据界面类型显示不同的设置内容。
|
||||
* 如果是 "info" 类型,显示用户个人信息,包括用户ID和用户名。
|
||||
* 如果是 "about" 类型,显示关于 LocalChatApp 的信息,包括版本号等。
|
||||
* @param type 界面类型,"info" 显示个人信息,"about" 显示关于 LocalChatApp 的信息
|
||||
*/
|
||||
private void initUI(String type) {
|
||||
setLayout(new BorderLayout());
|
||||
setBackground(Color.WHITE);
|
||||
|
||||
JPanel contentPanel = new JPanel();
|
||||
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
|
||||
contentPanel.setBorder(BorderFactory.createEmptyBorder(40, 40, 40, 40));
|
||||
contentPanel.setBackground(Color.WHITE);
|
||||
|
||||
if ("info".equals(type)) {
|
||||
addInfoContent(contentPanel);
|
||||
} else if ("about".equals(type)) {
|
||||
addAboutContent(contentPanel);
|
||||
}
|
||||
|
||||
add(contentPanel, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加个人信息内容到设置界面
|
||||
* 包括用户ID、用户名、邮箱、生日、地址、签名等。
|
||||
* @param panel 用于添加组件的面板
|
||||
*/
|
||||
private void addInfoContent(JPanel panel) {
|
||||
JLabel title = new JLabel("个人信息");
|
||||
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
title.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
panel.add(title);
|
||||
panel.add(Box.createVerticalStrut(20));
|
||||
|
||||
String myId = LocalData.get().getId();
|
||||
UserData myData = LocalData.get().getUserDetail(myId);
|
||||
if (myData == null) {
|
||||
// 如果数据尚未同步,使用本地基本信息创建一个临时对象
|
||||
myData = new UserData(LocalData.get().getUserName(myId), myId, null);
|
||||
}
|
||||
|
||||
addLabel(panel, "用户ID:", myId);
|
||||
addLabel(panel, "用户名:", myData.getNickname());
|
||||
|
||||
// 可编辑字段
|
||||
JTextField emailField = addEditableField(panel, "邮箱:", myData.getEmail());
|
||||
JTextField birthdayField = addEditableField(panel, "生日:", myData.getBirthday());
|
||||
JTextField addressField = addEditableField(panel, "地址:", myData.getAddress());
|
||||
JTextField signatureField = addEditableField(panel, "个性签名:", myData.getSignature());
|
||||
|
||||
panel.add(Box.createVerticalStrut(30));
|
||||
|
||||
// 保存修改按钮
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnPanel.setBackground(Color.WHITE);
|
||||
btnPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 保存修改按钮
|
||||
JButton saveBtn = new JButton("保存修改");
|
||||
final UserData currentData = myData;
|
||||
saveBtn.addActionListener(e -> {
|
||||
// 更新本地对象字段
|
||||
currentData.setEmail(emailField.getText().trim());
|
||||
currentData.setBirthday(birthdayField.getText().trim());
|
||||
currentData.setAddress(addressField.getText().trim());
|
||||
currentData.setSignature(signatureField.getText().trim());
|
||||
|
||||
// 发送更新请求
|
||||
ChatSender.addMsg(Wrapper.updateUserDetailRequest(myId, currentData));
|
||||
|
||||
// 更新本地缓存(虽然服务器会广播回来,但本地先更新体验更好)
|
||||
LocalData.get().updateUserDetails(currentData);
|
||||
|
||||
JOptionPane.showMessageDialog(this, "个人信息已保存", "提示", JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
|
||||
// 退出登录按钮
|
||||
JButton logoutBtn = new JButton("退出登录");
|
||||
logoutBtn.addActionListener(e -> {
|
||||
int confirm = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "提示", JOptionPane.YES_NO_OPTION);
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
MainPage.get().openLogInPage();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加按钮到面板
|
||||
btnPanel.add(saveBtn);
|
||||
btnPanel.add(logoutBtn);
|
||||
panel.add(btnPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加可编辑文本字段到设置界面
|
||||
* 用于用户输入个人信息的编辑。
|
||||
* @param panel 用于添加组件的面板
|
||||
* @param labelText 标签文本,描述字段的作用
|
||||
* @param value 初始文本字段值
|
||||
* @return 新创建的 JTextField 对象
|
||||
*/
|
||||
private JTextField addEditableField(JPanel panel, String labelText, String value) {
|
||||
JPanel fieldPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
fieldPanel.setBackground(Color.WHITE);
|
||||
fieldPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
JLabel label = new JLabel(labelText);
|
||||
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
label.setPreferredSize(new Dimension(80, 30));
|
||||
|
||||
JTextField textField = new JTextField(value, 20);
|
||||
textField.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
|
||||
fieldPanel.add(label);
|
||||
fieldPanel.add(textField);
|
||||
panel.add(fieldPanel);
|
||||
return textField;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关于 LocalChatApp 的内容到设置界面
|
||||
* 包括版本号、开发团队、应用描述等。
|
||||
* @param panel 用于添加组件的面板
|
||||
*/
|
||||
private void addAboutContent(JPanel panel) {
|
||||
JLabel title = new JLabel("关于 LocalChatApp");
|
||||
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
title.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
panel.add(title);
|
||||
panel.add(Box.createVerticalStrut(30));
|
||||
|
||||
JLabel version = new JLabel("版本: v1.0.0");
|
||||
version.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
panel.add(version);
|
||||
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
JLabel author = new JLabel("开发团队: 添砖加瓦小组");
|
||||
author.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
panel.add(author);
|
||||
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
JLabel desc = new JLabel("<html><body><p style='width:300px'>基于Java Swing和Socket开发的本地局域网聊天室。</p></body></html>");
|
||||
desc.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
panel.add(desc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加标签到设置界面
|
||||
* 用于显示键值对信息,例如用户ID和用户名。
|
||||
* @param panel 用于添加组件的面板
|
||||
* @param key 键,例如 "用户ID:"
|
||||
* @param value 值,例如 "10086"
|
||||
*/
|
||||
private void addLabel(JPanel panel, String key, String value) {
|
||||
// 创建一个行面板,用于添加键值对标签
|
||||
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
row.setBackground(Color.WHITE);
|
||||
row.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 键标签
|
||||
JLabel k = new JLabel(key);
|
||||
k.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
k.setPreferredSize(new Dimension(80, 30));
|
||||
|
||||
// 值标签
|
||||
JLabel v = new JLabel(value);
|
||||
v.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
|
||||
// 添加键值对标签到行面板
|
||||
row.add(k);
|
||||
row.add(v);
|
||||
panel.add(row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package client.view.main;
|
||||
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import com.formdev.flatlaf.FlatLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* 左侧侧边栏组件
|
||||
* 包含消息、好友、群聊、设置、黑暗模式切换按钮
|
||||
*/
|
||||
public class SideOptionView extends JPanel {
|
||||
private static volatile SideOptionView instance;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return 单例实例
|
||||
*/
|
||||
public static SideOptionView get() {
|
||||
if (instance == null) {
|
||||
synchronized (SideOptionView.class) {
|
||||
if (instance == null) {
|
||||
instance = new SideOptionView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照原型图,生成三个按钮组件:群聊,好友,设置组件
|
||||
* 每个组件都设置一个图标
|
||||
* 为每一个按钮配置一个事件
|
||||
* 群聊:exchangeToChatPage()/UIUpdate
|
||||
* 设置: exchangeToSettingPage()/UIUpdate。这个当前还没做,提示用户正在制作中。
|
||||
* 现在暂时没有设置相关功能
|
||||
*/
|
||||
public SideOptionView() {
|
||||
this.setLayout(new GridLayout(5, 1));
|
||||
|
||||
// this.setBackground(Color.GRAY);
|
||||
// 添加右侧边框分割线
|
||||
this.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")),
|
||||
BorderFactory.createEmptyBorder(5, 5, 5, 5)));
|
||||
|
||||
// 初始化五个核心按钮:消息、好友、群聊、设置、黑暗模式切换按钮
|
||||
initMessageButton();
|
||||
initFriendButton();
|
||||
initGroupButton();
|
||||
initSettingButton();
|
||||
initDarkModeButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化消息按钮
|
||||
* 点击后切换到消息页面
|
||||
*/
|
||||
private void initMessageButton() {
|
||||
JButton messageBtn = createIconButton("消", Color.LIGHT_GRAY);
|
||||
messageBtn.setToolTipText("消息");
|
||||
messageBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToMessageList();
|
||||
}
|
||||
});
|
||||
this.add(messageBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化好友按钮
|
||||
* 点击后切换到好友页面
|
||||
*/
|
||||
private void initFriendButton() {
|
||||
JButton friendBtn = createIconButton("友", Color.LIGHT_GRAY);
|
||||
friendBtn.setToolTipText("好友");
|
||||
friendBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToFriendList();
|
||||
}
|
||||
});
|
||||
this.add(friendBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化群聊按钮
|
||||
* 点击后切换到群聊页面
|
||||
*/
|
||||
private void initGroupButton() {
|
||||
JButton groupBtn = createIconButton("群", Color.LIGHT_GRAY);
|
||||
groupBtn.setToolTipText("群聊");
|
||||
groupBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToGroupList();
|
||||
}
|
||||
});
|
||||
this.add(groupBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化设置按钮
|
||||
* 点击后切换到设置页面
|
||||
*/
|
||||
private void initSettingButton() {
|
||||
JButton settingBtn = createIconButton("设", Color.LIGHT_GRAY);
|
||||
settingBtn.setToolTipText("设置");
|
||||
settingBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToSettingList();
|
||||
}
|
||||
});
|
||||
this.add(settingBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑暗模式切换按钮
|
||||
*/
|
||||
private void initDarkModeButton() {
|
||||
JButton darkModeBtn = createIconButton("黑", Color.DARK_GRAY);
|
||||
darkModeBtn.setToolTipText("切换模式");
|
||||
darkModeBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (FlatLaf.isLafDark()) {
|
||||
try {
|
||||
DesignToken.setDarkMode(false);
|
||||
FlatLightLaf.setup();
|
||||
FlatLaf.updateUI();
|
||||
ChatInfoView.get().updateTheme();
|
||||
// 切换图标
|
||||
darkModeBtn.setIcon(new CircleCharIcon2(Color.DARK_GRAY, Color.WHITE, "黑", 36));
|
||||
darkModeBtn.setToolTipText("切换到黑暗模式");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
DesignToken.setDarkMode(true);
|
||||
FlatDarkLaf.setup();
|
||||
FlatLaf.updateUI();
|
||||
ChatInfoView.get().updateTheme();
|
||||
// 切换图标
|
||||
darkModeBtn.setIcon(new CircleCharIcon2(Color.LIGHT_GRAY, Color.BLACK, "白", 36));
|
||||
darkModeBtn.setToolTipText("切换到明亮模式");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.add(darkModeBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带圆形图标的按钮
|
||||
*/
|
||||
private JButton createIconButton(String text, Color bgColor) {
|
||||
JButton button = new JButton();
|
||||
button.setPreferredSize(new Dimension(40, 40));
|
||||
button.setIcon(new CircleCharIcon2(bgColor, Color.WHITE, text, 36));
|
||||
button.setBorderPainted(false);
|
||||
button.setContentAreaFilled(false);
|
||||
button.setFocusPainted(false);
|
||||
return button;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class CircleCharIcon2 implements Icon {
|
||||
private Color circleColor;
|
||||
private Color textColor;
|
||||
private String character;
|
||||
private int size;
|
||||
|
||||
public CircleCharIcon2(Color circleColor, Color textColor, String character, int size) {
|
||||
this.circleColor = circleColor;
|
||||
this.textColor = textColor;
|
||||
this.character = character;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
|
||||
// 开启抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制圆形
|
||||
g2d.setColor(circleColor);
|
||||
g2d.fillOval(x, y, size - 1, size - 1);
|
||||
|
||||
// 绘制字符
|
||||
g2d.setColor(textColor);
|
||||
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(character);
|
||||
int textHeight = fm.getAscent();
|
||||
int textX = x + (size - textWidth) / 2;
|
||||
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
|
||||
g2d.drawString(character, textX, textY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package client.view.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用于设定 UI元素的各种变量
|
||||
*/
|
||||
public class DesignToken implements Serializable {
|
||||
private static final long serialVersionUID = -3791869536619757015L;
|
||||
|
||||
//============================窗口大小==============================
|
||||
// 主界面大小
|
||||
public final static int WINDOW_ORI_WIDTH = 1200;
|
||||
public final static int WINDOW_ORI_HEIGHT = 600;
|
||||
public final static int WINDOW_MIN_WIDTH = 800;
|
||||
public final static int WINDOW_MIN_HEIGHT = 600;
|
||||
// 登录界面大小
|
||||
public final static int LOGIN_WIDTH = 400;
|
||||
public final static int LOGIN_HEIGHT = 300;
|
||||
|
||||
// 主界面组件大小
|
||||
public final static int SIDE_PANEL_WIDTH = 50;
|
||||
public final static int SECONDARY_PANEL_WIDTH = 200;
|
||||
public final static int SECONDARY_PANEL_WIDTH_MIN = 150;
|
||||
public final static int CONTENT_PANEL_WIDTH = 570;
|
||||
|
||||
// 内容组件大小
|
||||
public final static int CHAT_PANEL_WIDTH = 400;
|
||||
public final static int INPUT_AREA_HEIGHT = 100;
|
||||
public final static int MSG_BUBBLE_WIDTH = 50;
|
||||
public final static int INFO_PANEL_WIDTH = 150;
|
||||
|
||||
public final static int CONTENT_PANEL_WIDTH_MIN = 300;
|
||||
public final static int GROUP_INFO_PANEL_WIDTH = 240;
|
||||
public final static int GROUP_CHAT_PANEL_WIDTH = 210;
|
||||
|
||||
//============================字体==================================
|
||||
public static int FONT_SIZE = 14;
|
||||
public static int FONT_SIZE_SMALL = 12;
|
||||
public static int FONT_SIZE_TITLE = 20;
|
||||
public static int FONT_SIZE_TITLE_MIN = 18;
|
||||
|
||||
public static String DEFAULT_FONT = "微软雅黑";
|
||||
|
||||
//=============================颜色=================================
|
||||
|
||||
// public static String COLOR_BACKGROUND = "#ddebee";
|
||||
|
||||
public static String BUBBLE_COLOR_GREEN = "#5aca58";
|
||||
public static String BUBBLE_COLOR_WHITE = "#c8cdcd";
|
||||
public static String BUBBLE_COLOR_GRAY = "#8a8a8a";
|
||||
public static String BUBBLE_COLOR_RED = "#ff4d4d";
|
||||
public static String BUBBLE_COLOR_BLUE = "#4097bc";
|
||||
public static String BUBBLE_COLOR_YELLOW = "#f7b500";
|
||||
|
||||
public static String BACKGROUND_COLOR = "#c7d9d9";
|
||||
public static String EDGE_COLOR = "#aab7b7";
|
||||
|
||||
public static String COLOR_FONT_BLUE = "#4097bc";// blue
|
||||
public static String COLOR_FONT_BLACK = "#000000";// black
|
||||
public static String COLOR_FONT_WHITE = "#ffffff";// white
|
||||
public static String COLOR_FONT_GRAY = "#8a8a8a";// gray
|
||||
public static String COLOR_FONT_GREEN = "#008000";// green
|
||||
public static String COLOR_FONT_RED = "#ff0000";// red
|
||||
public static String COLOR_FONT_YELLOW = "#ffff00";// yellow
|
||||
public static String COLOR_FONT_ORANGE = "#ff8c00";// orange
|
||||
public static String COLOR_FONT_PURPLE = "#800080";// purple
|
||||
|
||||
public static String COLOR_HEAD_BLACK = "#000000"; // black
|
||||
public static String COLOR_HEAD_GRAY = "#8a8a8a"; // gray
|
||||
public static String COLOR_HEAD_WHITE = "#ffffff"; // white
|
||||
public static String COLOR_HEAD_BLUE = "#4097bc"; // blue
|
||||
public static String COLOR_HEAD_GREEN = "#008000"; // green
|
||||
public static String COLOR_HEAD_RED = "#ff0000"; // red
|
||||
public static String COLOR_HEAD_YELLOW = "#ffff00"; // yellow
|
||||
public static String COLOR_HEAD_ORANGE = "#ff8c00"; // orange
|
||||
public static String COLOR_HEAD_PURPLE = "#800080"; // purple
|
||||
|
||||
public static void setDarkMode(boolean isDark) {
|
||||
if (isDark) {
|
||||
BACKGROUND_COLOR = "#3c3f41";
|
||||
EDGE_COLOR = "#5e6060";
|
||||
BUBBLE_COLOR_WHITE = "#505050"; // 白色气泡颜色(其他用户)
|
||||
COLOR_FONT_BLACK = "#dddddd"; // 黑色字体颜色(其他用户)
|
||||
BUBBLE_COLOR_GREEN = "#2e7d32"; // 绿色气泡颜色(自己)
|
||||
} else {
|
||||
BACKGROUND_COLOR = "#c7d9d9";
|
||||
EDGE_COLOR = "#aab7b7";
|
||||
BUBBLE_COLOR_WHITE = "#c8cdcd";
|
||||
COLOR_FONT_BLACK = "#000000";
|
||||
BUBBLE_COLOR_GREEN = "#5aca58";
|
||||
}
|
||||
}
|
||||
|
||||
//=============================文本域输入字数限制=================================
|
||||
public static int MAX_FONT_SIZE = 20;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 一个可以限制最小宽度的面板,由于splitPane中
|
||||
*/
|
||||
public class LimitSizePanel extends JPanel {
|
||||
private int minWidth;
|
||||
|
||||
public LimitSizePanel(int minWidth) {
|
||||
super();
|
||||
this.minWidth = minWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMinimumSize() {
|
||||
// 设置最小尺寸
|
||||
Dimension dim = super.getMinimumSize();
|
||||
dim.width = Math.max(dim.width, minWidth);
|
||||
return dim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
// 设置首选尺寸
|
||||
Dimension dim = super.getPreferredSize();
|
||||
dim.width = Math.max(dim.width, minWidth * 2);
|
||||
return dim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 圆角矩形字符图标
|
||||
*/
|
||||
public class RoundedRectCharIcon implements Icon {
|
||||
private Color bgColor; // 背景颜色
|
||||
private Color textColor; // 字体颜色
|
||||
private String character; // 显示的字符
|
||||
private int size; // 图标大小
|
||||
private int arc; // 圆角大小
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param bgColor 背景颜色
|
||||
* @param textColor 字体颜色
|
||||
* @param character 显示的字符
|
||||
* @param size 图标大小
|
||||
*/
|
||||
public RoundedRectCharIcon(Color bgColor, Color textColor, String character, int size) {
|
||||
this.bgColor = bgColor;
|
||||
this.textColor = textColor;
|
||||
this.character = character;
|
||||
this.size = size;
|
||||
this.arc = size / 3; // 圆角大小
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制图标
|
||||
*
|
||||
* @param c 组件
|
||||
* @param g 图形上下文
|
||||
* @param x 图标左上角的 x 坐标
|
||||
* @param y 图标左上角的 y 坐标
|
||||
*/
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
|
||||
// 开启抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制圆角矩形
|
||||
g2d.setColor(bgColor);
|
||||
g2d.fillRoundRect(x, y, size - 1, size - 1, arc, arc);
|
||||
|
||||
// 绘制字符
|
||||
g2d.setColor(textColor);
|
||||
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(character);
|
||||
int textHeight = fm.getAscent();
|
||||
int textX = x + (size - textWidth) / 2;
|
||||
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
|
||||
g2d.drawString(character, textX, textY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标宽度
|
||||
*
|
||||
* @return 图标宽度
|
||||
*/
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标高度
|
||||
*
|
||||
* @return 图标高度
|
||||
*/
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package global;
|
||||
|
||||
/**
|
||||
* 全局配置类
|
||||
* 全局变量均在此处,不要随意更改!!!局部变量命名不要有冲突
|
||||
*/
|
||||
public class global {
|
||||
// ====================== 1. 网络与基础配置 ======================
|
||||
public static final int SERVER_PORT = 1145; // 服务端监听端口,两端统一
|
||||
public static final String LOCAL_HOST = "127.0.0.1"; // 本地IP,客户端连接服务端用
|
||||
public static final String SERVER_ACCOUNT = "admin"; // 服务端账户名
|
||||
public static final int AUTO_SAVE_PERIOD = 1200; // 自动保存周期(单位:秒)
|
||||
public static final String DATA_SPLIT = "\uE000"; // 数据分隔符
|
||||
|
||||
// ====================== 2. 客户端请求操作码 (OPT) ======================
|
||||
// 注:常量值需保持唯一,避免冲突
|
||||
|
||||
// ------ 账户注册与登录 (1-4) ------
|
||||
public static final int OPT_REGISTER = 1; // 注册
|
||||
public static final int OPT_REGISTER_SUCCESS = 11; // 注册成功
|
||||
public static final int OPT_REGISTER_FAILED_ACC = 12; // 注册失败:账号存在
|
||||
public static final int OPT_REGISTER_FAILED_FORMAT = 13;// 注册失败:格式错误
|
||||
|
||||
public static final int OPT_LOGIN = 2; // 登录
|
||||
public static final int OPT_LOGIN_SUCCESS = 25; // 登录成功
|
||||
public static final int OPT_ERROR_NOT_LOGIN = 21; // 错误:还未登录
|
||||
public static final int OPT_LOGIN_FAILED_PWD = 22; // 登录失败:密码错误
|
||||
public static final int OPT_LOGIN_FAILED_ACC = 23; // 登录失败:账号错误
|
||||
public static final int OPT_LOGIN_FAILED_REPEATED = 24; // 登录失败:重复登录
|
||||
|
||||
public static final int OPT_LOGOUT = 3; // 登出
|
||||
public static final int OPT_DELETE_ACCOUNT = 4; // 注销/删除账号
|
||||
|
||||
// ------ 用户信息维护 (41-49) ------
|
||||
public static final int OPT_UPDATE_NICKNAME = 41; // 更新昵称
|
||||
public static final int OPT_UPDATE_PASSWORD = 42; // 更新密码
|
||||
public static final int OPT_USER_UPDATE_NAME_FAILED = 411; // 更新昵称失败
|
||||
public static final int OPT_USER_UPDATE_PASSWORD_FAILED = 412; // 更新密码失败
|
||||
public static final int OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT = 413; // 更新昵称失败:格式错误
|
||||
|
||||
// ------ 群组功能 (5-7) ------
|
||||
public static final int OPT_GROUP_CREATE = 5; // 创建群聊
|
||||
public static final int OPT_GROUP_CREATE_SUCCESS = 51; // 创建群聊成功
|
||||
|
||||
public static final int OPT_GROUP_INVITE = 6; // 邀请加入群聊
|
||||
public static final int OPT_GROUP_INVITE_AGREE = 61; // 同意加入群聊
|
||||
public static final int OPT_GROUP_INVITE_REFUSE = 62; // 拒绝加入群聊
|
||||
public static final int OPT_GROUP_INVITE_OFFLINE = 63; // 邀请加入群聊失败:用户离线
|
||||
|
||||
public static final int OPT_GROUP_JOIN = 64; // 申请加入群聊
|
||||
public static final int OPT_GROUP_JOIN_SUCCESS = 65; // 申请加入群聊成功
|
||||
public static final int OPT_GROUP_JOIN_FAILED = 66; // 申请加入群聊失败
|
||||
|
||||
public static final int OPT_GROUP_QUIT = 7; // 退出群聊
|
||||
public static final int OPT_GROUP_DISBAND = 71; // 解散群聊
|
||||
public static final int OPT_GROUP_UPDATE_NAME = 72; // 更新群聊名字
|
||||
public static final int OPT_GROUP_UPDATE_OWNER = 73; // 更新群聊拥有者
|
||||
|
||||
// ------ 好友功能 (67-69) ------
|
||||
public static final int OPT_FRIEND_ADD = 67; // 申请添加好友
|
||||
public static final int OPT_FRIEND_ADD_SUCCESS = 68; // 申请添加好友成功
|
||||
public static final int OPT_FRIEND_ADD_FAILED = 69; // 申请添加好友失败
|
||||
public static final int OPT_FRIEND_ADD_AGREE = 681; // 同意添加好友
|
||||
public static final int OPT_FRIEND_ADD_REFUSE = 682; // 拒绝添加好友
|
||||
|
||||
// ------ 聊天消息 (8) ------
|
||||
public static final int OPT_CHAT = 8; // 群聊消息
|
||||
public static final int OPT_PRIVATE_CHAT = 81; // 私聊消息
|
||||
|
||||
// ------ 数据初始化与同步 (9) ------
|
||||
public static final int OPT_INIT_CHAT = 9; // 初始化:群聊历史消息
|
||||
public static final int OPT_INIT_USER = 91; // 初始化:在线用户列表
|
||||
public static final int OPT_INIT_GROUP = 92; // 初始化:群组列表
|
||||
public static final int SERVER_MESSAGE = 93; // 服务器系统消息
|
||||
|
||||
// 扩展用户信息 (v2)
|
||||
public static final int OPT_INIT_USER_DETAIL = 94; // 初始化:用户详细信息
|
||||
public static final int OPT_UPDATE_USER_DETAIL = 95; // 更新:用户详细信息
|
||||
|
||||
// ------ 系统控制 ------
|
||||
public static final int OPT_EXIT = 999; // 服务器关闭通知
|
||||
public static final int OPT_QUEST_WRONG = 404; // 请求错误
|
||||
|
||||
// ====================== 3. 响应提示信息 (MSG) ======================
|
||||
// 配合结果码,客户端直接展示给用户
|
||||
public static final String MSG_SUCCESS = "操作成功";
|
||||
public static final String MSG_ACCOUNT_EXIST = "注册失败:该账户已存在(账户唯一)";
|
||||
public static final String MSG_ACCOUNT_NOT_EXIST = "登录失败:该账户不存在";
|
||||
public static final String MSG_PWD_ERROR = "登录失败:密码与账户不匹配";
|
||||
public static final String MSG_UNKNOWN_OPT = "请求失败:未知的操作类型";
|
||||
public static final String MSG_DATA_ERROR = "请求失败:数据格式错误";
|
||||
|
||||
// ====================== 4. 聊天应用配置 ======================
|
||||
public static final int MAX_MSG_SEND_GAP = 10; // 消息最大发送事件间隔/秒
|
||||
public static final int DISCONNECT_TIMEOUT = 30; // 断开连接超时时间/秒
|
||||
|
||||
public static final String DEFAULT_GROUP_ID = "group_default"; // 默认群ID
|
||||
public static final String CHAT_MSG_PREFIX = "【系统消息】"; // 系统消息前缀
|
||||
|
||||
// ====================== 5. 运行时状态标识 ======================
|
||||
public final static boolean IS_ONLINE = true;
|
||||
public final static boolean IS_OFFLINE = false;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package server;
|
||||
|
||||
import server.data.ServerData;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Server {
|
||||
|
||||
public static void main(String[] args) {
|
||||
//启动服务器
|
||||
ServerMainThread serverThread = new ServerMainThread();
|
||||
serverThread.start();
|
||||
|
||||
//启动定时任务:保存服务器数据
|
||||
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true); // 设置为守护线程
|
||||
return t;
|
||||
});
|
||||
|
||||
// 每隔半小时执行一次
|
||||
scheduler.scheduleAtFixedRate(() -> {
|
||||
System.out.println("保存数据中");
|
||||
FileUtil.saveServerData();
|
||||
}, 1200, 1200, TimeUnit.SECONDS);
|
||||
|
||||
Scanner sc = new Scanner(System.in);
|
||||
while (true) {
|
||||
// System.out.print("SERVER_CMD>>");
|
||||
String cmd = sc.nextLine();
|
||||
switch (cmd) {
|
||||
case "shutdown":
|
||||
serverThread.shutdown();
|
||||
System.out.println("=======服务器已关闭=======");
|
||||
System.exit(0);
|
||||
break;
|
||||
case "groupInfo":
|
||||
System.out.println("=======群聊列表=======");
|
||||
ServerData.getInstance().getServerGroups().values().forEach(
|
||||
group -> System.out.println(group.getGroupId() + " " + group.getGroupName())
|
||||
);
|
||||
break;
|
||||
case "chatThreadStatus":
|
||||
System.out.println("=======聊天线程状态=======");
|
||||
System.out.println(ServerMainThread.getChatThreadPoolStatus());
|
||||
break;
|
||||
case "receiveThreadStatus":
|
||||
System.out.println("=======接收线程状态=======");
|
||||
System.out.println(ServerMainThread.getReceiveThreadPoolStatus());
|
||||
break;
|
||||
case "blockingQueueStatus":
|
||||
System.out.println("=======阻塞队列状态=======");
|
||||
System.out.println(ServerMainThread.getBlockingQueueStatus());
|
||||
break;
|
||||
default:
|
||||
System.out.println("无效指令");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package server;
|
||||
|
||||
import server.serveice.*;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static global.global.SERVER_PORT;
|
||||
|
||||
public class ServerMainThread extends Thread {
|
||||
// 服务器运行状态
|
||||
private static volatile boolean running = true;
|
||||
// 线程池:用于处理聊天消息的线程池
|
||||
private static ExecutorService chatThreadPool;
|
||||
// 线程池:用于接收客户端消息的线程池
|
||||
private static ExecutorService receiveThreadPool;
|
||||
// 用于存储每个客户端的消息队列
|
||||
private static ConcurrentHashMap<Socket, ArrayBlockingQueue<Wrapper>> msgQueues;
|
||||
|
||||
// 核心:启动服务、监听端口、循环接收客户端连接
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("加载本地数据成功");
|
||||
// 初始化推送信息线程池
|
||||
chatThreadPool = new ThreadPoolExecutor(
|
||||
10, // 核心线程数
|
||||
50, // 最大线程数
|
||||
60L, TimeUnit.SECONDS, // 空闲线程存活时间
|
||||
new SynchronousQueue<>(), // 直接提交队列
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
receiveThreadPool = new ThreadPoolExecutor(
|
||||
10, // 核心线程数
|
||||
50, // 最大线程数
|
||||
60L, TimeUnit.SECONDS, // 空闲线程存活时间
|
||||
new SynchronousQueue<>(), // 直接提交队列
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
// 初始化消息队列
|
||||
msgQueues = new ConcurrentHashMap<>();
|
||||
|
||||
System.out.println("初始化信息线程池成功");
|
||||
System.out.println("服务器启动成功");
|
||||
try {
|
||||
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
|
||||
|
||||
while (running) {
|
||||
|
||||
// 扫描端口,接收链接请求,如果有链接请求,则尝试链接
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
System.out.println("有新的用户端连接: " + clientSocket.getPort());
|
||||
|
||||
// 创建线程,处理客户端请求
|
||||
ArrayBlockingQueue<Wrapper> threadQueue = new ArrayBlockingQueue<>(40);
|
||||
msgQueues.put(clientSocket, threadQueue);
|
||||
|
||||
ClientChatThread clientChatThread = new ClientChatThread(clientSocket, threadQueue);
|
||||
chatThreadPool.submit(clientChatThread);
|
||||
Thread.sleep(200);
|
||||
ClientReceiveThread clientReceiveThread = new ClientReceiveThread(clientSocket, threadQueue);
|
||||
receiveThreadPool.submit(clientReceiveThread);
|
||||
System.out.println("创建线程成功");
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭服务器
|
||||
public void shutdown() {
|
||||
FileUtil.saveServerData();
|
||||
// 向所有用户发送服务器关闭信息。
|
||||
if (msgQueues != null) {
|
||||
Wrapper exitMsg = new Wrapper(global.global.OPT_EXIT);
|
||||
for (ArrayBlockingQueue<Wrapper> queue : msgQueues.values()) {
|
||||
try {
|
||||
queue.put(exitMsg);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
running = false;
|
||||
}
|
||||
|
||||
// 检查服务器是否运行
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
// 检查chatThreadPool状态
|
||||
public static Map<String, Object> getChatThreadPoolStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
if (chatThreadPool == null) {
|
||||
status.put("error", "线程池未初始化");
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查是否为 ThreadPoolExecutor
|
||||
if (chatThreadPool instanceof ThreadPoolExecutor) {
|
||||
ThreadPoolExecutor tpe = (ThreadPoolExecutor) chatThreadPool;
|
||||
|
||||
status.put("poolSize", tpe.getPoolSize());
|
||||
status.put("activeCount", tpe.getActiveCount());
|
||||
status.put("corePoolSize", tpe.getCorePoolSize());
|
||||
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
|
||||
status.put("largestPoolSize", tpe.getLargestPoolSize());
|
||||
status.put("queueSize", tpe.getQueue().size());
|
||||
status.put("completedTaskCount", tpe.getCompletedTaskCount());
|
||||
status.put("taskCount", tpe.getTaskCount());
|
||||
status.put("isShutdown", tpe.isShutdown());
|
||||
status.put("isTerminated", tpe.isTerminated());
|
||||
}
|
||||
// 检查是否为 ForkJoinPool
|
||||
else if (chatThreadPool instanceof ForkJoinPool) {
|
||||
ForkJoinPool fjp = (ForkJoinPool) chatThreadPool;
|
||||
|
||||
status.put("poolSize", fjp.getPoolSize());
|
||||
status.put("activeCount", fjp.getActiveThreadCount());
|
||||
status.put("parallelism", fjp.getParallelism());
|
||||
status.put("runningThreadCount", fjp.getRunningThreadCount());
|
||||
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
|
||||
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
|
||||
status.put("stealCount", fjp.getStealCount());
|
||||
}
|
||||
// 其他类型的 ExecutorService
|
||||
else {
|
||||
// 使用反射尝试获取信息
|
||||
status.put("type", chatThreadPool.getClass().getName());
|
||||
status.put("info", "无法直接获取详细状态");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查receiveThreadPool的状态
|
||||
public static Map<String, Object> getReceiveThreadPoolStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
if (receiveThreadPool == null) {
|
||||
status.put("error", "线程池未初始化");
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查是否为 ThreadPoolExecutor
|
||||
if (receiveThreadPool instanceof ThreadPoolExecutor) {
|
||||
ThreadPoolExecutor tpe = (ThreadPoolExecutor) receiveThreadPool;
|
||||
|
||||
status.put("poolSize", tpe.getPoolSize());
|
||||
status.put("activeCount", tpe.getActiveCount());
|
||||
status.put("corePoolSize", tpe.getCorePoolSize());
|
||||
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
|
||||
status.put("largestPoolSize", tpe.getLargestPoolSize());
|
||||
status.put("queueSize", tpe.getQueue().size());
|
||||
status.put("completedTaskCount", tpe.getCompletedTaskCount());
|
||||
status.put("taskCount", tpe.getTaskCount());
|
||||
status.put("isShutdown", tpe.isShutdown());
|
||||
status.put("isTerminated", tpe.isTerminated());
|
||||
}
|
||||
// 检查是否为 ForkJoinPool
|
||||
else if (receiveThreadPool instanceof ForkJoinPool) {
|
||||
ForkJoinPool fjp = (ForkJoinPool) receiveThreadPool;
|
||||
|
||||
status.put("poolSize", fjp.getPoolSize());
|
||||
status.put("activeCount", fjp.getActiveThreadCount());
|
||||
status.put("parallelism", fjp.getParallelism());
|
||||
status.put("runningThreadCount", fjp.getRunningThreadCount());
|
||||
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
|
||||
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
|
||||
status.put("stealCount", fjp.getStealCount());
|
||||
}
|
||||
// 其他类型的 ExecutorService
|
||||
else {
|
||||
// 使用反射尝试获取信息
|
||||
status.put("type", receiveThreadPool.getClass().getName());
|
||||
status.put("info", "无法直接获取详细状态");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查阻塞队列状态
|
||||
public static List<String> getBlockingQueueStatus() {
|
||||
List<String> queueStatus = new ArrayList<>();
|
||||
|
||||
if (msgQueues == null) {
|
||||
queueStatus.add("阻塞队列未初始化");
|
||||
return queueStatus;
|
||||
}
|
||||
|
||||
msgQueues.forEach(
|
||||
(key, value) -> queueStatus.add(key + ": " + value.size()));
|
||||
|
||||
return queueStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于删除不需要的阻塞队列
|
||||
* 这里只能ClientChatThread调用
|
||||
*
|
||||
* @param socket 对应的客户端的socket
|
||||
*/
|
||||
public static void dropMsgQueue(Socket socket) {
|
||||
if (msgQueues == null) {
|
||||
System.out.println("意外:主线程为初始化的情况下调用了dropMsgQueue");
|
||||
return;
|
||||
}
|
||||
if (msgQueues.containsKey(socket)) {
|
||||
msgQueues.remove(socket);
|
||||
} else {
|
||||
System.out.println("意外:尝试删除不存在的阻塞队列");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package server.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 群聊信息类,用于保存聊天室的具体信息。
|
||||
*/
|
||||
public class GroupData implements Serializable {
|
||||
private static final long serialVersionUID = 4303981922076715842L;
|
||||
|
||||
public class GroupMember implements Comparable<GroupMember>, Serializable {
|
||||
private static final long serialVersionUID = 585007162886079570L;
|
||||
|
||||
public String id;
|
||||
public boolean isOut;
|
||||
|
||||
public GroupMember(String id) {
|
||||
this.id = id;
|
||||
isOut = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GroupMember o) {
|
||||
return this.id.compareTo(o.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 群聊id
|
||||
private String groupId;
|
||||
private String groupName;
|
||||
private GroupMember groupOwner;
|
||||
private TreeSet<GroupMember> members;
|
||||
|
||||
public GroupData(String groupId, String groupName, String groupOwner) {
|
||||
this.groupName = groupName;
|
||||
this.groupId = groupId;
|
||||
this.groupOwner = new GroupMember(groupOwner);
|
||||
members = new TreeSet<>();
|
||||
}
|
||||
|
||||
public GroupData(String groupId) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = "TEMP_TEST";
|
||||
}
|
||||
|
||||
// 添加组员
|
||||
public void addMember(String id) {
|
||||
members.add(new GroupMember(id));
|
||||
}
|
||||
|
||||
// 移除组员
|
||||
public boolean removeMember(String id) {
|
||||
GroupMember temp = new GroupMember(id);
|
||||
if (members.contains(temp)) {
|
||||
members.remove(new GroupMember(id));
|
||||
return true;
|
||||
} else {
|
||||
// System.out.println("组员移除失败. groupId: " + groupId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getMemberCount() {
|
||||
return members.size();
|
||||
}
|
||||
|
||||
// 获取群聊id
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
// 获取群聊名称
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
// 获取群主
|
||||
public GroupMember getGroupOwner() {
|
||||
return groupOwner;
|
||||
}
|
||||
|
||||
// 获取群成员
|
||||
public TreeSet<GroupMember> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
// 设置群主
|
||||
public void setGroupOwner(String id) {
|
||||
this.groupOwner = new GroupMember(id);
|
||||
}
|
||||
|
||||
public void setGroupName(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GroupData{" +
|
||||
"groupId='" + groupId + '\'' +
|
||||
", groupName='" + groupName + '\'' +
|
||||
", groupOwner=" + groupOwner.id +
|
||||
", members count=" + members.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package server.data;
|
||||
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
// 服务器数据,单个服务器仅对应一个服务器数据集合。
|
||||
// 允许存储,用于数据持久化
|
||||
// 辅助进行数据核验
|
||||
public class ServerData implements Serializable {
|
||||
private static final long serialVersionUID = 5016807647175865383L;
|
||||
|
||||
private static volatile ServerData instance = null;
|
||||
|
||||
// 获取唯一的serverData对象(线程安全的懒加载)
|
||||
public static ServerData getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (ServerData.class) {
|
||||
if (instance == null) {
|
||||
instance = new ServerData();
|
||||
instance.loadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 重置实例(用于测试或重新加载)
|
||||
public static void resetInstance() {
|
||||
synchronized (ServerData.class) {
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, UserData> serverUsers;
|
||||
private Map<String, GroupData> serverGroups;
|
||||
private transient boolean dataLoaded = false;
|
||||
|
||||
// 处理服务器信息的主类
|
||||
public ServerData() {
|
||||
// 初始化空数据
|
||||
serverUsers = new ConcurrentHashMap<>();
|
||||
serverGroups = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
// 显式加载数据的方法
|
||||
public void loadData() {
|
||||
if (!dataLoaded) {
|
||||
synchronized (this) {
|
||||
if (!dataLoaded) {
|
||||
ServerData loadedData = FileUtil.loadServerData();
|
||||
|
||||
if (loadedData != null) {
|
||||
// 如果加载到了数据,合并到当前实例
|
||||
if (loadedData.getServerUsers() != null) {
|
||||
this.serverUsers = loadedData.getServerUsers();
|
||||
}
|
||||
if (loadedData.getServerGroups() != null) {
|
||||
this.serverGroups = loadedData.getServerGroups();
|
||||
}
|
||||
|
||||
System.out.println("服务器数据加载成功,用户数: " +
|
||||
(serverUsers != null ? serverUsers.size() : 0) +
|
||||
", 群组数: " + (serverGroups != null ? serverGroups.size() : 0));
|
||||
} else {
|
||||
System.out.println("未找到数据文件或加载失败,使用初始化空数据");
|
||||
}
|
||||
|
||||
dataLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证数据是否已正确初始化
|
||||
private void validateData() {
|
||||
if (serverUsers == null) {
|
||||
serverUsers = new ConcurrentHashMap<>();
|
||||
}
|
||||
if (serverGroups == null) {
|
||||
serverGroups = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存服务器数据到本地
|
||||
public void saveServerData() {
|
||||
validateData(); // 确保数据有效
|
||||
FileUtil.saveServerData();
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
public void addUser(UserData userData) {
|
||||
serverUsers.put(userData.getUserId(), userData);
|
||||
}
|
||||
|
||||
// 移除用户
|
||||
public void removeUser(String userId) {
|
||||
serverUsers.remove(userId);
|
||||
serverGroups.values().forEach(groupData -> {
|
||||
groupData.removeMember(userId);
|
||||
if (groupData.getMemberCount() == 0) {
|
||||
removeGroup(groupData.getGroupId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改用户名字
|
||||
public void updateUserName(String userId, String newName) {
|
||||
serverUsers.get(userId).setNikename(newName);
|
||||
}
|
||||
|
||||
public void updateUserPwd(String userId, String newPwd) {
|
||||
serverUsers.get(userId).setPassword(newPwd);
|
||||
}
|
||||
|
||||
// 添加群聊
|
||||
public void addGroup(GroupData groupData) {
|
||||
serverGroups.put(groupData.getGroupId(), groupData);
|
||||
// 更新关联用户信息
|
||||
groupData.getMembers().forEach(member -> {
|
||||
addGroupToUser(member.id, groupData.getGroupId());
|
||||
});
|
||||
}
|
||||
|
||||
public void addGroupToUser(String userId, String groupId) {
|
||||
serverUsers.get(userId).addGroupId(groupId);
|
||||
}
|
||||
|
||||
// 移除群聊
|
||||
public void removeGroup(String groupId) {
|
||||
GroupData groupData = serverGroups.remove(groupId);
|
||||
groupData.getMembers().forEach(member -> {
|
||||
serverUsers.get(member.id).removeGroup(groupData.getGroupId());
|
||||
});
|
||||
}
|
||||
|
||||
// 更新群聊信息
|
||||
public void updateGroupName(String groupId, String newName) {
|
||||
serverGroups.get(groupId).setGroupName(newName);
|
||||
}
|
||||
|
||||
public void updateGroupOwner(String groupId, String newOwnerId) {
|
||||
serverGroups.get(groupId).setGroupOwner(newOwnerId);
|
||||
}
|
||||
|
||||
// 添加群聊成员
|
||||
public void addUserToGroup(String groupId, String userId) {
|
||||
serverGroups.get(groupId).addMember(userId);
|
||||
}
|
||||
|
||||
// 移除群聊成员
|
||||
public void removeUserFromGroup(String groupId, String userId) {
|
||||
serverGroups.values().forEach(groupData -> {
|
||||
if (groupData.getGroupId().equals(groupId)) {
|
||||
groupData.removeMember(userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 移除成员的群聊
|
||||
public void removeGroupFromUser(String userId, String groupId) {
|
||||
serverUsers.get(userId).removeGroup(groupId);
|
||||
}
|
||||
|
||||
// 获取用户名字,不存在就回复id本身
|
||||
public String getUserName(String userId) {
|
||||
if (serverUsers.containsKey(userId))
|
||||
return serverUsers.get(userId).getNickname();
|
||||
else
|
||||
return userId;
|
||||
}
|
||||
|
||||
// 获取用户数据
|
||||
public UserData getUserData(String userId) {
|
||||
return serverUsers.get(userId);
|
||||
}
|
||||
|
||||
// 获取群聊名字
|
||||
public String getGroupName(String groupId) {
|
||||
return serverGroups.get(groupId).getGroupName();
|
||||
}
|
||||
|
||||
// 判断群聊是否存在
|
||||
public boolean containsGroup(String groupId) {
|
||||
return serverGroups.containsKey(groupId);
|
||||
}
|
||||
|
||||
// 获取用户群聊id组
|
||||
public TreeSet<String> getUserGroups(String userId) {
|
||||
if (userId == null) {
|
||||
return new TreeSet<>();
|
||||
} else {
|
||||
return serverUsers.get(userId).getGroupIds();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取群聊的成员组
|
||||
public TreeSet<GroupData.GroupMember> getGroupUsers(String groupId) {
|
||||
return serverGroups.get(groupId).getMembers();
|
||||
}
|
||||
|
||||
// 获取群聊的成员id组
|
||||
public List<String> getGroupMembersId(String groupId) {
|
||||
List<String> members = new ArrayList<>();
|
||||
serverGroups.get(groupId).getMembers().forEach(groupMember -> {
|
||||
members.add(groupMember.id);
|
||||
});
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断账号是否存在
|
||||
*
|
||||
* @param userId 账号
|
||||
* @return true:存在 false:不存在
|
||||
*/
|
||||
public boolean IsAccountExist(String userId) {
|
||||
return serverUsers.containsKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验账号与密码是否匹配
|
||||
*
|
||||
* @param userId 待校验的用户账号
|
||||
* @param password 待校验的用户密码
|
||||
* @return 密码匹配返回true,不匹配返回false
|
||||
*/
|
||||
public boolean AccountAndPasswordIsMatch(String userId, String password) {
|
||||
if (serverUsers.get(userId).getPassword().equals(password)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器用户数据
|
||||
*
|
||||
* @param serverUsers 服务器用户数据映射表
|
||||
*/
|
||||
public void setServerUsers(Map<String, UserData> serverUsers) {
|
||||
this.serverUsers = serverUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器群聊数据
|
||||
*
|
||||
* @param serverGroups 服务器群聊数据映射表
|
||||
*/
|
||||
public void setServerGroups(Map<String, GroupData> serverGroups) {
|
||||
this.serverGroups = serverGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器用户数据映射表
|
||||
*
|
||||
* @return 服务器用户数据映射表
|
||||
*/
|
||||
public Map<String, UserData> getServerUsers() {
|
||||
return serverUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器用户id到用户名的映射表
|
||||
*
|
||||
* @return 服务器用户id到用户名的映射表
|
||||
*/
|
||||
public Map<String, String> getIdNameMap() {
|
||||
Map<String, String> idNameMap = new HashMap<>();
|
||||
serverUsers.values().forEach(user -> {
|
||||
idNameMap.put(user.getUserId(), user.getNickname());
|
||||
});
|
||||
return idNameMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器群聊数据映射表
|
||||
*
|
||||
* @return 服务器群聊数据映射表
|
||||
*/
|
||||
public Map<String, GroupData> getServerGroups() {
|
||||
return serverGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器群聊数据映射表
|
||||
*
|
||||
* @return 服务器群聊数据映射表
|
||||
*/
|
||||
public GroupData getGroupById(String groupId) {
|
||||
return serverGroups.get(groupId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package server.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 用户信息类,用于保存用户的具体信息。
|
||||
*/
|
||||
public class UserData implements Serializable, Comparable<UserData> {
|
||||
// 序列化版本号,用于版本控制
|
||||
private static final long serialVersionUID = 2809761558436195616L;
|
||||
|
||||
// 用户昵称
|
||||
private String nikename;
|
||||
// 用户ID
|
||||
private String id;
|
||||
// 用户密码
|
||||
private String password;
|
||||
// 所属群聊ID集合
|
||||
private TreeSet<String> groupIds;
|
||||
// 好友ID集合
|
||||
private TreeSet<String> friendIds;
|
||||
// 用户邮箱
|
||||
private String email;
|
||||
// 用户生日
|
||||
private String birthday;
|
||||
// 用户地址
|
||||
private String address;
|
||||
// 用户签名
|
||||
private String signature;
|
||||
|
||||
public UserData(String nikename, String id, String password) {
|
||||
this.nikename = nikename;
|
||||
this.id = id;
|
||||
this.password = password;
|
||||
this.groupIds = new TreeSet<>();
|
||||
this.friendIds = new TreeSet<>();
|
||||
// 初始化扩展信息为空字符串,避免 null
|
||||
this.email = "";
|
||||
this.birthday = "";
|
||||
this.address = "";
|
||||
this.signature = "";
|
||||
}
|
||||
|
||||
// 获取安全的副本(不包含密码),用于网络传输
|
||||
public UserData getSafeCopy() {
|
||||
UserData copy = new UserData(this.nikename, this.id, null);
|
||||
copy.setGroupIds(new TreeSet<>(this.groupIds));
|
||||
copy.setFriendIds(new TreeSet<>(this.friendIds));
|
||||
copy.setEmail(this.email);
|
||||
copy.setBirthday(this.birthday);
|
||||
copy.setAddress(this.address);
|
||||
copy.setSignature(this.signature);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nikename;
|
||||
}
|
||||
|
||||
public void setNikename(String nikename) {
|
||||
this.nikename = nikename;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroupIds() {
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public void setGroupIds(TreeSet<String> groupIds) {
|
||||
this.groupIds = groupIds;
|
||||
}
|
||||
|
||||
public TreeSet<String> getFriendIds() {
|
||||
return friendIds;
|
||||
}
|
||||
|
||||
public void setFriendIds(TreeSet<String> friendIds) {
|
||||
this.friendIds = friendIds;
|
||||
}
|
||||
|
||||
public void addFriend(String friendId) {
|
||||
this.friendIds.add(friendId);
|
||||
}
|
||||
|
||||
public boolean addGroupId(String groupId) {
|
||||
return this.groupIds.add(groupId);
|
||||
}
|
||||
|
||||
public boolean removeGroupId(Long groupId) {
|
||||
if (groupIds.contains(groupId.toString())) {
|
||||
groupIds.remove(groupId.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(UserData o) {
|
||||
return this.id.compareTo(o.id);
|
||||
}
|
||||
|
||||
public void removeGroup(String groupId) {
|
||||
this.groupIds.remove(groupId);
|
||||
}
|
||||
|
||||
// 扩展信息的 Getter 和 Setter
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getBirthday() {
|
||||
return birthday;
|
||||
}
|
||||
|
||||
public void setBirthday(String birthday) {
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public void setSignature(String signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.ServerMainThread;
|
||||
import server.data.*;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 不可独立创建线程,这个会在内部自主创建线程。
|
||||
* 每个客户端连接对应一个线程,用于处理客户端的请求。
|
||||
* 线程启动后,会进入一个循环,等待服务端发送的消息。
|
||||
* 收到消息后,会根据消息的操作码,调用对应的处理方法。
|
||||
* 处理完成后,会将结果发送给客户端。
|
||||
* 线程会在以下情况下退出:
|
||||
* 1. 服务端关闭
|
||||
* 2. 客户端主动关闭
|
||||
* 3. 线程被中断
|
||||
*/
|
||||
public class ClientChatThread implements Runnable {
|
||||
// 群在线用户存储:全局静态、线程安全
|
||||
// key:用户id value:用户在线输出流
|
||||
private static final Map<String, ObjectOutputStream> USER_ONLINE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
// 与客户端相连的套接字
|
||||
private final Socket clientSocket;
|
||||
// 是否登录
|
||||
private boolean isLogin = false;
|
||||
// 用户的账户
|
||||
private String userId;
|
||||
// 阻塞队列,用于进行线程的信息交流
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
private ObjectOutputStream oos;
|
||||
|
||||
private volatile boolean isRunning = true;
|
||||
|
||||
/**
|
||||
* 创建一个数据发送线程,并且附带创建一个信息接收线程。
|
||||
* 由于两个线程总是同步创建和销毁的,因此不进行单独创建。
|
||||
*
|
||||
* @param clientSocket 与客户端相连的套接字
|
||||
*/
|
||||
public ClientChatThread(Socket clientSocket, BlockingQueue<Wrapper> threadQueue)
|
||||
throws IOException {
|
||||
this.clientSocket = clientSocket;
|
||||
this.isLogin = false;
|
||||
this.userId = null;
|
||||
|
||||
messageQueue = threadQueue;
|
||||
}
|
||||
|
||||
// 线程核心:聊天业务主流程
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
oos = new ObjectOutputStream(clientSocket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
while (ServerMainThread.isRunning() && this.isRunning) {
|
||||
try {
|
||||
Wrapper msg = messageQueue.take();
|
||||
handleReceiveMsg(msg);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务端接收的消息,按规则解析。在解析出结果后,调用下面的内容来处理数据体
|
||||
*
|
||||
* @param msg 服务端接收的消息
|
||||
*/
|
||||
private void handleReceiveMsg(Wrapper msg) {
|
||||
int opt = msg.getOperation();
|
||||
String senderId = msg.getSenderId();
|
||||
|
||||
switch (opt) {
|
||||
// 登录请求
|
||||
case global.OPT_REGISTER:
|
||||
handleRegisterRequest(msg);
|
||||
break;
|
||||
case global.OPT_LOGIN:
|
||||
handleLogInRequest(msg);
|
||||
break;
|
||||
case global.OPT_LOGOUT:
|
||||
handleLogOutRequest(senderId);
|
||||
break;
|
||||
case global.OPT_DELETE_ACCOUNT:
|
||||
handleDeleteUserRequest(senderId);
|
||||
break;
|
||||
case global.OPT_UPDATE_NICKNAME:
|
||||
handleUpdateUserNameRequest(msg);
|
||||
break;
|
||||
case global.OPT_UPDATE_PASSWORD:
|
||||
handleUpdateUserPwdRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_CREATE:
|
||||
handleCreateGroupRequest((String) msg.getData(), msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE:
|
||||
handleInviteRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_JOIN:
|
||||
handleGroupJoinRequest(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD:
|
||||
handleFriendAddRequest(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD_AGREE:
|
||||
handleFriendAddAgree(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD_REFUSE:
|
||||
handleFriendAddRefuse(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE_AGREE:
|
||||
// 用户加入群聊
|
||||
handleJoinGroupRequest(senderId, msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE_REFUSE:
|
||||
// 拒绝加入群聊
|
||||
sendToUser(Wrapper.serverResponse(global.OPT_GROUP_INVITE_REFUSE), (String) msg.getData());
|
||||
break;
|
||||
case global.OPT_GROUP_QUIT:
|
||||
handleQuitGroupRequest(senderId, msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_DISBAND:
|
||||
handleDeleteGroupRequest(msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_CHAT:
|
||||
sendToGroupExceptSelf(msg, msg.getGroupId());
|
||||
FileUtil.addChatMessage(msg.getGroupId(), (String) msg.getData());
|
||||
break;
|
||||
case global.OPT_PRIVATE_CHAT:
|
||||
handlePrivateChatRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_UPDATE_NAME:
|
||||
handleUpdateGroupNameRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_UPDATE_OWNER:
|
||||
handleUpdateGroupOwnerRequest(msg);
|
||||
break;
|
||||
case global.OPT_INIT_CHAT:
|
||||
handleInitChatRequest();
|
||||
break;
|
||||
case global.OPT_INIT_GROUP:
|
||||
handleInitGroupRequest();
|
||||
break;
|
||||
case global.OPT_INIT_USER:
|
||||
// 发送用户 ID-Name 映射
|
||||
sendToSelf(Wrapper.initResponse(ServerData.getInstance().getIdNameMap()));
|
||||
break;
|
||||
case global.OPT_INIT_USER_DETAIL:
|
||||
// 发送所有用户详细信息
|
||||
handleInitUserDetailRequest();
|
||||
break;
|
||||
case global.OPT_UPDATE_USER_DETAIL:
|
||||
// 处理更新用户详细信息
|
||||
handleUpdateUserDetailRequest(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理注册请求
|
||||
* 1. 检查用户ID是否为空
|
||||
* 2. 检查用户名是否为空
|
||||
* 3. 检查密码是否为空
|
||||
* 4. 检查用户ID是否已存在
|
||||
* 5. 添加新用户到ServerData
|
||||
* 6. 发送注册成功消息给客户端
|
||||
*
|
||||
* @param msg 注册请求消息
|
||||
*/
|
||||
private void handleRegisterRequest(Wrapper msg) {
|
||||
String[] data = (String[]) msg.getData();
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String nikname = data[0];
|
||||
String password = data[1];
|
||||
|
||||
// 数据为空的情况
|
||||
if (userId == null || nikname == null || password == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
|
||||
return;
|
||||
}
|
||||
// 账户存在的情况
|
||||
if (ServerData.getInstance().IsAccountExist(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_FAILED_ACC));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().addUser(new UserData(nikname, userId, password));
|
||||
|
||||
this.isLogin = true;
|
||||
this.userId = userId;
|
||||
|
||||
USER_ONLINE_MAP.values().forEach(oos -> {
|
||||
Map<String, String> newRegister = new HashMap<>();
|
||||
newRegister.put(userId, nikname);
|
||||
try {
|
||||
oos.writeObject(Wrapper.initResponse(newRegister));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加注册用户的id oos映射,初始化id,群id映射
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_SUCCESS));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录请求
|
||||
* 1. 检查用户ID是否为空
|
||||
* 2. 检查密码是否为空
|
||||
* 3. 检查用户ID是否已存在
|
||||
* 4. 添加新用户到ServerData
|
||||
* 5. 发送登录成功消息给客户端
|
||||
*
|
||||
* @param msg 登录请求消息
|
||||
*/
|
||||
private void handleLogInRequest(Wrapper msg) {
|
||||
String userId = msg.getSenderId();
|
||||
String password = (String) msg.getData();
|
||||
|
||||
// 检测Id是否存在
|
||||
if (!ServerData.getInstance().IsAccountExist(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_ACC));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测是否重复登录
|
||||
if (USER_ONLINE_MAP.containsKey(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_REPEATED));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测账户密码是否相对应
|
||||
else if (!ServerData.getInstance().AccountAndPasswordIsMatch(userId, password)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_PWD));
|
||||
return;
|
||||
}
|
||||
// 修改数据
|
||||
this.isLogin = true;
|
||||
this.userId = userId;
|
||||
// 更新用户在线信息
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_SUCCESS));
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否已登录
|
||||
*
|
||||
* @return 如果用户已登录且在在线映射中,则返回true;否则返回false
|
||||
*/
|
||||
private boolean YesLogin() {
|
||||
return isLogin && USER_ONLINE_MAP.containsKey(userId) && this.userId != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前用户链接,关闭这个线程(接受线程在接收关闭信息时已经结束了)
|
||||
*/
|
||||
private void closeClient() {
|
||||
// 优先结束阻塞队列
|
||||
ServerMainThread.dropMsgQueue(this.clientSocket);
|
||||
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
if (oos != null) {
|
||||
try {
|
||||
oos.close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("关闭输出流异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (clientSocket != null && !clientSocket.isClosed()) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("关闭socket异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登出请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从在线用户映射中移除用户
|
||||
* 3. 设置用户状态为未登录
|
||||
* 4. 发送登出成功消息给客户端
|
||||
*
|
||||
* @param userId 要登出的用户ID
|
||||
*/
|
||||
private void handleLogOutRequest(String userId) {
|
||||
if (!YesLogin()) {
|
||||
// 出现错误,现在还未登录
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
USER_ONLINE_MAP.remove(this.userId);
|
||||
isLogin = false;
|
||||
this.userId = null;
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGOUT));
|
||||
try {
|
||||
Thread.sleep(100); // 短暂延迟
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
closeClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除用户请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从在线用户映射中移除用户
|
||||
* 3. 从服务器数据中删除用户
|
||||
* 4. 设置用户状态为未登录
|
||||
* 5. 发送删除成功消息给客户端
|
||||
*
|
||||
* @param userId 要删除的用户ID
|
||||
*/
|
||||
private void handleDeleteUserRequest(String userId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
USER_ONLINE_MAP.remove(this.userId);
|
||||
ServerData.getInstance().removeUser(userId);
|
||||
isLogin = false;
|
||||
this.userId = null;
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_DELETE_ACCOUNT));
|
||||
try {
|
||||
Thread.sleep(100); // 短暂延迟
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
closeClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化所有用户详细信息的请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从服务器数据中获取所有用户的详细信息
|
||||
* 3. 发送安全的用户详细信息副本给客户端
|
||||
*/
|
||||
private void handleInitUserDetailRequest() {
|
||||
if (!YesLogin())
|
||||
return;
|
||||
|
||||
Map<String, UserData> allUsers = ServerData.getInstance().getServerUsers();
|
||||
Map<String, UserData> safeUsers = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, UserData> entry : allUsers.entrySet()) {
|
||||
// 只发送安全的副本(无密码)
|
||||
safeUsers.put(entry.getKey(), entry.getValue().getSafeCopy());
|
||||
}
|
||||
|
||||
sendToSelf(Wrapper.initUserDetailResponse(safeUsers));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户详细信息的请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查更新数据是否包含有效用户ID
|
||||
* 3. 从服务器数据中获取原始用户数据
|
||||
* 4. 更新服务器上的用户详细信息
|
||||
* 5. 广播更新给所有在线用户
|
||||
*
|
||||
* @param msg 包含更新用户详细信息的消息
|
||||
*/
|
||||
private void handleUpdateUserDetailRequest(Wrapper msg) {
|
||||
if (!YesLogin())
|
||||
return;
|
||||
|
||||
UserData updatedData = (UserData) msg.getData();
|
||||
if (updatedData == null || !updatedData.getUserId().equals(userId)) {
|
||||
return; // 安全校验:只能更新自己的信息
|
||||
}
|
||||
|
||||
// 获取服务器上的原始数据
|
||||
UserData serverData = ServerData.getInstance().getUserData(userId);
|
||||
if (serverData != null) {
|
||||
// 更新字段
|
||||
serverData.setEmail(updatedData.getEmail());
|
||||
serverData.setBirthday(updatedData.getBirthday());
|
||||
serverData.setAddress(updatedData.getAddress());
|
||||
serverData.setSignature(updatedData.getSignature());
|
||||
// 注意:不更新密码和昵称(有专门的接口),也不更新关系链
|
||||
|
||||
// 广播更新给所有在线用户
|
||||
UserData safeCopy = serverData.getSafeCopy();
|
||||
Wrapper updateMsg = Wrapper.updateUserDetailResponse(safeCopy);
|
||||
|
||||
for (ObjectOutputStream oos : USER_ONLINE_MAP.values()) {
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(updateMsg);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理申请加入群聊请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群聊是否存在
|
||||
* 3. 检查用户是否已加入该群聊
|
||||
* 4. 加入群聊
|
||||
* 5. 通知群内其他成员有新人加入
|
||||
*
|
||||
* @param msg 包含群聊ID的消息
|
||||
*/
|
||||
private void handleGroupJoinRequest(Wrapper msg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String groupId = msg.getGroupId();
|
||||
|
||||
GroupData group = ServerData.getInstance().getGroupById(groupId);
|
||||
if (group == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_JOIN_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经是成员
|
||||
if (group.getMembers().contains(group.new GroupMember(userId))) {
|
||||
// 已经是成员,视为成功
|
||||
sendToSelf(Wrapper.initResponse(group));
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
group.addMember(userId);
|
||||
ServerData.getInstance().getUserData(userId).addGroupId(groupId);
|
||||
|
||||
// 1. 通知自己加入成功 (发送群组信息)
|
||||
sendToSelf(Wrapper.initResponse(group));
|
||||
|
||||
// 2. 通知群内其他成员有新人加入
|
||||
sendToGroupExceptSelf(Wrapper.initResponse(group), groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理添加好友请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查好友是否存在
|
||||
* 3. 检查是否已添加该好友
|
||||
* 4. 转发请求给目标用户(如果在线)
|
||||
*
|
||||
* @param msg 包含好友ID的消息
|
||||
*/
|
||||
private void handleFriendAddRequest(Wrapper msg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 目标好友ID
|
||||
|
||||
if (userId.equals(friendId)) {
|
||||
// 不能添加自己
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
UserData friendData = ServerData.getInstance().getUserData(friendId);
|
||||
if (friendData == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
UserData myData = ServerData.getInstance().getUserData(userId);
|
||||
|
||||
// 检查是否已经是好友
|
||||
if (myData.getFriendIds().contains(friendId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "你们已经是好友了"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果对方在线,转发请求
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
try {
|
||||
// 发送给目标用户:Sender=userId, Data=NickName
|
||||
USER_ONLINE_MAP.get(friendId)
|
||||
.writeObject(new Wrapper(myData.getNickname(), userId, null, global.OPT_FRIEND_ADD));
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "好友请求已发送"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "用户不在线,无法发送请求"));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFriendAddAgree(Wrapper msg) {
|
||||
String myId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 发起者的ID
|
||||
|
||||
UserData myData = ServerData.getInstance().getUserData(myId);
|
||||
UserData friendData = ServerData.getInstance().getUserData(friendId);
|
||||
|
||||
if (friendData == null)
|
||||
return;
|
||||
|
||||
// 双向添加
|
||||
myData.addFriend(friendId);
|
||||
friendData.addFriend(myId);
|
||||
|
||||
// 通知自己成功
|
||||
Map<String, String> friendInfo = new HashMap<>();
|
||||
friendInfo.put(friendId, friendData.getNickname());
|
||||
sendToSelf(new Wrapper(friendInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
|
||||
|
||||
// 通知对方成功
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
Map<String, String> myInfo = new HashMap<>();
|
||||
myInfo.put(myId, myData.getNickname());
|
||||
try {
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
new Wrapper(myInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
|
||||
// 还可以发个系统消息提示
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
Wrapper.serverResponse(global.SERVER_MESSAGE, myData.getNickname() + " 同意了你的好友请求"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理好友添加拒绝请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查好友是否存在
|
||||
* 3. 通知拒绝者好友请求已被拒绝
|
||||
*
|
||||
* @param msg 包含拒绝好友ID的消息
|
||||
*/
|
||||
private void handleFriendAddRefuse(Wrapper msg) {
|
||||
String myId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 发起者的ID
|
||||
UserData myData = ServerData.getInstance().getUserData(myId);
|
||||
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
try {
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
new Wrapper(myData.getNickname(), myId, null, global.OPT_FRIEND_ADD_REFUSE));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查接收者是否存在
|
||||
* 3. 检查接收者是否在线
|
||||
* 4. 转发消息给接收者
|
||||
*
|
||||
* @param msg 包含接收者ID和消息内容的消息
|
||||
*/
|
||||
private void handlePrivateChatRequest(Wrapper msg) {
|
||||
String receiverId = msg.getGroupId(); // 接收者ID
|
||||
|
||||
if (USER_ONLINE_MAP.containsKey(receiverId)) {
|
||||
try {
|
||||
// 转发消息给接收者
|
||||
USER_ONLINE_MAP.get(receiverId).writeObject(msg);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// 对方不在线
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "对方不在线"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户信息请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新用户昵称
|
||||
* 3. 通知所有连接的客户端更新用户信息
|
||||
*
|
||||
* @param wrapper 包含新昵称的消息
|
||||
*/
|
||||
private void handleUpdateUserNameRequest(Wrapper wrapper) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
String newName = (String) wrapper.getData();
|
||||
ServerData.getInstance().updateUserName(this.userId, newName);
|
||||
|
||||
Map<String, String> idNameMap = new HashMap<>();
|
||||
idNameMap.put(this.userId, newName);
|
||||
|
||||
// OPT_INIT_USER
|
||||
sentToConnectedGroups(Wrapper.initResponse(idNameMap), this.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户密码请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新用户密码
|
||||
* 3. 通知客户端密码更新成功
|
||||
*
|
||||
* @param wrapper 包含新密码的消息
|
||||
*/
|
||||
private void handleUpdateUserPwdRequest(Wrapper wrapper) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
String newPwd = (String) wrapper.getData();
|
||||
ServerData.getInstance().updateUserPwd(this.userId, newPwd);
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_UPDATE_PASSWORD));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群组名称是否为空
|
||||
* 3. 检查群组ID是否重复
|
||||
* 4. 创建新的群组
|
||||
* 5. 通知客户端群组创建成功
|
||||
*
|
||||
* @param groupName 群组名称
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleCreateGroupRequest(String groupName, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果信息为空
|
||||
if (groupName == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经有对应的id的群聊
|
||||
if (ServerData.getInstance().containsGroup(groupId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "群聊已存在,id重复"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数据调用serverData的addGroup
|
||||
GroupData groupData = new GroupData(groupId, groupName, userId);
|
||||
groupData.addMember(userId);
|
||||
ServerData.getInstance().addGroup(groupData);
|
||||
// 调用serverData的addUser
|
||||
ServerData.getInstance().addUserToGroup(groupId, userId);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_CREATE_SUCCESS));
|
||||
sendToSelf(Wrapper.initResponse(groupData));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邀请加入群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查邀请人是否是自己
|
||||
* 3. 检查用户是否已在群聊中
|
||||
* 4. 检查用户是否在线
|
||||
* 5. 通知被邀请人加入群组
|
||||
*
|
||||
* @param inviteMsg 包含被邀请人ID和群组ID的消息
|
||||
*/
|
||||
private void handleInviteRequest(Wrapper inviteMsg) {
|
||||
String inviteId = (String) inviteMsg.getData();
|
||||
String theGroupId = inviteMsg.getGroupId();
|
||||
String senderId = inviteMsg.getSenderId();
|
||||
|
||||
// 如果邀请人是自己
|
||||
if (senderId.equals(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "不能邀请自己加入群聊"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果邀请人已经在群聊中
|
||||
if (ServerData.getInstance().getGroupMembersId(theGroupId).contains(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "该用户已在群聊中"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户不在线
|
||||
if (!USER_ONLINE_MAP.containsKey(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(
|
||||
global.SERVER_MESSAGE,
|
||||
"用户(" + ServerData.getInstance().getUserName(inviteId) + ")不在线!"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知被邀请人加入群组
|
||||
sendToUser(Wrapper.groupInviteRequest(
|
||||
ServerData.getInstance().getGroupName(theGroupId), senderId, theGroupId),
|
||||
inviteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加入群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查用户是否已在群聊中
|
||||
* 3. 检查群组是否存在
|
||||
* 4. 加入群组
|
||||
* 5. 通知群组内所有人用户加入
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleJoinGroupRequest(String userId, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().addUserToGroup(groupId, userId);
|
||||
ServerData.getInstance().addGroupToUser(userId, groupId);
|
||||
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
// 向组内所有人推送更新。(人员加入)
|
||||
sendToGroup(wrapper, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退出群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查用户是否已在群聊中
|
||||
* 3. 从群组中移除用户
|
||||
* 4. 从用户群组列表中移除群组
|
||||
* 5. 如果群组为空,则删除群组
|
||||
* 6. 通知群组内其他用户更新
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleQuitGroupRequest(String userId, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().removeUserFromGroup(groupId, userId);
|
||||
ServerData.getInstance().removeGroupFromUser(userId, groupId);
|
||||
|
||||
if (ServerData.getInstance().getGroupById(groupId).getMemberCount() == 0) {
|
||||
// 没有人的群聊就删掉
|
||||
ServerData.getInstance().removeGroup(groupId);
|
||||
} else {
|
||||
// 不为空则需要推送更新
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
// 向组内其它所有人推送更新。
|
||||
sendToGroupExceptSelf(wrapper, groupId);
|
||||
}
|
||||
// 退出人也要进行更新
|
||||
sendToSelf(new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群组是否存在
|
||||
* 3. 检查用户是否是群组所有者
|
||||
* 4. 删除群组
|
||||
* 5. 通知所有连接的客户端群组已删除
|
||||
*
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleDeleteGroupRequest(String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
Wrapper wrapper = new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT);
|
||||
|
||||
// 向所有人推送更新。
|
||||
ServerData.getInstance().removeGroup(groupId);
|
||||
sendToGroup(wrapper, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新群组名称请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新群组名称
|
||||
* 3. 通知所有连接的客户端更新群组名称
|
||||
*
|
||||
* @param groupUpdateMsg 包含群组ID和新名称的消息
|
||||
*/
|
||||
private void handleUpdateGroupNameRequest(Wrapper groupUpdateMsg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
ServerData.getInstance().updateGroupName(groupUpdateMsg.getGroupId(),
|
||||
(String) groupUpdateMsg.getData());
|
||||
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新群组管理员请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查管理员是否是自己
|
||||
* 3. 更新群组管理员
|
||||
* 4. 通知群组内所有成员更新管理员
|
||||
*
|
||||
* @param groupUpdateMsg 包含新管理员ID和群组ID的消息
|
||||
*/
|
||||
private void handleUpdateGroupOwnerRequest(Wrapper groupUpdateMsg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
ServerData.getInstance().updateGroupOwner(groupUpdateMsg.getGroupId(), (String) groupUpdateMsg.getData());
|
||||
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化聊天消息请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 遍历用户所属的所有群组
|
||||
* 3. 加载群组聊天记录
|
||||
* 4. 向用户发送群组聊天记录初始化消息
|
||||
*/
|
||||
private void handleInitChatRequest() {
|
||||
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
|
||||
Wrapper wrapper = Wrapper.initResponse(FileUtil.loadGroupChatMsg(groupId), groupId);
|
||||
sendToSelf(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 遍历用户所属的所有群组
|
||||
* 3. 加载群组信息
|
||||
* 4. 向用户发送群组初始化消息
|
||||
*/
|
||||
private void handleInitGroupRequest() {
|
||||
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
sendToSelf(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户广播消息
|
||||
* 1. 遍历用户ID列表
|
||||
* 2. 检查用户是否在线
|
||||
* 3. 向在线用户发送消息
|
||||
*
|
||||
* @param userIds 接收消息的用户ID列表
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
*/
|
||||
public static void broadcastMsg(String[] userIds, Wrapper wrapper) {
|
||||
for (String userId : userIds) {
|
||||
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
|
||||
if (oos == null)
|
||||
continue;
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(wrapper);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.err.println("send error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向用户发送消息
|
||||
* 1. 检查用户是否在线
|
||||
* 2. 向在线用户发送消息
|
||||
*
|
||||
* @param o 要发送的消息包装对象
|
||||
*/
|
||||
private void sendToSelf(Wrapper o) {
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(o);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.out.println("send error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户发送消息
|
||||
* 1. 检查用户是否在线
|
||||
* 2. 向在线用户发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param userId 接收消息的用户ID
|
||||
*/
|
||||
private void sendToUser(Wrapper wrapper, String userId) {
|
||||
try {
|
||||
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
|
||||
if (oos != null) {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(wrapper);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.err.println("send error: " + userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定群聊发送消息
|
||||
* 1. 遍历群聊成员ID列表
|
||||
* 2. 检查成员是否在线
|
||||
* 3. 向在线成员发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param groupId 接收消息的群聊ID
|
||||
*/
|
||||
private void sendToGroup(Wrapper wrapper, String groupId) {
|
||||
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
|
||||
for (String member : members) {
|
||||
sendToUser(wrapper, member);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定群聊发送消息(排除发送者)
|
||||
* 1. 遍历群聊成员ID列表
|
||||
* 2. 检查成员是否在线
|
||||
* 3. 向在线成员发送消息(排除发送者)
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param groupId 接收消息的群聊ID
|
||||
*/
|
||||
private void sendToGroupExceptSelf(Wrapper wrapper, String groupId) {
|
||||
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
|
||||
for (String member : members) {
|
||||
if (!member.equals(userId)) {
|
||||
sendToUser(wrapper, member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向用户所属的所有群聊发送消息
|
||||
* 1. 遍历用户所属的所有群组
|
||||
* 2. 向每个群组发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param userId 接收消息的用户ID
|
||||
*/
|
||||
private void sentToConnectedGroups(Wrapper wrapper, String userId) {
|
||||
TreeSet<String> groups = ServerData.getInstance().getUserGroups(userId);
|
||||
groups.forEach(groupId -> sendToGroup(wrapper, groupId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.ServerMainThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class ClientReceiveThread implements Runnable {
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
private volatile boolean isRunning;
|
||||
|
||||
// 接收线程专属资源:构造器传入,仅用于接收消息
|
||||
private final Socket clientSocket;
|
||||
ObjectInputStream ois;
|
||||
|
||||
// 构造器:初始化套接字资源
|
||||
public ClientReceiveThread(
|
||||
Socket clientSocket,
|
||||
BlockingQueue<Wrapper> messageQueue) throws IOException {
|
||||
|
||||
this.messageQueue = messageQueue;
|
||||
this.clientSocket = clientSocket;
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
// 线程核心:循环接收服务端消息(历史/广播),打印展示
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ois = new ObjectInputStream(clientSocket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// 当主线程和自身都在跑的时候
|
||||
while (ServerMainThread.isRunning() && this.isRunning) {
|
||||
// 接收服务端消息
|
||||
// 接收到后将其添加到阻塞队列中
|
||||
try {
|
||||
Wrapper msg = (Wrapper) ois.readObject();
|
||||
// 如果收到的是关闭信息,则这个循环结束后关闭自身
|
||||
if (msg.getOperation() == global.OPT_LOGOUT) {
|
||||
isRunning = false;
|
||||
System.out.println("接受线程已结束");
|
||||
}
|
||||
// 添加信息到阻塞队列。
|
||||
messageQueue.put(msg);
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("消息队列被中断");
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
if (isConnectionClosed(e)) {
|
||||
// 链接断开则结束链接
|
||||
// isRunning =false;
|
||||
System.out.println(clientSocket.getPort() + ": 连接已断开");
|
||||
break;
|
||||
} else {
|
||||
System.out.println("发送消息时发生IO异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnectionClosed(IOException e) {
|
||||
// 根据异常类型判断连接是否断开
|
||||
return e instanceof SocketException ||
|
||||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
|
||||
e.getMessage().contains("Broken pipe") ||
|
||||
e.getMessage().contains("Connection refused") ||
|
||||
e.getMessage().contains("Software caused connection abort"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// 打包类,用于将数据打包传输
|
||||
|
||||
/**
|
||||
* Wrapper类的构造方法这里设置为私有,需要时请使用/创建对应的静态构造方法来获取对应的信息载体。
|
||||
* 详情请查阅源代码。
|
||||
*/
|
||||
public class Wrapper implements Serializable {
|
||||
private static final long serialVersionUID = 7499350690768481854L;
|
||||
|
||||
private Object data;
|
||||
|
||||
private String senderId;
|
||||
private String groupId;
|
||||
private int operation;
|
||||
|
||||
public Wrapper(Object data, String senderId, String groupId, int operation) {
|
||||
|
||||
this.data = data;
|
||||
this.senderId = senderId;
|
||||
this.groupId = groupId;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public int getOperation() {
|
||||
return operation;
|
||||
}
|
||||
|
||||
// 一下静态方法都是用于便捷创建消息体的方法。字如其名,当你需要创造相应的信息载体的时候,请使用对应的方法。
|
||||
// 如果有其它需要,请在这里手动添加新的静态方法用于创建对应数据类别。
|
||||
// 需要注意的时,有有一部分的类型的信息体是客户端和服务器端都可以发送的,但是他们发送具有不同的含义。
|
||||
|
||||
// 注册请求
|
||||
public static Wrapper registerRequest(String[] nickNameAndpwsd, String senderId) {
|
||||
return new Wrapper(nickNameAndpwsd, senderId, null, global.OPT_REGISTER);
|
||||
}
|
||||
|
||||
// 注册请求
|
||||
public static Wrapper registerRequest(String senderId, String password, String username) {
|
||||
String[] temp = new String[] { username, password };
|
||||
return new Wrapper(temp, senderId, null, global.OPT_REGISTER);
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
public static Wrapper loginRequest(String senderId, String password) {
|
||||
return new Wrapper(password, senderId, null, global.OPT_LOGIN);
|
||||
}
|
||||
|
||||
// 服务器回复,用于回复一些简单的关于操作流的信息
|
||||
public static Wrapper serverResponse(int opt) {
|
||||
return new Wrapper(null, global.SERVER_ACCOUNT, null, opt);
|
||||
}
|
||||
|
||||
// 服务器回复,带文本信息
|
||||
public static Wrapper serverResponse(int opt, String msg) {
|
||||
return new Wrapper(msg, global.SERVER_ACCOUNT, null, opt);
|
||||
}
|
||||
|
||||
// 构造一个只包含操作码的消息(用于 OPT_EXIT 等)
|
||||
public Wrapper(int operation) {
|
||||
this.data = null;
|
||||
this.senderId = global.SERVER_ACCOUNT;
|
||||
this.groupId = null;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
// 登出请求
|
||||
public static Wrapper logoutRequest(String senderId) {
|
||||
return new Wrapper(null, senderId, null, global.OPT_LOGOUT);
|
||||
}
|
||||
|
||||
// 初始化请求,请求服务器发送账户和当前的群聊数据
|
||||
public static Wrapper initRequest(String senderId, int opt) {
|
||||
return new Wrapper(null, senderId, null, opt);
|
||||
}
|
||||
|
||||
// 初始化回复,将群聊信息回复给客户端
|
||||
public static Wrapper initResponse(GroupData groupData) {
|
||||
return new Wrapper(groupData, global.SERVER_ACCOUNT, null, global.OPT_INIT_GROUP);
|
||||
}
|
||||
|
||||
// 初始化回复,将聊天记录回复给客户端
|
||||
public static Wrapper initResponse(List<String> chatRecords, String groupId) {
|
||||
return new Wrapper(chatRecords, global.SERVER_ACCOUNT, groupId, global.OPT_INIT_CHAT);
|
||||
}
|
||||
|
||||
// 将用户id/名字回复给客户端。
|
||||
public static Wrapper initResponse(Map<String, String> idNameMap) {
|
||||
return new Wrapper(idNameMap, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER);
|
||||
}
|
||||
|
||||
// 更新用户昵称请求
|
||||
public static Wrapper updateUserNameRequest(String senderId, String nickName) {
|
||||
return new Wrapper(nickName, senderId, null, global.OPT_UPDATE_NICKNAME);
|
||||
}
|
||||
|
||||
// 申请加入群聊
|
||||
public static Wrapper joinGroupRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_JOIN);
|
||||
}
|
||||
|
||||
// 申请添加好友
|
||||
public static Wrapper addFriendRequest(String senderId, String friendId) {
|
||||
// friendId 放在 data 中
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD);
|
||||
}
|
||||
|
||||
// 更新用户密码请求
|
||||
public static Wrapper updateUserPwdRequest2(String senderId, String newPwd) {
|
||||
return new Wrapper(newPwd, senderId, null, global.OPT_UPDATE_PASSWORD);
|
||||
}
|
||||
|
||||
// 创建群聊请求
|
||||
public static Wrapper createGroupRequest(String senderId, String groupName, String groupId) {
|
||||
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_CREATE);
|
||||
}
|
||||
|
||||
// 邀请加入群聊请求
|
||||
public static Wrapper groupInviteRequest(String invitedIdOrGroupName, String senderId, String groupId) {
|
||||
return new Wrapper(invitedIdOrGroupName, senderId, groupId, global.OPT_GROUP_INVITE);
|
||||
}
|
||||
|
||||
// 退出群聊请求
|
||||
public static Wrapper groupQuitRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_QUIT);
|
||||
}
|
||||
|
||||
// 退出群聊响应
|
||||
public static Wrapper groupQuitResponse(String quitMemberId, String groupId) {
|
||||
return new Wrapper(quitMemberId, global.SERVER_ACCOUNT, groupId, global.OPT_GROUP_QUIT);
|
||||
}
|
||||
|
||||
// 解散群聊请求
|
||||
public static Wrapper groupDisbandRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_DISBAND);
|
||||
}
|
||||
|
||||
// 更新群聊名字请求
|
||||
public static Wrapper groupUpdateNameRequest(String senderId, String groupId, String groupName) {
|
||||
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_UPDATE_NAME);
|
||||
}
|
||||
|
||||
// 更新群聊群主请求
|
||||
public static Wrapper groupUpdateOwnerRequest(String senderId, String groupId, String ownerId) {
|
||||
return new Wrapper(ownerId, senderId, groupId, global.OPT_GROUP_UPDATE_OWNER);
|
||||
}
|
||||
|
||||
// 聊天信息
|
||||
public static Wrapper groupChat(String text, String senderId, String groupId) {
|
||||
return new Wrapper(text, senderId, groupId, global.OPT_CHAT);
|
||||
}
|
||||
|
||||
// 私聊信息
|
||||
public static Wrapper privateChat(String text, String senderId, String receiverId) {
|
||||
// 私聊数据存储在 data/friends/chat_data 中
|
||||
// 为了复用字段,我们将 receiverId 放在 groupId 字段中作为目标ID
|
||||
return new Wrapper(text, senderId, receiverId, global.OPT_PRIVATE_CHAT);
|
||||
}
|
||||
|
||||
// 同意添加好友
|
||||
public static Wrapper friendAddAgree(String senderId, String friendId) {
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_AGREE);
|
||||
}
|
||||
|
||||
// 拒绝添加好友
|
||||
public static Wrapper friendAddRefuse(String senderId, String friendId) {
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_REFUSE);
|
||||
}
|
||||
|
||||
// 创建简单指令回复
|
||||
public static Wrapper simpleRequest(String senderId, String groupId, int opt) {
|
||||
return new Wrapper(null, senderId, groupId, opt);
|
||||
}
|
||||
|
||||
// 初始化用户详细信息回复(Map<String, UserData>)
|
||||
public static Wrapper initUserDetailResponse(Map<String, UserData> userDetails) {
|
||||
return new Wrapper(userDetails, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER_DETAIL);
|
||||
}
|
||||
|
||||
// 更新用户详细信息请求
|
||||
public static Wrapper updateUserDetailRequest(String senderId, UserData userData) {
|
||||
return new Wrapper(userData, senderId, null, global.OPT_UPDATE_USER_DETAIL);
|
||||
}
|
||||
|
||||
// 更新用户详细信息响应(服务端广播)
|
||||
public static Wrapper updateUserDetailResponse(UserData userData) {
|
||||
return new Wrapper(userData, global.SERVER_ACCOUNT, null, global.OPT_UPDATE_USER_DETAIL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package test;
|
||||
|
||||
import server.data.ServerData;
|
||||
import util.FileUtil;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class MainTest {
|
||||
|
||||
@Test
|
||||
public void testLoadEmptyFile() throws IOException {
|
||||
// 测试加载空文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
File folder = dataPath.getParent().toFile();
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs();
|
||||
}
|
||||
|
||||
// 创建空文件
|
||||
Files.write(dataPath, new byte[0]);
|
||||
|
||||
System.out.println("Created empty data file: " + dataPath.toAbsolutePath());
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
assertNotNull(data);
|
||||
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
|
||||
// assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
|
||||
|
||||
System.out.println("Test passed: Empty file handled correctly.");
|
||||
|
||||
// 删除测试文件
|
||||
Files.deleteIfExists(dataPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadMissingFile() {
|
||||
// 测试加载缺失文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
try {
|
||||
Files.deleteIfExists(dataPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
System.out.println("Ensured data file is missing.");
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
assertNotNull(data);
|
||||
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
|
||||
assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
|
||||
|
||||
System.out.println("Test passed: Missing file handled correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadValidFile() throws IOException {
|
||||
// 测试加载有效文件的情况
|
||||
System.out.println("Starting testLoadValidFile...");
|
||||
// 测试加载有效文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
try {
|
||||
Files.deleteIfExists(dataPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// 初始化 ServerData 实例,确保其为空
|
||||
ServerData.resetInstance();
|
||||
ServerData data = ServerData.getInstance();
|
||||
System.out.println("Initialized ServerData for saving.");
|
||||
|
||||
// 保存 ServerData 实例,确保文件存在
|
||||
data.saveServerData();
|
||||
System.out.println("Saved ServerData.");
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
System.out.println("Reset instance. Now calling getInstance() to trigger load...");
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
// 确保加载的 ServerData 实例不为空
|
||||
ServerData loaded = ServerData.getInstance();
|
||||
|
||||
assertNotNull(loaded);
|
||||
System.out.println("Test passed: Valid file loaded correctly.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class DataObj implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String message;
|
||||
|
||||
public DataObj(){}
|
||||
|
||||
public DataObj(String id, String name, String message) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package util;
|
||||
|
||||
import server.data.ServerData;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
/*
|
||||
*通用文件IO工具类
|
||||
*将聊天记录等信息写入本地文件
|
||||
*/
|
||||
public class FileUtil {
|
||||
// 数据文件夹
|
||||
public static final String DATA_FILE = "data";
|
||||
|
||||
// 子文件夹
|
||||
public static final String GROUPS_DIR = "groups";
|
||||
public static final String FRIENDS_DIR = "friends";
|
||||
|
||||
// 服务器数据文件名
|
||||
public static final String SERVER_DATA_FILENAME = "server_data.data";
|
||||
|
||||
// 聊天历史消息存储文件夹名
|
||||
public static final String CHAT_DATA_DIRNAME = "chat_data";
|
||||
|
||||
// 辅助方法:获取Group ServerData路径
|
||||
private static Path getGroupServerDataPath() {
|
||||
return Paths.get(DATA_FILE, GROUPS_DIR, SERVER_DATA_FILENAME);
|
||||
}
|
||||
|
||||
// 辅助方法:获取Friend ServerData路径
|
||||
private static Path getFriendServerDataPath() {
|
||||
return Paths.get(DATA_FILE, FRIENDS_DIR, SERVER_DATA_FILENAME);
|
||||
}
|
||||
|
||||
// 辅助方法:获取Chat Data路径
|
||||
private static Path getChatDataPath(String id, boolean isGroup) {
|
||||
String subDir = isGroup ? GROUPS_DIR : FRIENDS_DIR;
|
||||
return Paths.get(DATA_FILE, subDir, CHAT_DATA_DIRNAME, id + ".txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件中的serverData信息,返回ServerData对象
|
||||
* 从groups和friends文件夹分别读取并合并
|
||||
*/
|
||||
public static ServerData loadServerData() {
|
||||
ServerData finalData = new ServerData();
|
||||
boolean loadedAny = false;
|
||||
|
||||
// 1. Load Group Data
|
||||
ServerData groupData = loadDataFromFile(getGroupServerDataPath());
|
||||
if (groupData != null) {
|
||||
if (groupData.getServerGroups() != null) {
|
||||
finalData.setServerGroups(groupData.getServerGroups());
|
||||
}
|
||||
loadedAny = true;
|
||||
}
|
||||
|
||||
// 2. 加载好友数据
|
||||
ServerData friendData = loadDataFromFile(getFriendServerDataPath());
|
||||
if (friendData != null) {
|
||||
if (friendData.getServerUsers() != null) {
|
||||
finalData.setServerUsers(friendData.getServerUsers());
|
||||
}
|
||||
loadedAny = true;
|
||||
}
|
||||
|
||||
return loadedAny ? finalData : null;
|
||||
}
|
||||
|
||||
private static ServerData loadDataFromFile(Path path) {
|
||||
if (!Files.exists(path) || path.toFile().length() == 0)
|
||||
return null;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
new BufferedInputStream(new FileInputStream(path.toFile())))) {
|
||||
return (ServerData) ois.readObject();
|
||||
} catch (Exception e) {
|
||||
System.err.println("加载数据失败 (" + path + "): " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用静态方法获取serverData对象,将serverData中的信息写入文件中
|
||||
*/
|
||||
public static void saveServerData() {
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
// Save Groups
|
||||
ServerData groupData = new ServerData();
|
||||
groupData.setServerGroups(data.getServerGroups());
|
||||
// 清空 Users,避免重复存储到groups文件
|
||||
groupData.setServerUsers(null);
|
||||
saveDataToFile(getGroupServerDataPath(), groupData);
|
||||
|
||||
// Save Friends
|
||||
ServerData friendData = new ServerData();
|
||||
friendData.setServerUsers(data.getServerUsers());
|
||||
// 清空 Groups,避免重复存储到friends文件
|
||||
friendData.setServerGroups(null);
|
||||
saveDataToFile(getFriendServerDataPath(), friendData);
|
||||
|
||||
System.out.println("服务器数据保存成功");
|
||||
}
|
||||
|
||||
private static void saveDataToFile(Path path, ServerData data) {
|
||||
File file = path.toFile();
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
synchronized (ServerData.class) {
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(
|
||||
new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
oos.writeObject(data);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据文件是否存在 (任意一个存在即认为存在)
|
||||
*/
|
||||
public static boolean isDataFileExists() {
|
||||
return Files.exists(getGroupServerDataPath()) || Files.exists(getFriendServerDataPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据文件大小(字节)
|
||||
*/
|
||||
public static long getDataFileSize() {
|
||||
long size = 0;
|
||||
File gFile = getGroupServerDataPath().toFile();
|
||||
if (gFile.exists())
|
||||
size += gFile.length();
|
||||
File fFile = getFriendServerDataPath().toFile();
|
||||
if (fFile.exists())
|
||||
size += fFile.length();
|
||||
return size;
|
||||
}
|
||||
|
||||
// ===================================聊天信息=======================
|
||||
|
||||
/**
|
||||
* 将聊天信息追加写入文件
|
||||
*
|
||||
* @param groupId 群聊id 或 用户id
|
||||
* @param senderId 发送者id
|
||||
* @param senderName 发送者名字
|
||||
* @param content 发送内容
|
||||
*/
|
||||
public static void addChatMessage(String groupId, String senderId, String senderName, String content) {
|
||||
String msg = MsgUtil.combineMsg(senderId, senderName, content);
|
||||
writeChatMsg(groupId, msg);
|
||||
}
|
||||
|
||||
public static void addChatMessage(String groupId, String contentFromWrapper) {
|
||||
writeChatMsg(groupId, contentFromWrapper);
|
||||
}
|
||||
|
||||
private static void writeChatMsg(String id, String content) {
|
||||
// 判断是群聊还是私聊
|
||||
// 通过 ServerGroups 来判断,如果在群组Map中,则是群聊,否则认为是私聊
|
||||
boolean isGroup = false;
|
||||
if (ServerData.getInstance().getServerGroups() != null) {
|
||||
isGroup = ServerData.getInstance().getServerGroups().containsKey(id);
|
||||
}
|
||||
|
||||
Path path = getChatDataPath(id, isGroup);
|
||||
File file = path.toFile();
|
||||
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (FileWriter fw = new FileWriter(file, true);
|
||||
BufferedWriter bw = new BufferedWriter(fw)) {
|
||||
// 写入文本并换行
|
||||
bw.write(content);
|
||||
bw.newLine(); // 跨平台换行
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个群的所有聊天记录
|
||||
*
|
||||
* @return 历史消息列表,无内容/文件不存在返回空List,绝不返回null
|
||||
*/
|
||||
public static List<String> loadGroupChatMsg(String groupId) {
|
||||
// 判断是群聊还是私聊
|
||||
boolean isGroup = false;
|
||||
if (ServerData.getInstance().getServerGroups() != null) {
|
||||
isGroup = ServerData.getInstance().getServerGroups().containsKey(groupId);
|
||||
}
|
||||
|
||||
Path path = getChatDataPath(groupId, isGroup);
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(path);
|
||||
} catch (NoSuchFileException e) {
|
||||
lines = new ArrayList<>();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (lines == null) {
|
||||
lines = new ArrayList<>();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static global.global.DATA_SPLIT;
|
||||
|
||||
/**
|
||||
* 工具类,用于将聊过天记录拆分/整合
|
||||
*/
|
||||
public class MsgUtil implements Serializable {
|
||||
public static final int SENDERID_POS = 0;
|
||||
public static final int SENDER_NAME_POS = 1;
|
||||
public static final int TIME_POS = 2;
|
||||
public static final int CONTENT_POS = 3;
|
||||
|
||||
private static final Pattern SPLIT_PATTERN = Pattern.compile(Pattern.quote(DATA_SPLIT));
|
||||
|
||||
public static String combineMsg(String senderId, String senderName, String content) {
|
||||
return senderId + DATA_SPLIT + senderName + DATA_SPLIT + content;
|
||||
}
|
||||
|
||||
public static String[] splitMsg(String msg) {
|
||||
if (msg == null) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// 使用 -1 作为 limit 参数,保留空字符串
|
||||
return SPLIT_PATTERN.split(msg, -1);
|
||||
}
|
||||
|
||||
public static String getSenderName(String msg) {
|
||||
return splitMsg(msg)[SENDER_NAME_POS];
|
||||
}
|
||||
|
||||
public static String getSenderId(String msg) {
|
||||
return splitMsg(msg)[SENDERID_POS];
|
||||
}
|
||||
|
||||
public static String getTime(String msg) {
|
||||
return splitMsg(msg)[TIME_POS];
|
||||
}
|
||||
|
||||
public static String getContent(String msg) {
|
||||
return splitMsg(msg)[CONTENT_POS];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user