import React, { useState, useEffect, useRef, createContext, useContext } from 'react'; import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack, AliRtcSdkChannelProfile } from 'aliyun-rtc-sdk'; import { ToastContainer, toast } from 'react-toastify'; // 从 SDK 中提取需要的类型 type ImEngine = InstanceType; type ImGroupManager = AliVCInteraction.AliVCIMGroupManager; type ImMessageManager = AliVCInteraction.AliVCIMMessageManager; type ImLogLevel = AliVCInteraction.ImLogLevel; type ImMessageLevel = AliVCInteraction.ImMessageLevel; interface ImUser { userId: string; userExtension?: string; } interface ImGroupMessage { groupId: string; type: number; data: string; sender?: ImUser; timestamp?: number; } enum Role { Teacher = 'admin', Student = 'student' } // 课堂状态枚举 enum ClassStatus { NOT_STARTED = 'not_started', IN_PROGRESS = 'in_progress', ENDED = 'ended' } // 课堂上下文类型 // 互动消息类型 type InteractionAction = 'hand_up' | 'cancel_hand_up' | 'answer_hand_up'; // 互动消息基础接口 interface InteractionMessage { action: InteractionAction; studentId: string; studentName?: string; timestamp?: number; question?: string; } // 举手请求类型 interface HandUpRequest extends InteractionMessage { timestamp: number; } type ClassroomContextType = { userId: string; role: Role; isLoggedIn: boolean; isJoinedClass: boolean; messageList: string[]; errorMessage: string; classStatus: ClassStatus; handUpList: HandUpRequest[]; // 举手列表 questions: string[]; // 问题列表 setRole: (role: Role) => void; createClass: (className: string, maxMembers?: number) => Promise; // 创建课堂 startClass: () => Promise; endClass: () => Promise; toggleMuteMember: (userId: string, mute: boolean) => Promise; handUp: (question?: string) => Promise; // 学生举手 answerHandUp: (studentId: string) => Promise; // 老师应答 sendQuestion: (question: string) => Promise; // 发送问题 }; const ClassroomContext = createContext(null); // 辅助函数 function hex(buffer: ArrayBuffer): string { const hexCodes = []; const view = new DataView(buffer); for (let i = 0; i < view.byteLength; i += 4) { const value = view.getUint32(i); const stringValue = value.toString(16); const padding = '00000000'; const paddedValue = (padding + stringValue).slice(-padding.length); hexCodes.push(paddedValue); } return hexCodes.join(''); } async function generateToken( appId: string, appKey: string, channelId: string, userId: string, timestamp: number ): Promise { const encoder = new TextEncoder(); const data = encoder.encode(`${appId}${appKey}${channelId}${userId}${timestamp}`); const hash = await crypto.subtle.digest('SHA-256', data); return hex(hash); } function showToast(type: 'info' | 'success' | 'error', message: string): void { switch(type) { case 'info': toast.info(message); break; case 'success': toast.success(message); break; case 'error': toast.error(message); break; } } // 从SDK获取枚举值 const { ImLogLevel, ImMessageLevel } = window.AliVCInteraction; // 配置信息 const IM_APP_ID = '4c2ab5e1b1b0'; const IM_APP_KEY = '314bb5eee5b623549e8a41574ba3ff32'; const IM_APP_SIGN = 'H4sIAAAAAAAE/wCQAG//zguHB+lYCilkv7diSkk4GmcvLuds+InRu9vFOFebMwm/jEgsK5bBT85Z0owObMxG58uXHyPFlPEBEDQm9FswNJ+KmX0VDYkcfdPPWkafA6Hc0B6F+p5De9yJfPEfHzwo/DHMaygbHfLmBgUtmKveq421sJr/gNBz9D04Ewsg39us+ao0NegzLt7xtXvFXXXJAAAA//8BAAD//yoav6aQAAAA'; const RTC_APP_ID = 'a5842c2a-d94a-43be-81de-1fdb712476e1'; const RTC_APP_KEY = 'b71d65f4f84c450f6f058f4ad507bd42'; // IM Token生成 async function generateImToken(userId: string, role: string): Promise { const nonce = 'AK_4'; const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3; const pendingShaStr = `${IM_APP_ID}${IM_APP_KEY}${userId}${nonce}${timestamp}${role}`; const encoder = new TextEncoder(); const data = encoder.encode(pendingShaStr); const hash = await crypto.subtle.digest('SHA-256', data); return hex(hash); } export const ClassroomPage = () => { // 解析URL参数 useEffect(() => { const queryParams = new URLSearchParams(window.location.search); const urlClassId = queryParams.get('classId'); if (urlClassId) { setClassId(urlClassId); showMessage(`从分享链接获取课堂ID: ${urlClassId}`); } }, []); // 状态管理 const [userId, setUserId] = useState(''); const [isCameraOn, setIsCameraOn] = useState(false); const [isAudioOn, setIsAudioOn] = useState(false); const [isScreenSharing, setIsScreenSharing] = useState(false); const [className, setClassName] = useState(''); const [role, setRole] = useState(Role.Student); const [classId, setClassId] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const [isJoinedClass, setIsJoinedClass] = useState(false); const [msgText, setMsgText] = useState(''); const [messageList, setMessageList] = useState([]); const [errorMessage, setErrorMessage] = useState(''); const [classStatus, setClassStatus] = useState(ClassStatus.NOT_STARTED); const [handUpList, setHandUpList] = useState([]); const [questions, setQuestions] = useState([]); const [students, setStudents] = useState>([]); const [shareLink, setShareLink] = useState(''); // SDK实例 const imEngine = useRef(null); const imGroupManager = useRef(null); const imMessageManager = useRef(null); const aliRtcEngine = useRef(null); const remoteVideoElMap = useRef>({}); const remoteVideoContainer = useRef(null); // 消息管理模块 const showMessage = (text: string): void => { setMessageList((prevMessageList) => [...prevMessageList, text]) }; const listenImEvents = (): void => { if (!imEngine.current) return; imEngine.current.on('connectsuccess', () => { showMessage('IM连接成功'); }); imEngine.current.on('disconnect', async (code: number) => { showMessage(`IM断开连接: ${code}`); // 自动重连 try { const imToken = await generateImToken(userId, role); await imEngine.current!.login({ user: { userId, userExtension: '{}' }, userAuth: { nonce: 'AK_4', timestamp: Math.floor(Date.now() / 1000) + 3600 * 3, token: imToken, role } }); showMessage('IM自动重连成功'); } catch (err: unknown) { const error = err as Error; showMessage(`IM自动重连失败: ${error.message}`); } }); }; const listenGroupEvents = (): void => { if (!imGroupManager.current) return; imGroupManager.current.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => { showMessage(`成员变更: 加入${joinUsers.length}人, 离开${leaveUsers.length}人`); }); }; const listenMessageEvents = (): void => { if (!imMessageManager.current) return; imMessageManager.current.on('recvgroupmessage', (msg: AliVCInteraction.ImMessage, groupId: string) => { if (msg.type === 88889) { // 课堂状态消息 try { const data = JSON.parse(msg.data); if (data.action === 'start_class') { setClassStatus(ClassStatus.IN_PROGRESS); showMessage('老师已开始上课'); } else if (data.action === 'end_class') { setClassStatus(ClassStatus.ENDED); showMessage('老师已结束上课'); } } catch (err) { console.error('解析课堂状态消息失败', err); } } else if (msg.type === 88890) { // 静音指令 try { const data = JSON.parse(msg.data); if (data.action === 'toggle_mute' && data.userId === userId) { showMessage(data.mute ? '你已被老师静音' : '老师已取消你的静音'); } } catch (err) { console.error('解析静音指令失败', err); } } else if (msg.type === 88891) { // 举手消息 try { const data = JSON.parse(msg.data) as InteractionMessage; if (data.action === 'hand_up') { const handUpData: HandUpRequest = { ...data, timestamp: data.timestamp || Date.now() }; setHandUpList([...handUpList, handUpData]); showMessage(`${data.studentName || data.studentId} 举手了`); } else if (data.action === 'cancel_hand_up') { setHandUpList(handUpList.filter(h => h.studentId !== data.studentId)); } } catch (err) { console.error('解析举手消息失败', err); } } else if (msg.type === 88892) { // 问题消息 try { const data = JSON.parse(msg.data) as {question: string}; setQuestions([...questions, data.question]); showMessage(`收到问题: ${data.question}`); } catch (err) { console.error('解析问题消息失败', err); } } else if (msg.type === 88893) { // 应答消息 try { const data = JSON.parse(msg.data) as InteractionMessage; if (data.action === 'answer_hand_up' && data.studentId === userId) { showMessage('老师已应答你的举手'); setHandUpList(handUpList.filter(h => h.studentId !== data.studentId)); } } catch (err) { console.error('解析应答消息失败', err); } } else if (msg.type === 88888) { // 普通文本消息 showMessage(`${msg.sender?.userId || '未知用户'}: ${msg.data}`); } }); }; // 音视频模块 const removeRemoteVideo = (userId: string, type: 'camera' | 'screen' = 'camera') => { const vid = `${type}_${userId}`; const el = remoteVideoElMap.current[vid]; if (el) { aliRtcEngine.current!.setRemoteViewConfig(null, userId, type === 'camera' ? AliRtcVideoTrack.AliRtcVideoTrackCamera : AliRtcVideoTrack.AliRtcVideoTrackScreen); el.pause(); remoteVideoContainer.current?.removeChild(el); delete remoteVideoElMap.current[vid]; } }; const listenRtcEvents = () => { if (!aliRtcEngine.current) return; showMessage('注册rtc事件监听') aliRtcEngine.current.on('remoteUserOnLineNotify', (userId: string) => { showMessage(`用户 ${userId} 加入课堂`); console.log('用户上线通知:', userId); }); aliRtcEngine.current.on('remoteUserOffLineNotify', (userId: string) => { showMessage(`用户 ${userId} 离开课堂`); console.log('用户下线通知:', userId); removeRemoteVideo(userId, 'camera'); removeRemoteVideo(userId, 'screen'); }); // 订阅所有用户视频流 aliRtcEngine.current.on('videoSubscribeStateChanged', ( userId: string, oldState: AliRtcSubscribeState, newState: AliRtcSubscribeState, interval: number, channelId: string ) => { console.log(`视频订阅状态变化: 用户 ${userId}, 旧状态 ${oldState}, 新状态 ${newState}`); switch(newState) { case 3: // 订阅成功 try { console.log('开始创建远程视频元素'); // 检查是否已有该用户的视频元素 if (remoteVideoElMap.current[`camera_${userId}`]) { console.log(`用户 ${userId} 的视频元素已存在`); return; } const video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.className = 'w-80 h-45 mr-2 mb-2 bg-black'; if (!remoteVideoContainer.current) { console.error('远程视频容器未找到'); return; } // 确保容器可见 remoteVideoContainer.current.style.display = 'block'; remoteVideoContainer.current.appendChild(video); remoteVideoElMap.current[`camera_${userId}`] = video; // 设置远程视图配置 aliRtcEngine.current!.setRemoteViewConfig( video, userId, AliRtcVideoTrack.AliRtcVideoTrackCamera ); console.log(`已订阅用户 ${userId} 的视频流`); showMessage(`已显示用户 ${userId} 的视频`); } catch (err) { console.error(`订阅用户 ${userId} 视频流失败:`, err); showMessage(`订阅用户 ${userId} 视频流失败`); } break; case 1: // 取消订阅 console.log(`取消订阅用户 ${userId} 的视频流`); removeRemoteVideo(userId, 'camera'); break; case 2: // 订阅中 console.log(`正在订阅用户 ${userId} 的视频流...`); break; default: console.warn(`未知订阅状态: ${newState}`); } }); // 订阅屏幕分享流状态变化 aliRtcEngine.current.on('screenShareSubscribeStateChanged', ( userId: string, oldState: AliRtcSubscribeState, newState: AliRtcSubscribeState, elapseSinceLastState: number, channel: string ) => { console.log(`屏幕分享订阅状态变更:uid=${userId}, oldState=${oldState}, newState=${newState}`); switch(newState) { case 3: // 订阅成功 try { console.log('开始创建屏幕分享视频元素'); // 检查是否已有该用户的屏幕分享元素 if (remoteVideoElMap.current[`screen_${userId}`]) { console.log(`用户 ${userId} 的屏幕分享元素已存在`); return; } const video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.className = 'w-80 h-45 mr-2 mb-2 bg-black'; if (!remoteVideoContainer.current) { console.error('远程视频容器未找到'); return; } remoteVideoContainer.current.appendChild(video); remoteVideoElMap.current[`screen_${userId}`] = video; // 设置远程视图配置 aliRtcEngine.current!.setRemoteViewConfig( video, userId, AliRtcVideoTrack.AliRtcVideoTrackScreen ); console.log(`已订阅用户 ${userId} 的屏幕分享流`); showMessage(`已显示用户 ${userId} 的屏幕分享`); } catch (err) { console.error(`订阅用户 ${userId} 屏幕分享流失败:`, err); showMessage(`订阅用户 ${userId} 屏幕分享流失败`); } break; case 1: // 取消订阅 console.log(`取消订阅用户 ${userId} 的屏幕分享流`); removeRemoteVideo(userId, 'screen'); break; case 2: // 订阅中 console.log(`正在订阅用户 ${userId} 的屏幕分享流...`); break; default: console.warn(`未知屏幕分享订阅状态: ${newState}`); } }); }; // 获取学生列表 const fetchStudents = async (classId: string) => { try { if (!imEngine.current) { throw new Error('IM引擎未初始化'); } const groupManager = imEngine.current.getGroupManager(); if (!groupManager) { throw new Error('IM群组管理器未初始化'); } // 使用classId作为群组ID获取成员 const response = await groupManager.listRecentGroupUser(classId); // 转换IM用户数据格式 const students = response.userList.map((user: ImUser) => ({ id: user.userId, name: user.userExtension || `用户${user.userId}` })); setStudents(students); } catch (err) { console.error('从IM获取学生列表失败:', err); // 可选: 显示错误提示给用户 // setError('获取学生列表失败,请稍后重试'); } }; // 统一登录逻辑 const login = async (userId: string): Promise => { try { // 初始化IM const { ImEngine: ImEngineClass } = window.AliVCInteraction; imEngine.current = ImEngineClass.createEngine(); await imEngine.current.init({ deviceId: 'xxxx', appId: IM_APP_ID, appSign: IM_APP_SIGN, logLevel: ImLogLevel.ERROR, }); // 登录IM const imToken = await generateImToken(userId, role); await imEngine.current.login({ user: { userId, userExtension: '{}' }, userAuth: { nonce: 'AK_4', timestamp: Math.floor(Date.now() / 1000) + 3600 * 3, token: imToken, role } }); // 初始化RTC aliRtcEngine.current = AliRtcEngine.getInstance(); AliRtcEngine.setLogLevel(0); // 设置事件监听 listenImEvents(); listenRtcEvents(); setIsLoggedIn(true); setErrorMessage(''); showToast('success', '登录成功'); // 登录成功,不生成分享链接(将在课堂创建成功后生成) } catch (err: any) { setErrorMessage(`登录失败: ${err.message}`); showToast('error', '登录失败'); } }; // 加入课堂 const joinClass = async (classId: string): Promise => { if (!imEngine.current || !aliRtcEngine.current) return; if (!classId) { setErrorMessage('课堂ID不能为空'); showToast('error', '请输入有效的课堂ID'); return; } try { // 加入IM群组 const gm = imEngine.current.getGroupManager(); const mm = imEngine.current.getMessageManager(); imGroupManager.current = gm || null; imMessageManager.current = mm || null; await gm!.joinGroup(classId); listenGroupEvents(); listenMessageEvents(); // 加入RTC频道 await joinRtcChannel(classId); setIsJoinedClass(true); setErrorMessage(''); showToast('success', '加入课堂成功'); } catch (err: any) { setErrorMessage(`加入课堂失败: ${err.message}`); showToast('error', '加入课堂失败'); // 如果IM加入成功但RTC加入失败,需要离开IM群组 if (imGroupManager.current) { try { await imGroupManager.current.leaveGroup(classId); } catch (leaveErr) { console.error('离开IM群组失败:', leaveErr); } } } }; // 离开课堂 const leaveClass = async (): Promise => { try { if (imGroupManager.current && classId) { await imGroupManager.current.leaveGroup(classId); } if (aliRtcEngine.current) { await leaveRtcChannel(); } setIsJoinedClass(false); showToast('info', '已离开课堂'); } catch (err) { console.error('离开课堂失败:', err); showToast('error', '离开课堂时发生错误'); } }; // 发送消息 const sendMessage = async (): Promise => { if (!imMessageManager.current || !classId) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: msgText, type: 88888, level: ImMessageLevel.NORMAL, }); setMsgText(''); setErrorMessage(''); } catch (err: any) { setErrorMessage(`消息发送失败: ${err.message}`); } }; // 开始上课 const startClass = async (): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { // 发送开始上课消息 await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'start_class' }), type: 88889, // 自定义消息类型 level: ImMessageLevel.HIGH, }); setClassStatus(ClassStatus.IN_PROGRESS); showToast('success', '课堂已开始'); } catch (err: any) { setErrorMessage(`开始上课失败: ${err.message}`); } }; // 结束上课 const endClass = async (): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'end_class' }), type: 88889, // 自定义消息类型 level: ImMessageLevel.HIGH, }); setClassStatus(ClassStatus.ENDED); showToast('success', '课堂已结束'); // 离开RTC频道 try { await leaveRtcChannel(); } catch (err: any) { console.error('离开RTC频道失败:', err); showToast('error', '离开RTC频道失败'); } } catch (err: any) { setErrorMessage(`结束上课失败: ${err.message}`); } }; // 静音/取消静音成员 const toggleMuteMember = async (userId: string, mute: boolean): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'toggle_mute', userId, mute }), type: 88890, // 自定义消息类型 level: ImMessageLevel.HIGH, }); showToast('info', mute ? `已静音用户 ${userId}` : `已取消静音用户 ${userId}`); } catch (err: any) { setErrorMessage(`操作失败: ${err.message}`); } }; // 创建课堂 const createClass = async (className: string, maxMembers = 200): Promise => { if (!imEngine.current || !isLoggedIn || role !== Role.Teacher) { showToast('error', '只有老师可以创建课堂'); return null; } try { const groupManager = imEngine.current.getGroupManager(); if (!groupManager) { throw new Error('群组管理器未初始化'); } // 显示创建中状态 showToast('info', '正在创建课堂...'); // 调用IM SDK创建群组 const response = await groupManager.createGroup({ groupName: className, groupMeta: JSON.stringify({ classType: 'interactive', creator: userId, createdAt: Date.now(), maxMembers }) }); if (!response?.groupId) { throw new Error('创建群组失败: 未返回群组ID'); } // 创建成功后自动加入群组 try { await groupManager.joinGroup(response.groupId); showToast('success', '课堂创建并加入成功'); showMessage(`课堂 ${className} 创建成功,ID: ${response.groupId}`); // 更新状态 setClassId(response.groupId); setIsJoinedClass(true); // 初始化群组消息管理器 const messageManager = imEngine.current.getMessageManager(); if (messageManager) { imMessageManager.current = messageManager; listenMessageEvents(); } // 加入RTC频道 await joinRtcChannel(response.groupId); // 记录创建时间 const createTime = new Date(); showMessage(`创建时间: ${createTime.toLocaleString()}`); // 创建成功后生成分享链接 setShareLink(`${window.location.href.split('?')[0]}?classId=${response.groupId}`); return response.groupId; } catch (joinErr: any) { throw new Error(`创建成功但加入失败: ${joinErr.message}`); } } catch (err: any) { const errorMsg = err.message.includes('alreadyExist') ? '课堂已存在' : `课堂创建失败: ${err.message}`; setErrorMessage(errorMsg); showToast('error', errorMsg); return null; } }; // 加入RTC频道 const joinRtcChannel = async (classId: string, publishOptions?: { publishVideo?: boolean publishAudio?: boolean publishScreen?: boolean }) => { if (!aliRtcEngine.current) return; const { publishVideo = false, publishAudio = false, publishScreen = false, } = publishOptions || {}; const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3; const token = await generateToken(RTC_APP_ID, RTC_APP_KEY, classId, userId, timestamp); await aliRtcEngine.current.publishLocalVideoStream(publishVideo); await aliRtcEngine.current.publishLocalAudioStream(publishAudio); await aliRtcEngine.current.publishLocalScreenShareStream(publishScreen); await aliRtcEngine.current.joinChannel( { channelId: classId, userId, appId: RTC_APP_ID, token, timestamp, }, userId ); // showToast('info', '已加入RTC频道'); }; // 离开RTC频道 const leaveRtcChannel = async () => { if (!aliRtcEngine.current) return; await aliRtcEngine.current.leaveChannel(); // showToast('info', '已离开RTC频道'); }; // 切换摄像头状态 const toggleCamera = async () => { try { if (isCameraOn) { await leaveRtcChannel(); await joinRtcChannel(classId, { publishVideo: false, publishAudio: isAudioOn, publishScreen: isScreenSharing }); await aliRtcEngine.current?.stopPreview(); } else { await leaveRtcChannel(); await joinRtcChannel(classId, { publishVideo: true, publishAudio: isAudioOn, publishScreen: isScreenSharing }); await aliRtcEngine.current?.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera); await aliRtcEngine.current?.startPreview(); } setIsCameraOn(!isCameraOn); } catch (err) { console.error('切换摄像头状态失败:', err); showToast('error', '切换摄像头失败'); } }; // 切换音频状态 const toggleAudio = async () => { try { await leaveRtcChannel(); await joinRtcChannel(classId, { publishVideo: isCameraOn, publishAudio: !isAudioOn, publishScreen: isScreenSharing }); setIsAudioOn(!isAudioOn); } catch (err) { console.error('切换麦克风状态失败:', err); showToast('error', '切换麦克风失败'); } }; // 切换屏幕分享状态 const toggleScreenShare = async () => { try { if (isScreenSharing) { await leaveRtcChannel(); await joinRtcChannel(classId, { publishVideo: isCameraOn, publishAudio: isAudioOn, publishScreen: false }); await aliRtcEngine.current?.stopPreviewScreen(); } else { await leaveRtcChannel(); // 设置屏幕分享预览视图 await joinRtcChannel(classId, { publishVideo: isCameraOn, publishAudio: isAudioOn, publishScreen: true }); await aliRtcEngine.current?.setLocalViewConfig( 'screenPreviewer', AliRtcVideoTrack.AliRtcVideoTrackScreen ); // await aliRtcEngine.current?.startPreviewScreen({ // audio: isAudioOn, // 根据音频状态决定是否共享音频 // videoTrack: undefined, // 使用默认视频轨道 // audioTrack: undefined // 使用默认音频轨道 // }); await aliRtcEngine.current?.startPreviewScreen() } setIsScreenSharing(!isScreenSharing); } catch (err) { console.error('切换屏幕分享失败:', err); showToast('error', '切换屏幕分享失败'); } }; // 清理资源 useEffect(() => { return () => { if (imGroupManager.current) { imGroupManager.current.removeAllListeners(); } if (imMessageManager.current) { imMessageManager.current.removeAllListeners(); } if (imEngine.current) { imEngine.current.removeAllListeners(); } if (aliRtcEngine.current) { aliRtcEngine.current.destroy(); } }; }, []); // 学生举手 const handUp = async (question?: string): Promise => { if (!imMessageManager.current || !classId || role !== 'student') return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'hand_up', studentId: userId, timestamp: Date.now(), question }), type: 88891, level: ImMessageLevel.NORMAL, }); } catch (err: any) { setErrorMessage(`举手失败: ${err.message}`); } }; // 老师应答举手 const answerHandUp = async (studentId: string): Promise => { if (!imMessageManager.current || !classId || role !== Role.Teacher) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: JSON.stringify({ action: 'answer_hand_up', studentId }), type: 88893, level: ImMessageLevel.HIGH, }); } catch (err: any) { setErrorMessage(`应答失败: ${err.message}`); } }; // 发送问题 const sendQuestion = async (question: string): Promise => { if (!imMessageManager.current || !classId) return; try { await imMessageManager.current.sendGroupMessage({ groupId: classId, data: question, type: 88892, level: ImMessageLevel.NORMAL, }); } catch (err: any) { setErrorMessage(`问题发送失败: ${err.message}`); } }; return ( setRole(role as Role), createClass, startClass, endClass, toggleMuteMember, handUp, answerHandUp, sendQuestion }}>

互动课堂

{shareLink && (

课堂分享链接

)}
{!isLoggedIn && (
setClassName(e.target.value)} placeholder="输入课堂名称" />
)}
setUserId(e.target.value)} />
setClassId(e.target.value)} />
{!isLoggedIn && ( )} {isLoggedIn && role === Role.Teacher && ( )}
setMsgText(e.target.value)} />
{role === 'student' && isJoinedClass && (

互动功能

)} {role === Role.Teacher && handUpList.length > 0 && (

举手列表 ({handUpList.length})

{handUpList.map((req, i) => (
{req.studentName || req.studentId}
{req.question &&
{req.question}
}
))}
)} {questions.length > 0 && (

问题列表 ({questions.length})

{questions.map((q, i) => (
问题 {i + 1}
{q}
))}
)}

消息记录

{messageList.map((msg, i) => (
{msg}
))}
{role === Role.Teacher && isJoinedClass && (

老师控制面板

成员管理
{students.map(student => (
{student.name}
))}
)}

本地视频

屏幕分享

远程视频

{errorMessage && (
{errorMessage}
)}
); };