提交之GitHub

This commit is contained in:
2026-02-08 23:58:00 +08:00
commit b4f25e99b1
43 changed files with 7926 additions and 0 deletions
+530
View File
@@ -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());
});
}
}
+84
View File
@@ -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();
}
}
+95
View File
@@ -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);
}
}
+127
View File
@@ -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);
}
}
}
+366
View File
@@ -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);
}
}
+214
View File
@@ -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();
}
}
+198
View File
@@ -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);
}
}
+177
View File
@@ -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;
}
}