feat: 支持监控端进入并优化登录态验证
- 允许教师从加入页面选择进入监控端或课堂端 - 修改登录态验证逻辑,仅检查 Redis 中是否存在 token 而非严格相等,以支持同一账号多端登录 - 监控端默认不发布本地音视频轨道,但可通过配置动态启用 - 优化 WebSocket 鉴权失败处理,自动跳转登录页
This commit is contained in:
@ -85,7 +85,8 @@ export class AuthGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
const redisToken = redisObj?.Token || redisObj?.token;
|
const redisToken = redisObj?.Token || redisObj?.token;
|
||||||
if (redisToken !== token) {
|
// 因为会有同一个账号同平台登录多个(监控端和教师端使用同一个账号也都是web所以不做过多验证)
|
||||||
|
if (!redisToken) {
|
||||||
throw new UnauthorizedException('登录态已失效');
|
throw new UnauthorizedException('登录态已失效');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -136,7 +136,8 @@ export class MeetingAuthGuard implements CanActivate {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const redisToken = redisObj?.Token || redisObj?.token || '';
|
const redisToken = redisObj?.Token || redisObj?.token || '';
|
||||||
if (redisToken !== token) {
|
// 因为会有同一个账号同平台登录多个(监控端和教师端使用同一个账号也都是web所以不做过多验证)
|
||||||
|
if (!redisToken) {
|
||||||
throw new WsException('登录态已失效');
|
throw new WsException('登录态已失效');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,11 @@
|
|||||||
<el-space alignment="center" :size="25">
|
<el-space alignment="center" :size="25">
|
||||||
<app-button-full-screen></app-button-full-screen>
|
<app-button-full-screen></app-button-full-screen>
|
||||||
<app-button-back-page></app-button-back-page>
|
<app-button-back-page></app-button-back-page>
|
||||||
<button class="btn btn-primary" @click="handleJoinRoom">
|
<button v-if="role === 'teacher'" class="btn btn-monitoring" @click="handleJoinRoom('monitor')">
|
||||||
|
<icon-mdi-login class="btn-icon"></icon-mdi-login>
|
||||||
|
<span>进入监控</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" @click="handleJoinRoom('classroom')">
|
||||||
<icon-mdi-login class="btn-icon"></icon-mdi-login>
|
<icon-mdi-login class="btn-icon"></icon-mdi-login>
|
||||||
<span>进入课程</span>
|
<span>进入课程</span>
|
||||||
</button>
|
</button>
|
||||||
@ -418,7 +422,7 @@
|
|||||||
/**
|
/**
|
||||||
* 处理学生加入房间操作
|
* 处理学生加入房间操作
|
||||||
*/
|
*/
|
||||||
async function handleJoinRoom() {
|
async function handleJoinRoom(target: 'classroom' | 'monitor' = 'classroom') {
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -428,9 +432,10 @@
|
|||||||
// 触发表单验证
|
// 触发表单验证
|
||||||
|
|
||||||
console.log('即将跳转到 meeting-room 页面');
|
console.log('即将跳转到 meeting-room 页面');
|
||||||
const targetName = role.value === 'student' ? 'meeting-room-student' : 'meeting-room';
|
const targetPath =
|
||||||
|
role.value === 'student' ? '/meeting/meeting-room-student' : target === 'monitor' ? '/meeting/meeting-room-monitor' : '/meeting/meeting-room';
|
||||||
router.push({
|
router.push({
|
||||||
name: targetName,
|
path: targetPath,
|
||||||
query: {
|
query: {
|
||||||
homeworkId: homeworkId.value,
|
homeworkId: homeworkId.value,
|
||||||
name: courseRoomName.value,
|
name: courseRoomName.value,
|
||||||
@ -706,13 +711,21 @@
|
|||||||
box-shadow: 0 4px 12px #fccd6d;
|
box-shadow: 0 4px 12px #fccd6d;
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
box-shadow: 0 6px 15px #f07f70;
|
box-shadow: 0 6px 15px rgba(240, 127, 112, 0.5);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&.btn-monitoring {
|
||||||
cursor: not-allowed;
|
width: 220px;
|
||||||
opacity: 0.5;
|
margin-bottom: 10px;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(45deg, #bd62fd, #c7aaff);
|
||||||
|
box-shadow: 0 4px 12px #c7aaff;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 6px 15px rgba(189, 98, 253, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,11 @@
|
|||||||
/** 老师短 UID */
|
/** 老师短 UID */
|
||||||
const teacherUid = ref<number>(0);
|
const teacherUid = ref<number>(0);
|
||||||
|
|
||||||
|
/** 监控端是否发布本地音视频(默认不发布) */
|
||||||
|
const isPublishLocalTracks = ref<boolean>(false);
|
||||||
|
/** 是否禁用本地音视频轨道 */
|
||||||
|
const disableLocalTracks = computed(() => !isPublishLocalTracks.value);
|
||||||
|
|
||||||
const meeting = useAgoraMeeting(
|
const meeting = useAgoraMeeting(
|
||||||
courseRoomId,
|
courseRoomId,
|
||||||
userName,
|
userName,
|
||||||
@ -96,7 +101,7 @@
|
|||||||
Toast.error(error.message || '操作失败');
|
Toast.error(error.message || '操作失败');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ disableLocalTracks: true }
|
{ disableLocalTracks }
|
||||||
);
|
);
|
||||||
|
|
||||||
const socket = useWebSocket();
|
const socket = useWebSocket();
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import AgoraRTC, {
|
|||||||
type NetworkQuality,
|
type NetworkQuality,
|
||||||
type RemoteStreamType,
|
type RemoteStreamType,
|
||||||
} from 'agora-rtc-sdk-ng';
|
} from 'agora-rtc-sdk-ng';
|
||||||
import { type Ref, computed, nextTick, reactive, ref, shallowRef, watch, watchEffect } from 'vue';
|
import { type Ref, computed, nextTick, reactive, ref, shallowRef, unref, watch, watchEffect } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import type { LocalUser, MeetingEventCallbacks, NetworkQualityLevel, OnJoinRoomData } from './types';
|
import type { LocalUser, MeetingEventCallbacks, NetworkQualityLevel, OnJoinRoomData } from './types';
|
||||||
import { TokenManager } from './useTokenManager';
|
import { TokenManager } from './useTokenManager';
|
||||||
@ -27,7 +27,7 @@ import { TokenManager } from './useTokenManager';
|
|||||||
*/
|
*/
|
||||||
export interface AgoraMeetingOptions {
|
export interface AgoraMeetingOptions {
|
||||||
/** 是否禁用本地音视频轨道(用于监控端等纯观看场景) */
|
/** 是否禁用本地音视频轨道(用于监控端等纯观看场景) */
|
||||||
disableLocalTracks?: boolean;
|
disableLocalTracks?: Ref<boolean> | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +97,7 @@ export function useAgoraMeeting(
|
|||||||
/** 是否正在共享屏幕 */
|
/** 是否正在共享屏幕 */
|
||||||
const isScreenSharing = ref(false);
|
const isScreenSharing = ref(false);
|
||||||
/** 是否禁用本地音视频轨道 */
|
/** 是否禁用本地音视频轨道 */
|
||||||
const disableLocalTracks = Boolean(options?.disableLocalTracks);
|
const disableLocalTracks = computed(() => Boolean(unref(options?.disableLocalTracks ?? false)));
|
||||||
|
|
||||||
// 同步本地用户名称
|
// 同步本地用户名称
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@ -372,7 +372,7 @@ export function useAgoraMeeting(
|
|||||||
try {
|
try {
|
||||||
FullLoading.show('正在加入会议...');
|
FullLoading.show('正在加入会议...');
|
||||||
|
|
||||||
if (!disableLocalTracks) {
|
if (!disableLocalTracks.value) {
|
||||||
// 初始化本地麦克风
|
// 初始化本地麦克风
|
||||||
try {
|
try {
|
||||||
localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
|
localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
|
||||||
@ -424,7 +424,7 @@ export function useAgoraMeeting(
|
|||||||
tokenManagerRef.value.setCurrentToken(roomData.tokenInfo);
|
tokenManagerRef.value.setCurrentToken(roomData.tokenInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disableLocalTracks) {
|
if (!disableLocalTracks.value) {
|
||||||
// ✅ 根据恢复的状态决定是否发布轨道(如果被禁麦/禁视频,只发布另一个)
|
// ✅ 根据恢复的状态决定是否发布轨道(如果被禁麦/禁视频,只发布另一个)
|
||||||
const tracksToPublish: (ICameraVideoTrack | IMicrophoneAudioTrack)[] = [];
|
const tracksToPublish: (ICameraVideoTrack | IMicrophoneAudioTrack)[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { type ComputedRef, ref } from 'vue';
|
|||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import type { MeetingClientSocket, OnJoinRoomData, SendJoinRoomData, ServerToClientEvents } from './types';
|
import type { MeetingClientSocket, OnJoinRoomData, SendJoinRoomData, ServerToClientEvents } from './types';
|
||||||
import { getDefaultAuthHeaders } from '@/utils/axios/auth-headers';
|
import { getDefaultAuthHeaders } from '@/utils/axios/auth-headers';
|
||||||
|
import router from '@/router/router';
|
||||||
let socket: MeetingClientSocket | null = null;
|
let socket: MeetingClientSocket | null = null;
|
||||||
/** 鉴权失败回调集合 */
|
/** 鉴权失败回调集合 */
|
||||||
const authFailedHandlers = new Set<(err: Error) => void>();
|
const authFailedHandlers = new Set<(err: Error) => void>();
|
||||||
@ -56,14 +56,10 @@ export function useWebSocket(): MeetingClientSocket {
|
|||||||
console.error('[会议室] WebSocket 连接错误:', error);
|
console.error('[会议室] WebSocket 连接错误:', error);
|
||||||
const msg = String(error?.message || '');
|
const msg = String(error?.message || '');
|
||||||
if (msg === 'Unauthorized') {
|
if (msg === 'Unauthorized') {
|
||||||
authFailedHandlers.forEach((cb) => {
|
|
||||||
try {
|
|
||||||
cb(error as Error);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
socket?.disconnect();
|
socket?.disconnect();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
router.replace('/login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,15 +162,7 @@ export function useMeetingSocket(
|
|||||||
cleanupConnect();
|
cleanupConnect();
|
||||||
sendJoin();
|
sendJoin();
|
||||||
};
|
};
|
||||||
const onConnectError = (error: any) => {
|
|
||||||
const msg = String(error?.message || '');
|
|
||||||
cleanupConnect();
|
|
||||||
if (msg === 'Unauthorized') {
|
|
||||||
reject(new Error('连接鉴权失败'));
|
|
||||||
} else {
|
|
||||||
reject(new Error('连接服务器失败,请检查网络'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const connectTimer = setTimeout(() => {
|
const connectTimer = setTimeout(() => {
|
||||||
cleanupConnect();
|
cleanupConnect();
|
||||||
reject(new Error('连接服务器超时,请检查网络'));
|
reject(new Error('连接服务器超时,请检查网络'));
|
||||||
@ -183,11 +171,9 @@ export function useMeetingSocket(
|
|||||||
/** 清理连接事件监听器 */
|
/** 清理连接事件监听器 */
|
||||||
function cleanupConnect() {
|
function cleanupConnect() {
|
||||||
ws.off('connect', onConnect);
|
ws.off('connect', onConnect);
|
||||||
ws.off('connect_error', onConnectError);
|
|
||||||
clearTimeout(connectTimer);
|
clearTimeout(connectTimer);
|
||||||
}
|
}
|
||||||
ws.once('connect', onConnect);
|
ws.once('connect', onConnect);
|
||||||
ws.on('connect_error', onConnectError);
|
|
||||||
ws.connect();
|
ws.connect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user