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

✨ feat(classroom): 新增图片消息发送与显示功能

- 添加图片选择按钮和对话框,支持从设备选择图片
- 实现图片消息发送功能,包括文件上传和消息分发
- 优化消息列表,支持图片消息的渲染和错误处理
- 增强消息解析逻辑,区分文本和图片消息类型
- 添加图片发送状态反馈和错误提示

🐛 fix(classroom): 修复消息发送者信息显示问题

- 修正发送者信息解析逻辑,确保正确显示昵称或用户名
- 优化未知用户显示逻辑,使用userId作为备选显示名称
- 添加用户扩展信息解析容错处理,防止JSON解析失败

🔧 chore(classroom): 引入新组件和依赖

- 导入Dialog组件用于图片选择对话框
- 添加FileSelector组件处理图片文件选择
- 引入toast组件提供操作反馈
yourname 6 месяцев назад
Родитель
Сommit
487fb706e1

+ 90 - 6
src/client/mobile/components/Classroom/ClassroomLayout.tsx

@@ -7,11 +7,15 @@ import {
   MicrophoneIcon,
   ShareIcon,
   ClipboardDocumentIcon,
-  PaperAirplaneIcon
+  PaperAirplaneIcon,
+  PhotoIcon
 } from '@heroicons/react/24/outline';
 import { Button } from '@/client/components/ui/button';
 import { Textarea } from '@/client/components/ui/textarea';
 import { Card, CardContent } from '@/client/components/ui/card';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { XMarkIcon } from '@heroicons/react/24/outline';
+import { toast } from 'sonner';
 import { TeacherClassControlButton } from './TeacherClassControlButton';
 import { TeacherStudentManagementButton } from './TeacherStudentManagementButton';
 import { TeacherHandUpManagementButton } from './TeacherHandUpManagementButton';
@@ -19,6 +23,7 @@ import { TeacherQuestionManagementButton } from './TeacherQuestionManagementButt
 import { AllMuteToggleButton } from './AllMuteToggleButton';
 import { StudentHandUpButton } from './StudentHandUpButton';
 import { StudentQuestionButton } from './StudentQuestionButton';
+import { FileSelector } from '@/client/mobile/components/FileSelector';
 
 interface ClassroomLayoutProps {
   children: ReactNode;
@@ -28,6 +33,7 @@ interface ClassroomLayoutProps {
 export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
   const [showVideo, setShowVideo] = React.useState(role !== Role.Teacher);
   const [showShareLink, setShowShareLink] = React.useState(false);
+  const [showImageSelector, setShowImageSelector] = React.useState(false);
   const {
     remoteScreenContainer,
     remoteCameraContainer,
@@ -46,7 +52,9 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
     classStatus,
     shareLink,
     showCameraOverlay,
-    setShowCameraOverlay
+    setShowCameraOverlay,
+    showToast,
+    sendImageMessageById
   } = useClassroomContext();
 
   return (
@@ -93,9 +101,34 @@ 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) => (
-              <div key={i} className="text-sm mb-1">{msg}</div>
-            ))}
+            {messageList.map((msg, i) => {
+              // 检查是否是图片消息
+              if (msg.startsWith('[图片]')) {
+                const parts = msg.split(': ');
+                const senderInfo = parts[0].replace('[图片] ', '');
+                const imageUrl = parts.slice(1).join(': ');
+                
+                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>
+                );
+              }
+              
+              // 普通文本消息
+              return (
+                <div key={i} className="text-sm mb-1">{msg}</div>
+              );
+            })}
           </div>
         </div>
 
@@ -161,6 +194,18 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
                 </Button>
               )}
 
+              {/* 图片选择按钮 */}
+              <Button
+                type="button"
+                onClick={() => setShowImageSelector(true)}
+                className="p-2 rounded-full bg-blue-500 text-white"
+                title="选择图片"
+                size="icon"
+                variant="ghost"
+              >
+                <PhotoIcon className="w-4 h-4" />
+              </Button>
+  
               {/* 角色特定按钮 */}
               {role === Role.Teacher && (
                 <>
@@ -171,7 +216,7 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
                   <AllMuteToggleButton />
                 </>
               )}
-
+  
               {role === Role.Student && (
                 <>
                   <StudentHandUpButton />
@@ -236,6 +281,45 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
           </div>
         </div>
       </div>
+
+      {/* 图片选择器对话框 */}
+      <Dialog open={showImageSelector} onOpenChange={setShowImageSelector}>
+        <DialogContent className="max-w-md">
+          <DialogHeader>
+            <DialogTitle>选择图片</DialogTitle>
+            <DialogDescription>
+              选择要发送的图片文件
+            </DialogDescription>
+          </DialogHeader>
+          
+          <FileSelector
+            filterType="image"
+            allowMultiple={false}
+            showPreview={true}
+            placeholder="选择图片"
+            title="选择图片"
+            description="选择要发送的图片文件"
+            onChange={(fileId) => {
+              if (fileId) {
+                // 使用文件ID发送图片消息
+                sendImageMessageById(fileId as number);
+                setShowImageSelector(false);
+              }
+            }}
+          />
+
+          <DialogFooter>
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => setShowImageSelector(false)}
+            >
+              取消
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
     </div>
   );
 };

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

