2
0
Просмотр исходного кода

♻️ refactor(classroom): 重构消息列表组件结构

- 将消息列表渲染逻辑从ClassroomLayout抽离到独立的MessageList组件
- 创建MessageBubble组件统一处理不同类型消息的展示逻辑
- 优化消息布局样式,区分自己发送和他人发送的消息样式
- 添加消息发送时间显示功能

✨ feat(classroom): 增强消息显示功能

- 实现消息自动滚动到底部功能,新消息出现时自动定位
- 添加消息发送者ID字段,用于判断是否为自己发送的消息
- 为消息添加发送时间格式化显示
- 优化图片消息布局和样式,增加圆角和阴影效果
yourname 6 месяцев назад
Родитель
Сommit
82abd7473b

+ 2 - 41
src/client/mobile/components/Classroom/ClassroomLayout.tsx

@@ -24,6 +24,7 @@ import { AllMuteToggleButton } from './AllMuteToggleButton';
 import { StudentHandUpButton } from './StudentHandUpButton';
 import { StudentQuestionButton } from './StudentQuestionButton';
 import { FileSelector } from '@/client/mobile/components/FileSelector';
+import { MessageList } from './MessageList';
 
 interface ClassroomLayoutProps {
   children: ReactNode;
@@ -100,47 +101,7 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
         {/* 消息区域 */}
         <div className="flex flex-col h-full">
           {/* 消息列表 - 填充剩余空间 */}
-          <div className="flex-1 overflow-y-auto bg-white shadow-lg p-4">
-            {messageList.map((msg, i) => {
-              switch (msg.type) {
-                case 'image':
-                  return (
-                    <div key={i} className="mb-3 p-2 bg-gray-50 rounded-lg">
-                      {msg.sender && (
-                        <div className="text-xs text-gray-500 mb-1">{msg.sender}</div>
-                      )}
-                      <img
-                        src={msg.content}
-                        alt="图片消息"
-                        className="max-w-full max-h-48 rounded object-contain"
-                        onError={(e) => {
-                          (e.target as HTMLImageElement).style.display = 'none';
-                        }}
-                      />
-                    </div>
-                  );
-                
-                case 'text':
-                  return (
-                    <div key={i} className="text-sm mb-1">
-                      {msg.sender ? `${msg.sender}: ${msg.content}` : msg.content}
-                    </div>
-                  );
-                
-                case 'system':
-                  return (
-                    <div key={i} className="text-sm mb-1 text-gray-500 italic">
-                      {msg.content}
-                    </div>
-                  );
-                
-                default:
-                  return (
-                    <div key={i} className="text-sm mb-1">{msg.content}</div>
-                  );
-              }
-            })}
-          </div>
+          <MessageList messages={messageList} />
         </div>
 
         {/* 底部固定区域 */}

+ 93 - 0
src/client/mobile/components/Classroom/MessageBubble.tsx

@@ -0,0 +1,93 @@
+import React from 'react';
+import { Message } from './useClassroom';
+import { useClassroomContext } from './ClassroomProvider';
+
+interface MessageBubbleProps {
+  message: Message;
+  isOwnMessage: boolean;
+}
+
+export const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isOwnMessage }) => {
+  
+  const formatTime = (timestamp: number) => {
+    return new Date(timestamp).toLocaleTimeString('zh-CN', {
+      hour: '2-digit',
+      minute: '2-digit'
+    });
+  };
+
+  switch (message.type) {
+    case 'text':
+      return (
+        <div className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'} mb-3`}>
+          <div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-2xl ${
+            isOwnMessage 
+              ? 'bg-blue-500 text-white rounded-br-none' 
+              : 'bg-gray-200 text-gray-800 rounded-bl-none'
+          }`}>
+            {!isOwnMessage && message.sender && (
+              <div className="text-xs font-medium mb-1 opacity-80">
+                {message.sender}
+              </div>
+            )}
+            <div className="text-sm break-words">
+              {message.content}
+            </div>
+            <div className={`text-xs mt-1 opacity-70 ${
+              isOwnMessage ? 'text-blue-100' : 'text-gray-500'
+            }`}>
+              {formatTime(message.timestamp)}
+            </div>
+          </div>
+        </div>
+      );
+
+    case 'image':
+      return (
+        <div className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'} mb-3`}>
+          <div className={`max-w-xs px-3 py-2 rounded-2xl ${
+            isOwnMessage 
+              ? 'bg-blue-500 text-white rounded-br-none' 
+              : 'bg-gray-200 text-gray-800 rounded-bl-none'
+          }`}>
+            {!isOwnMessage && message.sender && (
+              <div className="text-xs font-medium mb-1 opacity-80">
+                {message.sender}
+              </div>
+            )}
+            <img
+              src={message.content}
+              alt="图片消息"
+              className="max-w-full max-h-48 rounded-lg object-contain"
+              onError={(e) => {
+                (e.target as HTMLImageElement).style.display = 'none';
+              }}
+            />
+            <div className={`text-xs mt-1 opacity-70 ${
+              isOwnMessage ? 'text-blue-100' : 'text-gray-500'
+            }`}>
+              {formatTime(message.timestamp)}
+            </div>
+          </div>
+        </div>
+      );
+
+    case 'system':
+      return (
+        <div className="flex justify-center mb-3">
+          <div className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full italic">
+            {message.content}
+          </div>
+        </div>
+      );
+
+    default:
+      return (
+        <div className="flex justify-center mb-3">
+          <div className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded">
+            {message.content}
+          </div>
+        </div>
+      );
+  }
+};

