2
0
Эх сурвалжийг харах

支持/mobile/classroom/:id/:role路由格式
正确处理teacher/student角色参数

yourname 7 сар өмнө
parent
commit
be5e2f6406

+ 13 - 4
client/mobile/components/Classroom/ClassroomProvider.tsx

@@ -1,4 +1,5 @@
 import React, { useState, useEffect, useRef, createContext, useContext } from 'react';
+import { useParams } from 'react-router';
 import { useClassroom , Role } from './useClassroom.ts';
 
 type ClassroomContextType = ReturnType<typeof useClassroom>;
@@ -7,16 +8,24 @@ const ClassroomContext = createContext<ClassroomContextType | null>(null);
 
 export const ClassroomProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
   const classroom = useClassroom();
+  const { id: classId, role: pathRole } = useParams();
 
   useEffect(() => {
     // 解析URL参数中的classId
     const params = new URLSearchParams(window.location.search);
-    const classId = params.get('classId');
-    if (classId) {
-      classroom.setClassId(classId);
+    const queryClassId = params.get('classId');
+    const finalClassId = classId || queryClassId;
+    
+    if (finalClassId) {
+      classroom.setClassId(finalClassId);
+    }
+    
+    if (pathRole && ['teacher', 'student'].includes(pathRole)) {
+      classroom.setRole(pathRole === 'teacher' ? Role.Teacher : Role.Student);
+    } else if (queryClassId) {
       classroom.setRole(Role.Student);
     }
-  }, []);
+  }, [classId, pathRole]);
 
   return (
     <ClassroomContext.Provider value={classroom}>

+ 5 - 2
client/mobile/components/Classroom/useClassroom.ts

@@ -465,9 +465,12 @@ export const useClassroom = () => {
   const joinClass = async (classId?: string): Promise<void> => {
     if (!imEngine.current || !aliRtcEngine.current) return;
     
-    // 优先使用URL参数中的classId
-    const { id: pathClassId } = useParams();
+    // 优先使用URL参数中的classId和role
+    const { id: pathClassId, role: pathRole } = useParams();
     const finalClassId = (classId || pathClassId) as string;
+    if (pathRole && ['teacher', 'student'].includes(pathRole)) {
+      setRole(pathRole === 'teacher' ? Role.Teacher : Role.Student);
+    }
     
     if (!finalClassId) {
       setErrorMessage('课堂ID不能为空');

+ 6 - 1
client/mobile/mobile_app.tsx

@@ -254,7 +254,12 @@ const App = () => {
       errorElement: <ErrorPage />
     },
     {
-      path: '/mobile/classroom/:id',
+      path: '/mobile/classroom',
+      element: <ClassroomPage />,
+      errorElement: <ErrorPage />
+    },
+    {
+      path: '/mobile/classroom/:id/:role?',
       element: <ClassroomPage />,
       errorElement: <ErrorPage />
     },

+ 0 - 1292
client/mobile/pages_classroom.tsx

@@ -1,1292 +0,0 @@
-import React, { useState, useEffect, useRef, createContext, useContext } from 'react';
-// @ts-types="../share/aliyun-rtc-sdk.d.ts"
-import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack, AliRtcSdkChannelProfile } from 'aliyun-rtc-sdk';
-// import AliRtcEngine, { AliRtcSubscribeState, AliRtcVideoTrack, AliRtcSdkChannelProfile } from '../share/aliyun-rtc-sdk.js';
-import { ToastContainer, toast } from 'react-toastify';
-
-// 从 SDK 中提取需要的类型
-type ImEngine = InstanceType<typeof AliVCInteraction.ImEngine>;
-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<string | null>; // 创建课堂
-  startClass: () => Promise<void>;
-  endClass: () => Promise<void>;
-  toggleMuteMember: (userId: string, mute: boolean) => Promise<void>;
-  handUp: (question?: string) => Promise<void>; // 学生举手
-  answerHandUp: (studentId: string) => Promise<void>; // 老师应答
-  sendQuestion: (question: string) => Promise<void>; // 发送问题
-};
-
-const ClassroomContext = createContext<ClassroomContextType | null>(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<string> {
-  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<string> {
-    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<string>('');
-  const [isCameraOn, setIsCameraOn] = useState<boolean>(false);
-  const [isAudioOn, setIsAudioOn] = useState<boolean>(false);
-  const [isScreenSharing, setIsScreenSharing] = useState<boolean>(false);
-  const [className, setClassName] = useState<string>('');
-  const [role, setRole] = useState<Role>(Role.Student);
-  const [classId, setClassId] = useState<string>('');
-  const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
-  const [isJoinedClass, setIsJoinedClass] = useState<boolean>(false);
-  const [msgText, setMsgText] = useState<string>('');
-  const [messageList, setMessageList] = useState<string[]>([]);
-  const [errorMessage, setErrorMessage] = useState<string>('');
-  const [classStatus, setClassStatus] = useState<ClassStatus>(ClassStatus.NOT_STARTED);
-  const [handUpList, setHandUpList] = useState<HandUpRequest[]>([]);
-  const [questions, setQuestions] = useState<string[]>([]);
-  const [students, setStudents] = useState<Array<{id: string, name: string}>>([]);
-  const [shareLink, setShareLink] = useState<string>('');
-
-  // SDK实例
-  const imEngine = useRef<ImEngine | null>(null);
-  const imGroupManager = useRef<ImGroupManager | null>(null);
-  const imMessageManager = useRef<ImMessageManager | null>(null);
-  const aliRtcEngine = useRef<AliRtcEngine | null>(null);
-  const remoteVideoElMap = useRef<Record<string, HTMLVideoElement>>({});
-  const remoteVideoContainer = useRef<HTMLDivElement>(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<void> => {
-    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<void> => {
-    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<void> => {
-    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<void> => {
-    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<void> => {
-    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<void> => {
-    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<void> => {
-    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<string | null> => {
-    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 () => {
-    if(!aliRtcEngine.current?.isInCall){
-      showToast('error', '先加入课堂');
-      return;
-    }
-
-    try {
-      if (isCameraOn) {
-        await aliRtcEngine.current?.stopPreview();
-        await aliRtcEngine.current?.enableLocalVideo(false)
-        await aliRtcEngine.current?.publishLocalVideoStream(false)
-      } else {
-        await aliRtcEngine.current?.setLocalViewConfig('localPreviewer', AliRtcVideoTrack.AliRtcVideoTrackCamera);
-        await aliRtcEngine.current?.enableLocalVideo(true)
-        await aliRtcEngine.current?.startPreview();
-        await aliRtcEngine.current?.publishLocalVideoStream(true)
-   
-      }
-      await aliRtcEngine.current?.startAndPublishDefaultDevices()
-      setIsCameraOn(!isCameraOn);
-    } catch (err) {
-      console.error('切换摄像头状态失败:', err);
-      showToast('error', '切换摄像头失败');
-    }
-  };
-
-  // 切换音频状态
-  const toggleAudio = async () => {
-    if(!aliRtcEngine.current?.isInCall){
-      showToast('error', '先加入课堂');
-      return;
-    }
-
-    try {
-      if (isAudioOn) {
-        await aliRtcEngine.current?.stopAudioCapture()
-        await aliRtcEngine.current?.publishLocalAudioStream(false);
-      } else {
-        await aliRtcEngine.current?.publishLocalAudioStream(true);
-      }
-      await aliRtcEngine.current?.startAndPublishDefaultDevices();
-      setIsAudioOn(!isAudioOn);
-    } catch (err) {
-      console.error('切换麦克风状态失败:', err);
-      showToast('error', '切换麦克风失败');
-    }
-  };
-
-  // 切换屏幕分享状态
-  const toggleScreenShare = async () => {
-    if(!aliRtcEngine.current?.isInCall){
-      showToast('error', '先加入课堂');
-      return;
-    }
-
-    try {
-      if (isScreenSharing) {
-        await aliRtcEngine.current?.publishLocalScreenShareStream(false)
-        await aliRtcEngine.current?.stopScreenShare()
-      } else {
-        await aliRtcEngine.current?.publishLocalScreenShareStream(true)
-        await aliRtcEngine.current?.setLocalViewConfig(
-          'screenPreviewer',
-          AliRtcVideoTrack.AliRtcVideoTrackScreen
-        );
-      }
-      await aliRtcEngine.current?.startAndPublishDefaultDevices()
-      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<void> => {
-    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<void> => {
-    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<void> => {
-    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 (
-    <ClassroomContext.Provider value={{
-      userId,
-      role,
-      isLoggedIn,
-      isJoinedClass,
-      messageList,
-      errorMessage,
-      classStatus,
-      handUpList,
-      questions,
-      setRole: (role: Role) => setRole(role as Role),
-      createClass,
-      startClass,
-      endClass,
-      toggleMuteMember,
-      handUp,
-      answerHandUp,
-      sendQuestion
-    }}>
-      <div className="container mx-auto p-4">
-        <h1 className="text-2xl font-bold mb-4">互动课堂</h1>
-        
-        <ToastContainer
-          position="top-right"
-          autoClose={5000}
-          hideProgressBar={false}
-          newestOnTop={false}
-          closeOnClick
-          rtl={false}
-          pauseOnFocusLoss
-          draggable
-          pauseOnHover
-        />
-
-        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
-          <div className="md:col-span-1">
-            {shareLink && (
-              <div className="mb-4 p-4 bg-white rounded-md shadow">
-                <h4 className="text-lg font-medium mb-2">课堂分享链接</h4>
-                <div className="flex items-center">
-                  <input
-                    type="text"
-                    readOnly
-                    value={shareLink}
-                    className="flex-1 px-3 py-2 border border-gray-300 rounded-l-md"
-                  />
-                  <button
-                    type="button"
-                    className="px-3 py-2 bg-blue-600 text-white rounded-r-md"
-                    onClick={() => {
-                      navigator.clipboard.writeText(shareLink);
-                      showToast('info', '链接已复制');
-                    }}
-                  >
-                    复制
-                  </button>
-                </div>
-              </div>
-            )}
-            <form>
-              {!isLoggedIn && (
-                <div className="mb-2">
-                  <label className="block text-sm font-medium text-gray-700">课堂名称</label>
-                  <input
-                    className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
-                    value={className}
-                    onChange={(e) => setClassName(e.target.value)}
-                    placeholder="输入课堂名称"
-                  />
-                </div>
-              )}
-              <div className="mb-2">
-                <label className="block text-sm font-medium text-gray-700">用户ID</label>
-                <input
-                  className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
-                  value={userId}
-                  onChange={(e) => setUserId(e.target.value)}
-                />
-              </div>
-              
-              <div className="mb-2">
-                <label className="block text-sm font-medium text-gray-700">课堂ID</label>
-                <input
-                  className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
-                  value={classId}
-                  onChange={(e) => setClassId(e.target.value)}
-                />
-              </div>
-              
-              <div className="mb-2">
-                <label className="block text-sm font-medium text-gray-700">角色</label>
-                <select
-                  className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
-                  value={role}
-                  onChange={(e) => setRole(e.target.value as Role)}
-                >
-                  <option value={Role.Student}>学生</option>
-                  <option value={Role.Teacher}>老师</option>
-                </select>
-              </div>
-              
-              <div className="flex space-x-2 mb-2">
-                {!isLoggedIn && (
-                  <button
-                    type="button"
-                    className="px-3 py-2 bg-blue-600 text-white rounded-md"
-                    onClick={() => login(userId)}
-                  >
-                    登录
-                  </button>
-                )}
-                
-                {isLoggedIn && role === Role.Teacher && (
-                  <button
-                    type="button"
-                    className="px-3 py-2 bg-green-600 text-white rounded-md"
-                    disabled={!className}
-                    onClick={async () => {
-                      const classId = await createClass(className);
-                      if (classId) {
-                        setClassId(classId);
-                      }
-                    }}
-                  >
-                    创建课堂
-                  </button>
-                )}
-                <button
-                  type="button"
-                  className="px-3 py-2 bg-blue-600 text-white rounded-md"
-                  disabled={!isLoggedIn || isJoinedClass}
-                  onClick={() => joinClass(classId)}
-                >
-                  加入课堂
-                </button>
-                
-                <button
-                  type="button"
-                  className="px-3 py-2 bg-gray-600 text-white rounded-md"
-                  disabled={!isJoinedClass}
-                  onClick={leaveClass}
-                >
-                  离开课堂
-                </button>
-              </div>
-            </form>
-            
-            <div className="mt-4">
-              <label className="block text-sm font-medium text-gray-700">消息</label>
-              <input
-                className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
-                value={msgText}
-                onChange={(e) => setMsgText(e.target.value)}
-              />
-              <button
-                type="button"
-                className="mt-2 px-3 py-2 bg-blue-600 text-white rounded-md"
-                disabled={!isJoinedClass}
-                onClick={sendMessage}
-              >
-                发送
-              </button>
-            </div>
-            
-            {role === 'student' && isJoinedClass && (
-              <div className="mt-4 p-4 bg-white rounded-md shadow">
-                <h4 className="text-lg font-medium mb-2">互动功能</h4>
-                <div className="space-y-3">
-                  <button
-                    type="button"
-                    className="w-full px-3 py-2 bg-green-600 text-white rounded-md"
-                    onClick={() => handUp()}
-                  >
-                    举手
-                  </button>
-                  <div className="flex space-x-2">
-                    <input
-                      type="text"
-                      placeholder="输入问题..."
-                      className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
-                      id="questionInput"
-                    />
-                    <button
-                      type="button"
-                      className="px-3 py-2 bg-blue-600 text-white rounded-md"
-                      onClick={() => {
-                        const input = document.getElementById('questionInput') as HTMLInputElement;
-                        if (input.value) {
-                          sendQuestion(input.value);
-                          input.value = '';
-                        }
-                      }}
-                    >
-                      提问
-                    </button>
-                  </div>
-                </div>
-              </div>
-            )}
-            
-            {role === Role.Teacher && handUpList.length > 0 && (
-              <div className="mt-4 p-4 bg-white rounded-md shadow">
-                <h4 className="text-lg font-medium mb-2">举手列表 ({handUpList.length})</h4>
-                <div className="space-y-2">
-                  {handUpList.map((req, i) => (
-                    <div key={i} className="flex items-center justify-between p-2 border-b">
-                      <div>
-                        <div className="font-medium">{req.studentName || req.studentId}</div>
-                        {req.question && <div className="text-sm text-gray-500">{req.question}</div>}
-                      </div>
-                      <button
-                        type="button"
-                        className="px-2 py-1 bg-blue-600 text-white rounded text-sm"
-                        onClick={() => answerHandUp(req.studentId)}
-                      >
-                        应答
-                      </button>
-                    </div>
-                  ))}
-                </div>
-              </div>
-            )}
-            
-            {questions.length > 0 && (
-              <div className="mt-4 p-4 bg-white rounded-md shadow">
-                <h4 className="text-lg font-medium mb-2">问题列表 ({questions.length})</h4>
-                <div className="space-y-2">
-                  {questions.map((q, i) => (
-                    <div key={i} className="p-2 border-b">
-                      <div className="font-medium">问题 {i + 1}</div>
-                      <div className="text-gray-700">{q}</div>
-                    </div>
-                  ))}
-                </div>
-              </div>
-            )}
-          </div>
-          
-          <div className="md:col-span-1">
-            <h4 className="text-lg font-medium mb-2">消息记录</h4>
-            <div className="bg-gray-100 p-2 rounded-md h-64 overflow-y-auto">
-              {messageList.map((msg, i) => (
-                <div key={i} className="mb-1">{msg}</div>
-              ))}
-            </div>
-
-            {role === Role.Teacher && isJoinedClass && (
-              <div className="mt-4 p-4 bg-white rounded-md shadow">
-                <h4 className="text-lg font-medium mb-2">老师控制面板</h4>
-                <div className="flex space-x-2 mb-4">
-                  <button
-                    type="button"
-                    className="px-3 py-2 bg-green-600 text-white rounded-md"
-                    disabled={classStatus === ClassStatus.IN_PROGRESS}
-                    onClick={startClass}
-                  >
-                    开始上课
-                  </button>
-                  <button
-                    type="button"
-                    className="px-3 py-2 bg-red-600 text-white rounded-md"
-                    disabled={classStatus !== ClassStatus.IN_PROGRESS}
-                    onClick={endClass}
-                  >
-                    结束上课
-                  </button>
-                </div>
-                <div>
-                  <h5 className="font-medium mb-2">成员管理</h5>
-                  <div className="space-y-2">
-                    {students.map(student => (
-                      <div key={student.id} className="flex items-center justify-between">
-                        <span>{student.name}</span>
-                        <div className="space-x-2">
-                          <button
-                            type="button"
-                            className="px-2 py-1 bg-yellow-500 text-white rounded text-sm"
-                            onClick={() => toggleMuteMember(student.id, true)}
-                          >
-                            静音
-                          </button>
-                          <button
-                            type="button"
-                            className="px-2 py-1 bg-blue-500 text-white rounded text-sm"
-                            onClick={() => toggleMuteMember(student.id, false)}
-                          >
-                            取消静音
-                          </button>
-                        </div>
-                      </div>
-                    ))}
-                  </div>
-                </div>
-              </div>
-            )}
-          </div>
-          
-          <div className="md:col-span-1">
-            <div className="mb-4">
-              <h4 className="text-lg font-medium mb-2">本地视频</h4>
-              <div className="relative">
-                <video
-                  id="localPreviewer"
-                  muted
-                  className="w-full h-48 bg-black"
-                ></video>
-                <div className="absolute bottom-2 right-2 flex space-x-2">
-                  <button
-                    onClick={toggleCamera}
-                    className={`px-3 py-1 rounded-md ${isCameraOn ? 'bg-red-600' : 'bg-blue-600'} text-white`}
-                  >
-                    {isCameraOn ? '关闭摄像头' : '开启摄像头'}
-                  </button>
-                  <button
-                    onClick={toggleAudio}
-                    className={`px-3 py-1 rounded-md ${isAudioOn ? 'bg-red-600' : 'bg-blue-600'} text-white`}
-                  >
-                    {isAudioOn ? '关闭麦克风' : '开启麦克风'}
-                  </button>
-                </div>
-              </div>
-            </div>
-            
-            <div className="mb-4">
-              <h4 className="text-lg font-medium mb-2">屏幕分享</h4>
-              <div className="relative">
-                <video
-                  id="screenPreviewer"
-                  muted
-                  className="w-full h-48 bg-black"
-                ></video>
-                <div className="absolute bottom-2 right-2">
-                  <button
-                    onClick={toggleScreenShare}
-                    className={`px-3 py-1 rounded-md ${isScreenSharing ? 'bg-red-600' : 'bg-blue-600'} text-white`}
-                  >
-                    {isScreenSharing ? '停止分享' : '分享屏幕'}
-                  </button>
-                </div>
-              </div>
-            </div>
-
-            <div>
-              <h4 className="text-lg font-medium mb-2">远程视频</h4>
-              <div
-                id="remoteVideoContainer"
-                ref={remoteVideoContainer}
-                className="grid grid-cols-2 gap-2"
-              ></div>
-            </div>
-          </div>
-        </div>
-        
-        {errorMessage && (
-          <div className="mt-2 text-red-500">{errorMessage}</div>
-        )}
-      </div>
-    </ClassroomContext.Provider>
-  );
-};

+ 2 - 2
client/mobile/pages_classroom_new.tsx

@@ -64,14 +64,14 @@ const JoinClassSection = () => {
 };
 
 const CreateClassSection = () => {
-  const { classStatus, createClass, className, setClassName } = useClassroomContext();
+  const { classStatus, createClass, className, setClassName, role } = useClassroomContext();
   const navigate = useNavigate();
   
   const handleCreateClass = async () => {
     if (!className.trim()) return;
     const classId = await createClass(className);
     if (classId) {
-      navigate(`/mobile/classroom/${classId}`, { replace: true });
+      navigate(`/mobile/classroom/${classId}/${role === Role.Teacher ? 'teacher' : 'student'}`, { replace: true });
     }
   };