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

✨ feat(classroom): 实现多类型消息系统

- 定义Message接口,支持text/image/system三种消息类型
- 重构消息展示组件,根据消息类型渲染不同UI
- 优化图片消息显示,移除冗余图片标签
- 将系统通知统一改为system类型消息
- 改进消息发送逻辑,区分不同类型消息处理

♻️ refactor(classroom): 优化消息处理逻辑

- 重命名showMessage为showSystemMessage,明确其用途
- 调整消息状态管理,使用强类型Message数组替代字符串数组
- 优化IM事件处理,统一使用系统消息展示连接状态变更
- 改进课堂状态变更通知,使用系统消息提示用户
- 移除冗余代码,清理注释和调试信息
yourname 6 месяцев назад
Родитель
Сommit
efa1ca25a1

+ 36 - 25
src/client/mobile/components/Classroom/ClassroomLayout.tsx

@@ -1,5 +1,5 @@
 import React, { ReactNode } from 'react';
 import React, { ReactNode } from 'react';
-import { Role } from './useClassroom';
+import { Role, type Message } from './useClassroom';
 import { useClassroomContext } from './ClassroomProvider';
 import { useClassroomContext } from './ClassroomProvider';
 import {
 import {
   VideoCameraIcon,
   VideoCameraIcon,
@@ -102,32 +102,43 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
           {/* 消息列表 - 填充剩余空间 */}
           {/* 消息列表 - 填充剩余空间 */}
           <div className="flex-1 overflow-y-auto bg-white shadow-lg p-4">
           <div className="flex-1 overflow-y-auto bg-white shadow-lg p-4">
             {messageList.map((msg, i) => {
             {messageList.map((msg, i) => {
-              // 检查是否是图片消息
-              if (msg.startsWith('[图片]')) {
-                const parts = msg.split(': ');
-                const senderInfo = parts[0].replace('[图片] ', '');
-                const imageUrl = parts.slice(1).join(': ');
+              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>
+                  );
                 
                 
-                return (
-                  <div key={i} className="mb-3 p-2 bg-gray-50 rounded-lg">
-                    <div className="text-xs text-gray-500 mb-1">{senderInfo}</div>
-                    <img
-                      src={imageUrl}
-                      alt="图片消息"
-                      className="max-w-full max-h-48 rounded object-contain"
-                      onError={(e) => {
-                        (e.target as HTMLImageElement).style.display = 'none';
-                      }}
-                    />
-                    <div className="text-xs text-gray-400 mt-1">[图片]</div>
-                  </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>
+                  );
               }
               }
-              
-              // 普通文本消息
-              return (
-                <div key={i} className="text-sm mb-1">{msg}</div>
-              );
             })}
             })}
           </div>
           </div>
         </div>
         </div>

+ 60 - 34
src/client/mobile/components/Classroom/useClassroom.ts

@@ -72,6 +72,14 @@ export enum ClassStatus {
 }
 }
 
 
 
 