+ 44 - 0
src/client/mobile/components/Classroom/MessageList.tsx

@@ -0,0 +1,44 @@
+import React, { useEffect, useRef } from 'react';
+import { Message } from './useClassroom';
+import { useClassroomContext } from './ClassroomProvider';
+import { MessageBubble } from './MessageBubble';
+
+interface MessageListProps {
+  messages: Message[];
+}
+
+export const MessageList: React.FC<MessageListProps> = ({ messages }) => {
+  const { userId } = useClassroomContext();
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+
+  // 自动滚动到底部
+  const scrollToBottom = () => {
+    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+  };
+
+  useEffect(() => {
+    scrollToBottom();
+  }, [messages]);
+
+  // 判断消息是否是自己发送的
+  const isOwnMessage = (message: Message): boolean => {
+    // 系统消息不算自己的消息
+    if (message.type === 'system') return false;
+    
+    // 只有有senderId且与当前用户ID匹配时才认为是自己的消息
+    return message.senderId === userId;
+  };
+
+  return (
+    <div className="flex-1 overflow-y-auto bg-white p-4 space-y-2">
+      {messages.map((message, index) => (
+        <MessageBubble
+          key={`${message.timestamp}-${index}`}
+          message={message}
+          isOwnMessage={isOwnMessage(message)}
+        />
+      ))}
+      <div ref={messagesEndRef} />
+    </div>
+  );
+};

+ 7 - 4
src/client/mobile/components/Classroom/useClassroom.ts

@@ -77,6 +77,7 @@ export interface Message {
   type: 'text' | 'image' | 'system';
   content: string;
   sender?: string;
+  senderId?: string;
   timestamp: number;
 }
 
@@ -114,21 +115,23 @@ export const useClassroom = ({ user }:{ user : User }) => {
   const remoteCameraContainer = useRef<HTMLDivElement>(null); // 摄像头小窗容器
 
   // 辅助函数
-  const showMessage = (text: string, sender?: string): void => {
+  const showMessage = (text: string, sender?: string, senderId?: string): void => {
     const message: Message = {
       type: 'text',
       content: text,
       sender,
+      senderId,
       timestamp: Date.now()
     };
     setMessageList((prevMessageList) => [...prevMessageList, message])
   };
 
-  const showImageMessage = (imageUrl: string, senderName: string): void => {
+  const showImageMessage = (imageUrl: string, senderName: string, senderId?: string): void => {
     const message: Message = {
       type: 'image',
       content: imageUrl,
       sender: senderName,
+      senderId,
       timestamp: Date.now()
     };
     setMessageList((prevMessageList) => [...prevMessageList, message])
@@ -363,7 +366,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         // 使用正确的 ImUser 类型处理发送者信息
         const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
         const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
-        showMessage(msg.data, senderName);
+        showMessage(msg.data, senderName, sender?.userId);
       } else if (msg.type === 88895) { // 图片消息
         try {
           const data = JSON.parse(msg.data);
@@ -377,7 +380,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
             const response = await fileClient[':id']['$get']({ param: { id: data.fileId.toString() } });
             if (response.status === 200) {
               const fileInfo = await response.json();
-              showImageMessage(fileInfo.fullUrl, senderName);
+              showImageMessage(fileInfo.fullUrl, senderName, sender?.userId);
             } else {
               showSystemMessage(`${senderName}: 图片获取失败`);
             }