聊天界面所有前端

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