+// 定义消息类型
+export interface Message {
+  type: 'text' | 'image' | 'system';
+  content: string;
+  sender?: string;
+  timestamp: number;
+}
+
 export const useClassroom = ({ user }:{ user : User }) => {
 export const useClassroom = ({ user }:{ user : User }) => {
   // 状态管理
   // 状态管理
   // const [userId, setUserId] = useState<string>(''); // 保持string类型
   // const [userId, setUserId] = useState<string>(''); // 保持string类型
@@ -85,7 +93,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
   const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
   const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
   const [isJoinedClass, setIsJoinedClass] = useState<boolean>(false);
   const [isJoinedClass, setIsJoinedClass] = useState<boolean>(false);
   const [msgText, setMsgText] = useState<string>('');
   const [msgText, setMsgText] = useState<string>('');
-  const [messageList, setMessageList] = useState<string[]>([]);
+  const [messageList, setMessageList] = useState<Message[]>([]);
   const [errorMessage, setErrorMessage] = useState<string>('');
   const [errorMessage, setErrorMessage] = useState<string>('');
   const [classStatus, setClassStatus] = useState<ClassStatus>(ClassStatus.NOT_STARTED);
   const [classStatus, setClassStatus] = useState<ClassStatus>(ClassStatus.NOT_STARTED);
   const [handUpList, setHandUpList] = useState<HandUpRequest[]>([]);
   const [handUpList, setHandUpList] = useState<HandUpRequest[]>([]);
@@ -106,12 +114,33 @@ export const useClassroom = ({ user }:{ user : User }) => {
   const remoteCameraContainer = useRef<HTMLDivElement>(null); // 摄像头小窗容器
   const remoteCameraContainer = useRef<HTMLDivElement>(null); // 摄像头小窗容器
 
 
   // 辅助函数
   // 辅助函数
-  const showMessage = (text: string): void => {
-    setMessageList((prevMessageList) => [...prevMessageList, text])
+  const showMessage = (text: string, sender?: string): void => {
+    const message: Message = {
+      type: 'text',
+      content: text,
+      sender,
+      timestamp: Date.now()
+    };
+    setMessageList((prevMessageList) => [...prevMessageList, message])
   };
   };
 
 
   const showImageMessage = (imageUrl: string, senderName: string): void => {
   const showImageMessage = (imageUrl: string, senderName: string): void => {
-    setMessageList((prevMessageList) => [...prevMessageList, `[图片] ${senderName}: ${imageUrl}`])
+    const message: Message = {
+      type: 'image',
+      content: imageUrl,
+      sender: senderName,
+      timestamp: Date.now()
+    };
+    setMessageList((prevMessageList) => [...prevMessageList, message])
+  };
+
+  const showSystemMessage = (text: string): void => {
+    const message: Message = {
+      type: 'system',
+      content: text,
+      timestamp: Date.now()
+    };
+    setMessageList((prevMessageList) => [...prevMessageList, message])
   };
   };
 
 
   const showToast = (type: 'info' | 'success' | 'error', message: string): void => {
   const showToast = (type: 'info' | 'success' | 'error', message: string): void => {
@@ -133,11 +162,11 @@ export const useClassroom = ({ user }:{ user : User }) => {
     if (!role) return;    
     if (!role) return;    
 
 
     imEngine.current.on('connectsuccess', () => {
     imEngine.current.on('connectsuccess', () => {
-      showMessage('IM连接成功');
+      showSystemMessage('IM连接成功');
     });
     });
 
 
     imEngine.current.on('disconnect', async (code: number) => {
     imEngine.current.on('disconnect', async (code: number) => {
-      showMessage(`IM断开连接: ${code}`);
+      showSystemMessage(`IM断开连接: ${code}`);
       // 自动重连
       // 自动重连
       try {
       try {
         const res = await aliyunClient.im_token.$post({
         const res = await aliyunClient.im_token.$post({
@@ -160,10 +189,10 @@ export const useClassroom = ({ user }:{ user : User }) => {
             role
             role
           }
           }
         });
         });
-        showMessage('IM自动重连成功');
+        showSystemMessage('IM自动重连成功');
       } catch (err: unknown) {
       } catch (err: unknown) {
         const error = err as Error;
         const error = err as Error;
-        showMessage(`IM自动重连失败: ${error.message}`);
+        showSystemMessage(`IM自动重连失败: ${error.message}`);
       }
       }
     });
     });
   };
   };
@@ -172,7 +201,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
     if (!imGroupManager.current) return;
     if (!imGroupManager.current) return;
 
 
     imGroupManager.current.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => {
     imGroupManager.current.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => {
-      showMessage(`成员变更: 加入${joinUsers.length}人, 离开${leaveUsers.length}人`);
+      showSystemMessage(`成员变更: 加入${joinUsers.length}人, 离开${leaveUsers.length}人`);
       
       
       // 更新学生列表
       // 更新学生列表
       setStudents(prevStudents => {
       setStudents(prevStudents => {
@@ -229,17 +258,17 @@ export const useClassroom = ({ user }:{ user : User }) => {
           const data = JSON.parse(msg.data);
           const data = JSON.parse(msg.data);
           if (data.action === 'start_class') {
           if (data.action === 'start_class') {
             setClassStatus(ClassStatus.IN_PROGRESS);
             setClassStatus(ClassStatus.IN_PROGRESS);
-            showMessage('老师已开始上课');
+            showSystemMessage('老师已开始上课');
           } else if (data.action === 'end_class') {
           } else if (data.action === 'end_class') {
             setClassStatus(ClassStatus.ENDED);
             setClassStatus(ClassStatus.ENDED);
-            showMessage('老师已结束上课');
+            showSystemMessage('老师已结束上课');
             
             
             // 学生端收到结束课堂消息后,立即自动离开群组
             // 学生端收到结束课堂消息后,立即自动离开群组
             if (role === Role.Student) {
             if (role === Role.Student) {
               try {
               try {
                 if (imGroupManager.current && classId) {
                 if (imGroupManager.current && classId) {
                   await imGroupManager.current.leaveGroup(classId);
                   await imGroupManager.current.leaveGroup(classId);
-                  showMessage('已自动离开课堂群组');
+                  showSystemMessage('已自动离开课堂群组');
                 }
                 }
                 if (aliRtcEngine.current) {
                 if (aliRtcEngine.current) {
                   await leaveRtcChannel();
                   await leaveRtcChannel();
@@ -263,7 +292,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         try {
         try {
           const data = JSON.parse(msg.data);
           const data = JSON.parse(msg.data);
           if (data.action === 'toggle_mute' && data.userId === userId) {
           if (data.action === 'toggle_mute' && data.userId === userId) {
-            showMessage(data.mute ? '你已被老师静音' : '老师已取消你的静音');
+            showSystemMessage(data.mute ? '你已被老师静音' : '老师已取消你的静音');
           }
           }
         } catch (err) {
         } catch (err) {
           console.error('解析静音指令失败', err);
           console.error('解析静音指令失败', err);
@@ -277,7 +306,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
               timestamp: data.timestamp || Date.now()
               timestamp: data.timestamp || Date.now()
             };
             };
             setHandUpList([...handUpList, handUpData]);
             setHandUpList([...handUpList, handUpData]);
-            showMessage(`${data.studentName || data.studentId} 举手了`);
+            showSystemMessage(`${data.studentName || data.studentId} 举手了`);
           } else if (data.action === InteractionAction.CancelHandUp) {
           } else if (data.action === InteractionAction.CancelHandUp) {
             setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
             setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
           }
           }
@@ -302,7 +331,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
             };
             };
             setQuestions([...questions, question]);
             setQuestions([...questions, question]);
           }
           }
-          showMessage(`收到问题: ${data.question}`);
+          showSystemMessage(`收到问题: ${data.question}`);
         } catch (err) {
         } catch (err) {
           console.error('解析问题消息失败', err);
           console.error('解析问题消息失败', err);
         }
         }
@@ -310,7 +339,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         try {
         try {
           const data = JSON.parse(msg.data) as InteractionMessage;
           const data = JSON.parse(msg.data) as InteractionMessage;
           if (data.action === InteractionAction.AnswerHandUp && data.studentId === userId) {
           if (data.action === InteractionAction.AnswerHandUp && data.studentId === userId) {
-            showMessage('老师已应答你的举手');
+            showSystemMessage('老师已应答你的举手');
             setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
             setHandUpList(handUpList.filter(h => h.studentId !== data.studentId));
           }
           }
         } catch (err) {
         } catch (err) {
@@ -320,7 +349,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         try {
         try {
           const data = JSON.parse(msg.data) as InteractionMessage;
           const data = JSON.parse(msg.data) as InteractionMessage;
           if (data.action === InteractionAction.KickStudent && data.studentId === userId) {
           if (data.action === InteractionAction.KickStudent && data.studentId === userId) {
-            showMessage('您已被老师移出课堂');
+            showSystemMessage('您已被老师移出课堂');
             // 学生被踢出后自动离开课堂
             // 学生被踢出后自动离开课堂
             setTimeout(() => {
             setTimeout(() => {
               leaveClass();
               leaveClass();
@@ -334,7 +363,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         // 使用正确的 ImUser 类型处理发送者信息
         // 使用正确的 ImUser 类型处理发送者信息
         const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
         const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
         const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
         const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
-        showMessage(`${senderName}: ${msg.data}`);
+        showMessage(msg.data, senderName);
       } else if (msg.type === 88895) { // 图片消息
       } else if (msg.type === 88895) { // 图片消息
         try {
         try {
           const data = JSON.parse(msg.data);
           const data = JSON.parse(msg.data);
@@ -350,12 +379,12 @@ export const useClassroom = ({ user }:{ user : User }) => {
               const fileInfo = await response.json();
               const fileInfo = await response.json();
               showImageMessage(fileInfo.fullUrl, senderName);
               showImageMessage(fileInfo.fullUrl, senderName);
             } else {
             } else {
-              showMessage(`${senderName}: [图片] (获取失败)`);
+              showSystemMessage(`${senderName}: 图片获取失败`);
             }
             }
           }
           }
         } catch (err) {
         } catch (err) {
           console.error('解析图片消息失败', err);
           console.error('解析图片消息失败', err);
-          showMessage('收到一张图片(解析失败)');
+          showSystemMessage('图片消息解析失败');
         }
         }
       }
       }
     });
     });
@@ -382,7 +411,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
   const listenRtcEvents = () => {
   const listenRtcEvents = () => {
     if (!aliRtcEngine.current) return;
     if (!aliRtcEngine.current) return;
 
 
-    showMessage('注册rtc事件监听')
+    showSystemMessage('注册rtc事件监听')
 
 
     aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
     aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => {
       // showMessage(`用户 ${userId} 加入课堂`);
       // showMessage(`用户 ${userId} 加入课堂`);
@@ -435,16 +464,16 @@ export const useClassroom = ({ user }:{ user : User }) => {
             );
             );
             
             
             console.log(`已订阅用户 ${userId} 的视频流`);
             console.log(`已订阅用户 ${userId} 的视频流`);
-            showMessage(`已显示用户 ${userId} 的视频`);
+            showSystemMessage(`已显示用户 ${userId} 的视频`);
           } catch (err) {
           } catch (err) {
             console.error(`订阅用户 ${userId} 视频流失败:`, err);
             console.error(`订阅用户 ${userId} 视频流失败:`, err);
-            showMessage(`订阅用户 ${userId} 视频流失败`);
+            showSystemMessage(`订阅用户 ${userId} 视频流失败`);
           }
           }
           break;
           break;
           
           
         case 1: // 取消订阅
         case 1: // 取消订阅
           console.log(`取消订阅用户 ${userId} 的视频流`);
           console.log(`取消订阅用户 ${userId} 的视频流`);
-          showMessage(`取消订阅用户 ${userId} 的视频流`);
+          showSystemMessage(`取消订阅用户 ${userId} 的视频流`);
           removeRemoteVideo(userId, 'camera');
           removeRemoteVideo(userId, 'camera');
           break;
           break;
           
           
@@ -496,16 +525,16 @@ export const useClassroom = ({ user }:{ user : User }) => {
             );
             );
             
             
             console.log(`已订阅用户 ${userId} 的屏幕分享流`);
             console.log(`已订阅用户 ${userId} 的屏幕分享流`);
-            showMessage(`已显示用户 ${userId} 的屏幕分享`);
+            showSystemMessage(`已显示用户 ${userId} 的屏幕分享`);
           } catch (err) {
           } catch (err) {
             console.error(`订阅用户 ${userId} 屏幕分享流失败:`, err);
             console.error(`订阅用户 ${userId} 屏幕分享流失败:`, err);
-            showMessage(`订阅用户 ${userId} 屏幕分享流失败`);
+            showSystemMessage(`订阅用户 ${userId} 屏幕分享流失败`);
           }
           }
           break;
           break;
           
           
         case 1: // 取消订阅
         case 1: // 取消订阅
           console.log(`取消订阅用户 ${userId} 的屏幕分享流`);
           console.log(`取消订阅用户 ${userId} 的屏幕分享流`);
-          showMessage(`取消订阅用户 ${userId} 的屏幕分享流`);
+          showSystemMessage(`取消订阅用户 ${userId} 的屏幕分享流`);
           removeRemoteVideo(userId, 'screen');
           removeRemoteVideo(userId, 'screen');
           break;
           break;
           
           
@@ -607,11 +636,11 @@ export const useClassroom = ({ user }:{ user : User }) => {
               });
               });
             
             
             setStudents(existingMembers);
             setStudents(existingMembers);
-            showMessage(`已加载 ${existingMembers.length} 名现有学生`);
+            showSystemMessage(`已加载 ${existingMembers.length} 名现有学生`);
           }
           }
         } catch (err) {
         } catch (err) {
           console.error('获取群组成员失败:', err);
           console.error('获取群组成员失败:', err);
-          showMessage('获取现有学生列表失败,将只显示新加入的学生');
+          showSystemMessage('获取现有学生列表失败,将只显示新加入的学生');
         }
         }
       }
       }
 
 