@@ -6,7 +6,7 @@ import { useParams } from 'react-router';
 import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack } from 'aliyun-rtc-sdk';
 import { toast } from 'sonner';
 import { User } from '../../hooks/AuthProvider';
-import { aliyunClient } from '@/client/api';
+import { aliyunClient, fileClient } from '@/client/api';
 import { UserType } from '@/server/modules/users/user.enum';
 export enum Role {
   Teacher = UserType.TEACHER,
@@ -110,6 +110,10 @@ export const useClassroom = ({ user }:{ user : User }) => {
     setMessageList((prevMessageList) => [...prevMessageList, text])
   };
 
+  const showImageMessage = (imageUrl: string, senderName: string): void => {
+    setMessageList((prevMessageList) => [...prevMessageList, `[图片] ${senderName}: ${imageUrl}`])
+  };
+
   const showToast = (type: 'info' | 'success' | 'error', message: string): void => {
     if (type === 'info') {
       toast.info(message);
@@ -327,9 +331,32 @@ export const useClassroom = ({ user }:{ user : User }) => {
         }
       } else if (msg.type === 88888) { // 普通文本消息
         const sender = msg.sender;
-        const userExtension = JSON.parse(sender?.userExtension || '{}') as User;
-        const senderName = userExtension.nickname || userExtension.username;
-        showMessage(`${ senderName || '未知用户' }: ${msg.data}`);
+        // 使用正确的 ImUser 类型处理发送者信息
+        const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
+        const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
+        showMessage(`${senderName}: ${msg.data}`);
+      } else if (msg.type === 88895) { // 图片消息
+        try {
+          const data = JSON.parse(msg.data);
+          if (data.type === 'image' && data.fileId) {
+            const sender = msg.sender;
+            // 使用正确的 ImUser 类型处理发送者信息
+            const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
+            const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
+            
+            // 收到文件ID后获取文件详情
+            const response = await fileClient[':id']['$get']({ param: { id: data.fileId.toString() } });
+            if (response.status === 200) {
+              const fileInfo = await response.json();
+              showImageMessage(fileInfo.fullUrl, senderName);
+            } else {
+              showMessage(`${senderName}: [图片] (获取失败)`);
+            }
+          }
+        } catch (err) {
+          console.error('解析图片消息失败', err);
+          showMessage('收到一张图片(解析失败)');
+        }
       }
     });
   };
@@ -671,6 +698,42 @@ export const useClassroom = ({ user }:{ user : User }) => {
     }
   };
 
+  const sendImageMessageById = async (fileId: number): Promise<void> => {
+    if (!imMessageManager.current || !classId) return;
+
+    try {
+      await imMessageManager.current!.sendGroupMessage({
+        groupId: classId,
+        data: JSON.stringify({
+          type: 'image',
+          fileId: fileId
+        }),
+        type: 88895, // 使用单一消息类型用于图片
+        level: NORMAL,
+      });
+
+      // 本地显示图片消息(发送方也需要获取文件详情)
+      const response = await fileClient[':id']['$get']({ param: { id: fileId.toString() } });
+      if (response.status !== 200) {
+        throw new Error('获取文件详情失败');
+      }
+      
+      const fileInfo = await response.json();
+      // 发送方显示自己的消息,使用当前用户信息
+      const senderName = user?.nickname || user?.username || '我';
+      showImageMessage(fileInfo.fullUrl, senderName);
+      
+    } catch (err: any) {
+      // 检查是否为禁言错误 (错误码442)
+      if (err.message?.includes('code:442') || err.message?.includes('not allowed to send message')) {
+        showToast('error', '您已被禁言,无法发送图片');
+      } else {
+        showToast('error', `图片发送失败: ${err.message}`);
+      }
+    }
+  };
+
+
   const startClass = async (): Promise<void> => {
     if (!imMessageManager.current || !classId || role !== Role.Teacher) return;
     
@@ -1262,6 +1325,8 @@ export const useClassroom = ({ user }:{ user : User }) => {
     checkRtcMuteStatus,
     checkRtcMuteStatusBatch,
     getAllRtcMuteStatus,
+    sendImageMessageById,
+    showToast,
 
     // 统计信息
     onlineCount,