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