|
|
@@ -58,6 +58,7 @@ type ClassroomContextType = {
|
|
|
handUpList: HandUpRequest[]; // 举手列表
|
|
|
questions: string[]; // 问题列表
|
|
|
setRole: (role: 'teacher' | 'student') => void;
|
|
|
+ createClass: (className: string, maxMembers?: number) => Promise<string | null>; // 创建课堂
|
|
|
startClass: () => Promise<void>;
|
|
|
endClass: () => Promise<void>;
|
|
|
toggleMuteMember: (userId: string, mute: boolean) => Promise<void>;
|
|
|
@@ -119,9 +120,21 @@ const IM_APP_SIGN = 'H4sIAAAAAAAE/wCQAG//zguHB+lYCilkv7diSkk4GmcvLuds+InRu9vFOFe
|
|
|
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 = () => {
|
|
|
// 状态管理
|
|
|
const [userId, setUserId] = useState<string>('');
|
|
|
+ const [className, setClassName] = useState<string>('');
|
|
|
const [role, setRole] = useState<'teacher' | 'student'>('student');
|
|
|
const [classId, setClassId] = useState<string>('');
|
|
|
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
|
|
@@ -153,8 +166,28 @@ export const ClassroomPage = () => {
|
|
|
showMessage('IM连接成功');
|
|
|
});
|
|
|
|
|
|
- imEngine.current.on('disconnect', (code: number) => {
|
|
|
+ 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}`);
|
|
|
+ }
|
|
|
});
|
|
|
};
|
|
|
|
|
|
@@ -268,6 +301,21 @@ export const ClassroomPage = () => {
|
|
|
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();
|
|
|
@@ -415,6 +463,73 @@ export const ClassroomPage = () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 创建课堂
|
|
|
+ const createClass = async (className: string, maxMembers = 200): Promise<string | null> => {
|
|
|
+ if (!imEngine.current || !isLoggedIn || 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录创建时间
|
|
|
+ const createTime = new Date();
|
|
|
+ showMessage(`创建时间: ${createTime.toLocaleString()}`);
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 清理资源
|
|
|
useEffect(() => {
|
|
|
return () => {
|
|
|
@@ -501,6 +616,7 @@ export const ClassroomPage = () => {
|
|
|
handUpList,
|
|
|
questions,
|
|
|
setRole,
|
|
|
+ createClass,
|
|
|
startClass,
|
|
|
endClass,
|
|
|
toggleMuteMember,
|
|
|
@@ -526,6 +642,17 @@ export const ClassroomPage = () => {
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
<div className="md:col-span-1">
|
|
|
<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
|
|
|
@@ -557,6 +684,21 @@ export const ClassroomPage = () => {
|
|
|
</div>
|
|
|
|
|
|
<div className="flex space-x-2 mb-2">
|
|
|
+ {!isLoggedIn && (
|
|
|
+ <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"
|