@@ -711,9 +740,6 @@ export const useClassroom = ({ user }:{ user : User }) => {
         type: 88895, // 使用单一消息类型用于图片
         type: 88895, // 使用单一消息类型用于图片
         level: NORMAL,
         level: NORMAL,
       });
       });
-// 发送方不需要手动显示消息,IM系统会自动将消息广播给所有用户(包括发送方自己)
-// 消息接收处理逻辑(第338-360行)会自动处理并显示图片消息
-
       
       
     } catch (err: any) {
     } catch (err: any) {
       // 检查是否为禁言错误 (错误码442)
       // 检查是否为禁言错误 (错误码442)
@@ -769,7 +795,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         // 3. 关闭群组(删除群组)
         // 3. 关闭群组(删除群组)
         if (imGroupManager.current) {
         if (imGroupManager.current) {
           await imGroupManager.current.closeGroup(classId);
           await imGroupManager.current.closeGroup(classId);
-          showMessage('群组已关闭');
+          showSystemMessage('群组已关闭');
           
           
           // 4. 清理状态
           // 4. 清理状态
           setIsJoinedClass(false);
           setIsJoinedClass(false);
@@ -859,7 +885,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
       await gm.joinGroup(response.groupId);
       await gm.joinGroup(response.groupId);
       
       
       showToast('success', '课堂创建并加入成功');
       showToast('success', '课堂创建并加入成功');
-      showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`);
+      showSystemMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`);
       
       
       setClassId(response.groupId);
       setClassId(response.groupId);
       setIsJoinedClass(true);
       setIsJoinedClass(true);