聊天界面所有前端
Change-Id: I0278cae1cd7152f4f797c00ac68cbdb36a2dd768
diff --git a/src/api/__tests__/chat.test.js b/src/api/__tests__/chat.test.js
new file mode 100644
index 0000000..8571fbc
--- /dev/null
+++ b/src/api/__tests__/chat.test.js
@@ -0,0 +1,84 @@
+// src/api/__tests__/chat.test.js
+const axios = require('axios');
+const MockAdapter = require('axios-mock-adapter');
+const {
+ createChat,
+ deleteChat,
+ getChatsByUser,
+ getChatsBetweenUsers
+} = require('../chat');
+
+jest.mock('axios');
+
+describe('Chat API Tests', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('createChat should post chat data', async () => {
+ const mockData = {
+ senderId: 1,
+ receiverId: 2,
+ content: 'Hello!',
+ talkTime: '2025-06-09T10:00:00'
+ };
+ const mockResponse = { data: { ...mockData, informationid: 101 } };
+ axios.post.mockResolvedValue(mockResponse);
+
+ const response = await createChat(mockData);
+
+ expect(axios.post).toHaveBeenCalledWith(
+ 'http://localhost:8080/chat/create',
+ mockData
+ );
+ expect(response.data).toEqual(mockResponse.data);
+ });
+
+ test('deleteChat should send delete request', async () => {
+ axios.delete.mockResolvedValue({ data: '删除成功' });
+
+ const response = await deleteChat(101);
+
+ expect(axios.delete).toHaveBeenCalledWith('http://localhost:8080/chat/delete/101');
+ expect(response.data).toBe('删除成功');
+ });
+
+ test('getChatsByUser should fetch all chats for a user', async () => {
+ const userId = 1;
+ const mockData = { data: [{ senderId: 1, receiverId: 2, content: 'Hi' }] };
+ axios.get.mockResolvedValue(mockData);
+
+ const result = await getChatsByUser(userId);
+
+ expect(axios.get).toHaveBeenCalledWith(`http://localhost:8080/chat/user/${userId}`);
+ expect(result).toEqual(mockData.data);
+ });
+
+ test('getChatsBetweenUsers should fetch chats between two users', async () => {
+ const user1 = 1;
+ const user2 = 2;
+ const mockData = {
+ data: [{ senderId: 1, receiverId: 2, content: 'Hi there!' }]
+ };
+ axios.get.mockResolvedValue(mockData);
+
+ const result = await getChatsBetweenUsers(user1, user2);
+
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/chat/between', {
+ params: { user1, user2 }
+ });
+ expect(result).toEqual(mockData.data);
+ });
+
+ test('getChatsByUser should return [] on error', async () => {
+ axios.get.mockRejectedValue(new Error('Failed'));
+ const result = await getChatsByUser(1);
+ expect(result).toEqual([]);
+ });
+
+ test('getChatsBetweenUsers should return [] on error', async () => {
+ axios.get.mockRejectedValue(new Error('Failed'));
+ const result = await getChatsBetweenUsers(1, 2);
+ expect(result).toEqual([]);
+ });
+});
diff --git a/src/api/chat.js b/src/api/chat.js
new file mode 100644
index 0000000..ab2d38d
--- /dev/null
+++ b/src/api/chat.js
@@ -0,0 +1,40 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/chat';
+
+// 创建聊天记录
+export const createChat = (chatData) => {
+ return axios.post(`${BASE_URL}/create`, chatData);
+};
+
+// 删除聊天记录
+export const deleteChat = (chatId) => {
+ return axios.delete(`${BASE_URL}/delete/${chatId}`);
+};
+
+// 获取某个用户的所有聊天记录(与所有人的)
+export async function getChatsByUser(userId) {
+ try {
+ const response = await axios.get(`${BASE_URL}/user/${userId}`);
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('获取用户聊天记录失败', error);
+ return [];
+ }
+}
+
+// 获取两个用户之间的聊天记录(请求参数形式)
+export async function getChatsBetweenUsers(user1Id, user2Id) {
+ try {
+ const response = await axios.get(`${BASE_URL}/between`, {
+ params: {
+ user1: user1Id,
+ user2: user2Id
+ }
+ });
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('获取两人聊天记录失败', error);
+ return [];
+ }
+}
diff --git a/src/components/ChatBox.jsx b/src/components/ChatBox.jsx
new file mode 100644
index 0000000..2efbfc8
--- /dev/null
+++ b/src/components/ChatBox.jsx
@@ -0,0 +1,202 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Input, Button, Avatar, Typography, List, Popover, message as antdMessage } from 'antd';
+import { SendOutlined, SmileOutlined } from '@ant-design/icons';
+import { getChatsBetweenUsers, createChat } from '../api/chat';
+import axios from 'axios';
+import './chat.css';
+
+const { Text } = Typography;
+const { TextArea } = Input;
+
+// 完整表情数组
+const emojis = [
+ "😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇",
+ "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚",
+ "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🤩",
+ "🥳", "😏", "😒", "😞", "😔", "😟", "😕", "🙁", "☹️", "😣",
+ "😖", "😫", "😩", "🥺", "😢", "😭", "😤", "😠", "😡", "🤬",
+ "🤯", "😳", "🥵", "🥶", "😱", "😨", "😰", "😥", "😓", "🤗",
+ "🤔", "🤭", "🤫", "🤥", "😶", "😐", "😑", "😬", "🙄", "😯",
+ "😦", "😧", "😮", "😲", "🥱", "😴", "🤤", "😪", "😵", "🤐",
+ "🥴", "🤢", "🤮", "🤧", "😷", "🤒", "🤕", "🤑", "🤠", "😈",
+ "👿", "👹", "👺", "🤡", "💩", "👻", "💀", "☠️", "👽", "👾",
+ "🤖", "🎃", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿",
+ "😾", "👋", "🤚", "🖐", "✋", "🖖", "👌", "🤏", "✌️", "🤞",
+ "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "☝️", "👍",
+ "👎", "✊", "👊", "🤛", "🤜", "👏", "🙌", "👐", "🤲", "🤝",
+ "🙏", "✍️", "💅", "🤳", "💪", "🦾", "🦵", "🦿", "🦶", "👂",
+ "🦻", "👃", "🧠", "🦷", "🦴", "👀", "👁", "👅", "👄", "💋",
+ "🩸", "💘", "💝", "💖", "💗", "💓", "💞", "💕", "💟", "❣️",
+ "💔", "❤️", "🧡", "💛", "💚", "💙", "💜", "🤎", "🖤", "🤍",
+ "💯", "💢", "💥", "💫", "💦", "💨", "🕳", "💣", "💬", "👁️🗨️",
+ "🗨", "🗯", "💭", "💤", "👋", "🤚", "🖐", "✋", "🖖", "👌"
+];
+
+const ChatBox = ({ senderId, receiverId }) => {
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState('');
+ const [showEmojis, setShowEmojis] = useState(false);
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+ const [loading, setLoading] = useState(false);
+ const [cursorPosition, setCursorPosition] = useState(0);
+ const [users, setUsers] = useState({}); // {id: {username, avatar}}
+
+ const formatTime = timeStr => new Date(timeStr).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+
+ const fetchUserInfo = async (id) => {
+ if (!id || users[id]) return;
+ try {
+ const res = await axios.get(`http://localhost:8080/user/getDecoration?userid=${id}`);
+ const info = res.data.data;
+ setUsers(u => ({ ...u, [id]: { username: info.username, avatar: info.image } }));
+ } catch (e) {
+ console.error('获取用户信息失败', e);
+ setUsers(u => ({ ...u, [id]: { username: `用户${id}`, avatar: null } }));
+ }
+ };
+
+ const fetchMessages = async () => {
+ if (!senderId || !receiverId) return;
+ try {
+ await fetchUserInfo(senderId);
+ await fetchUserInfo(receiverId);
+ const data = await getChatsBetweenUsers(senderId, receiverId);
+ setMessages(data.sort((a, b) => new Date(a.talkTime) - new Date(b.talkTime)));
+ } catch {
+ antdMessage.error('加载聊天记录失败');
+ }
+ };
+
+ useEffect(fetchMessages, [senderId, receiverId]);
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ const handleSend = async () => {
+ const trimmed = input.trim();
+ if (!trimmed) return;
+ setLoading(true);
+ const newChat = { senderId, receiverId, content: trimmed, talkTime: new Date().toISOString() };
+ try {
+ await createChat(newChat);
+ setInput('');
+ fetchMessages();
+ } catch {
+ antdMessage.error('发送失败,请重试');
+ }
+ setLoading(false);
+ };
+
+ const handleKeyDown = e => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ const handleEmojiInsert = emoji => {
+ const start = input.slice(0, cursorPosition);
+ const end = input.slice(cursorPosition);
+ const newText = start + emoji + end;
+ setInput(newText);
+ setShowEmojis(false);
+ setTimeout(() => {
+ const pos = start.length + emoji.length;
+ inputRef.current.focus();
+ inputRef.current.setSelectionRange(pos, pos);
+ setCursorPosition(pos);
+ }, 0);
+ };
+
+ const handleInputSelection = () => inputRef.current && setCursorPosition(inputRef.current.selectionStart);
+
+ const renderEmojiPicker = () => (
+ <div className="emoji-picker">
+ {emojis.map((e, i) => (
+ <span key={i} className="emoji-item" onClick={() => handleEmojiInsert(e)}>
+ {e}
+ </span>
+ ))}
+ </div>
+ );
+
+ return (
+ <div className="chatbox-container">
+ <div className="chatbox-header">
+ <Text strong style={{ fontSize: 20 }}>
+ 🟢 Chat with {users[receiverId]?.username || receiverId}
+ </Text>
+ </div>
+
+ <div className="chatbox-content">
+ {messages.length === 0 ? (
+ <div className="no-messages">暂无聊天记录</div>
+ ) : (
+ <List
+ dataSource={messages}
+ renderItem={msg => {
+ const isSender = msg.senderId === senderId;
+ const otherId = isSender ? senderId : receiverId;
+ const user = users[otherId] || {};
+ const avatarContent = user.avatar ? (
+ <Avatar src={user.avatar} className="avatar" />
+ ) : (
+ <Avatar className="avatar">{String(otherId).slice(-2)}</Avatar>
+ );
+ return (
+ <List.Item key={msg.informationid} className={`chat-message ${isSender ? 'sender' : 'receiver'}`}>
+ {!isSender && avatarContent}
+ <div className="message-bubble-wrapper">
+ <div className="message-bubble">
+ <Text>{msg.content}</Text>
+ <div className="message-time">{formatTime(msg.talkTime)}</div>
+ </div>
+ </div>
+ {isSender && avatarContent}
+ </List.Item>
+ );
+ }}
+ />
+ )}
+ <div ref={messagesEndRef} />
+ </div>
+
+ <div className="chatbox-footer">
+ <Popover
+ content={renderEmojiPicker()}
+ title="选择表情"
+ trigger="click"
+ open={showEmojis}
+ onOpenChange={setShowEmojis}
+ placement="topLeft"
+ overlayClassName="emoji-popover"
+ arrow={false}
+ >
+ <Button icon={<SmileOutlined />} type="text" className="emoji-btn" />
+ </Popover>
+ <TextArea
+ ref={inputRef}
+ value={input}
+ onChange={e => { setInput(e.target.value); setCursorPosition(e.target.selectionStart); }}
+ onClick={handleInputSelection}
+ onSelect={handleInputSelection}
+ onKeyUp={handleInputSelection}
+ onKeyDown={handleKeyDown}
+ placeholder="输入消息,Enter发送,Shift+Enter换行"
+ autoSize={{ minRows: 2, maxRows: 6 }}
+ disabled={loading}
+ maxLength={500}
+ showCount
+ className="chat-input"
+ />
+ <Button type="primary" icon={<SendOutlined />} onClick={handleSend} loading={loading} disabled={!input.trim()} className="send-btn">
+ 发送
+ </Button>
+ </div>
+ </div>
+ );
+};
+
+export default ChatBox;
\ No newline at end of file
diff --git a/src/components/chat.css b/src/components/chat.css
new file mode 100644
index 0000000..b02d961
--- /dev/null
+++ b/src/components/chat.css
@@ -0,0 +1,216 @@
+/* chat.css */
+.chatbox-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: #fafafa;
+ border: 1px solid #d9d9d9;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.chatbox-header {
+ padding: 16px;
+ background-color: #f0f5ff;
+ border-bottom: 1px solid #adc6ff;
+ text-align: center;
+}
+
+.chatbox-content {
+ flex: 1;
+ padding: 16px;
+ background-color: #fff;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: #91d5ff transparent;
+}
+
+.chatbox-content::-webkit-scrollbar {
+ width: 8px;
+}
+
+.chatbox-content::-webkit-scrollbar-thumb {
+ background-color: #91d5ff;
+ border-radius: 4px;
+}
+
+.chatbox-content::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.no-messages {
+ color: #999;
+ text-align: center;
+ margin-top: 40px;
+ font-size: 14px;
+}
+
+.chat-message {
+ display: flex;
+ align-items: flex-end;
+ margin-bottom: 10px;
+ gap: 8px;
+ max-width: 100%;
+}
+
+.chat-message.sender {
+ justify-content: flex-end;
+}
+
+.chat-message.receiver {
+ justify-content: flex-start;
+}
+
+.message-bubble-wrapper {
+ max-width: 75%;
+}
+
+.message-bubble {
+ padding: 10px 14px;
+ border-radius: 14px;
+ font-size: 14px;
+ word-break: break-word;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+}
+
+.chat-message.sender .message-bubble {
+ background-color: #69c0ff;
+ color: #fff;
+ border-bottom-right-radius: 0;
+}
+
+.chat-message.receiver .message-bubble {
+ background-color: #e6f7ff;
+ color: #000;
+ border-bottom-left-radius: 0;
+}
+
+.message-time {
+ font-size: 10px;
+ color: #666;
+ margin-top: 4px;
+ text-align: right;
+}
+
+.avatar {
+ flex-shrink: 0;
+ user-select: none;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
+}
+
+.chatbox-footer {
+ display: flex;
+ padding: 12px;
+ border-top: 1px solid #f0f0f0;
+ background-color: #fff;
+ gap: 8px;
+ align-items: flex-end;
+}
+
+.chat-input {
+ flex: 1;
+ border-radius: 8px;
+ resize: none;
+}
+
+.send-btn {
+ min-width: 80px;
+ border-radius: 8px;
+ height: 44px;
+}
+
+.emoji-btn {
+ background: white;
+ border: none;
+ padding: 6px;
+ border-radius: 8px;
+ color: #666;
+ font-size: 18px;
+ cursor: pointer;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.emoji-btn:hover {
+ background-color: #f0f0f0;
+ color: #333;
+}
+
+.emoji-picker {
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ gap: 6px;
+ padding: 10px;
+ width: 280px;
+ max-height: 260px;
+ overflow-y: auto;
+}
+
+.emoji-item {
+ font-size: 20px;
+ padding: 4px;
+ cursor: pointer;
+ text-align: center;
+ transition: all 0.2s ease;
+}
+
+.emoji-item:hover {
+ transform: scale(1.2);
+ background: #f0f9ff;
+ border-radius: 4px;
+}
+
+.emoji-popover .ant-popover-inner {
+ padding: 0;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.emoji-popover .ant-popover-title {
+ padding: 8px;
+ border-bottom: 1px solid #f0f0f0;
+ font-weight: bold;
+ background-color: #f9f9f9;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+ .chatbox-container {
+ border-radius: 0;
+ border: none;
+ }
+
+ .chatbox-header {
+ padding: 12px;
+ }
+
+ .chatbox-content {
+ padding: 12px;
+ }
+
+ .message-bubble-wrapper {
+ max-width: 85%;
+ }
+
+ .chatbox-footer {
+ padding: 8px;
+ gap: 6px;
+ }
+
+ .emoji-picker {
+ width: 240px;
+ grid-template-columns: repeat(7, 1fr);
+ }
+
+ .send-btn {
+ min-width: 60px;
+ height: 40px;
+ }
+
+ .emoji-btn {
+ height: 40px;
+ }
+}
\ No newline at end of file