lingxixue-web/src/hooks/useClassMonitor.ts
2026-05-19 14:41:51 +08:00

270 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import studentScreenShareApi from '@/api/subject/studentScreenShare'
import { $XXL } from '@/utils'
import { userStore } from '@/stores'
import { useTips } from './useTips'
let monitorSessionId = ''
let monitorState: 0 | 1 = 0
let closeMonitorTip: (() => void) | null = null
/** 旧版课堂监控 WS 类型 */
const MONITOR_MSG_TYPE = 'monitorStart'
const MONITOR_STOP_MSG_TYPE = 'monitorStop'
/** 新版学生投屏 WS 类型 */
const STUDENT_SCREEN_SHARE_START = 'studentScreenShareStart'
const STUDENT_SCREEN_SHARE_STOP = 'studentScreenShareStop'
const MONITOR_WS_TYPES = new Set([
MONITOR_MSG_TYPE,
MONITOR_STOP_MSG_TYPE,
STUDENT_SCREEN_SHARE_START,
STUDENT_SCREEN_SHARE_STOP,
])
const MONITOR_LOG_PREFIX = '[class-monitor]'
const monitorLog = (...args: unknown[]) => console.log(MONITOR_LOG_PREFIX, ...args)
const monitorWarn = (...args: unknown[]) => console.warn(MONITOR_LOG_PREFIX, ...args)
const showMonitorTip = (tips: string) => {
closeMonitorTip?.()
closeMonitorTip = useTips({
tips,
showCancel: false,
})
}
const mergeMonitorPayload = (data: anyObj): anyObj => {
const msg = data?.msg && typeof data.msg === 'object' && !Array.isArray(data.msg) ? data.msg : {}
return {
...(data && typeof data === 'object' ? data : {}),
...msg,
}
}
const getMonitorSessionKey = (msg: anyObj) => {
const key =
msg?.sessionId ??
msg?.startedAt ??
msg?.classId ??
msg?.monitorSessionId ??
msg?.classMonitorSessionId
return key !== undefined && key !== null ? String(key).trim() : ''
}
const applyStudentShareStart = (msg: anyObj): boolean => {
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
const port = Number(msg.port)
if (!host || !Number.isFinite(port)) {
monitorWarn('startStudentShareScreen missing ip/port', msg)
return false
}
const u = userStore().userInfo
const config = {
host,
port,
name: String(u?.name || u?.nickName || ''),
id: String(u?.userId ?? ''),
}
monitorLog('startStudentShareScreen config', config)
$XXL.startStudentShareScreen(config)
return true
}
const startStudentShareScreenLive = (msg: anyObj) => {
const sessionKey = getMonitorSessionKey(msg)
monitorLog('start student screen share', msg)
if (monitorState === 1 && sessionKey && monitorSessionId === sessionKey) {
monitorLog('start ignored, session already running', sessionKey)
return
}
stopClassMonitor()
monitorSessionId = sessionKey
monitorState = 1
// showMonitorTip('已进入课堂监控状态')
if (!applyStudentShareStart(msg)) {
monitorState = 0
monitorSessionId = ''
}
}
/** 与老师讲解 WS 等场景兼容:外层 type 可能是 push/classMessage监控类型在 msg.type 或 data.type */
export const resolveMonitorWsType = (data: anyObj): string | undefined => {
const candidates = [data?.msg?.type, data?.type]
const inner = (data as Record<string, unknown>).data
if (inner && typeof inner === 'object') {
candidates.push((inner as anyObj).type)
}
for (const t of candidates) {
if (typeof t === 'string' && MONITOR_WS_TYPES.has(t)) return t
}
return undefined
}
export const stopClassMonitor = (showTip = false) => {
const isMonitoring = !!monitorSessionId || monitorState === 1
monitorLog('stop monitor', { showTip, isMonitoring, monitorSessionId, monitorState })
if (monitorState === 1) {
$XXL.stopStudentShareScreen()
}
if (showTip && isMonitoring) {
// showMonitorTip('已结束课堂监控')
}
monitorSessionId = ''
monitorState = 0
}
const startClassMonitor = (msg: anyObj) => {
const sessionId = msg?.sessionId
monitorLog('start monitor message', msg)
if (!sessionId) {
monitorWarn('start monitor ignored because sessionId is empty')
return
}
if (monitorState === 1 && monitorSessionId === String(sessionId)) {
monitorLog('start monitor ignored because session is already running', sessionId)
return
}
stopClassMonitor()
monitorSessionId = String(sessionId)
monitorState = 1
// showMonitorTip('已进入课堂监控状态')
if (!applyStudentShareStart(msg)) {
monitorState = 0
monitorSessionId = ''
}
}
const getMonitorSessionId = (data: anyObj) => {
const sessionId =
data?.sessionId ||
data?.monitorSessionId ||
data?.classMonitorSessionId ||
data?.monitorInfo?.sessionId ||
data?.classMonitor?.sessionId
return typeof sessionId === 'string' || typeof sessionId === 'number'
? String(sessionId).trim()
: ''
}
export const handleClassMonitorMessage = (data: anyObj) => {
const monitorType = resolveMonitorWsType(data)
monitorLog('handle monitor ws message', { monitorType, data })
if (!monitorType) return false
const msg = mergeMonitorPayload(data)
if (monitorType === STUDENT_SCREEN_SHARE_STOP || monitorType === MONITOR_STOP_MSG_TYPE) {
stopClassMonitor(true)
return true
}
if (monitorType === STUDENT_SCREEN_SHARE_START) {
startStudentShareScreenLive(msg)
return true
}
if (monitorType !== MONITOR_MSG_TYPE) return false
const monitoring = msg?.monitoring
if (monitoring === true || monitoring === 'true') {
startClassMonitor(msg)
return true
}
if (monitoring === false || monitoring === 'false') {
stopClassMonitor(true)
return true
}
return false
}
const resolveStudentScreenShareClassId = (msg: Record<string, unknown>): string => {
if (msg.classId !== undefined && msg.classId !== null && String(msg.classId).trim() !== '') {
return String(msg.classId)
}
const userClassId = userStore().userInfo?.classId
if (userClassId !== undefined && userClassId !== null && String(userClassId).trim() !== '') {
return String(userClassId)
}
return ''
}
/**
* 同步 getHomePage 返回的 studentScreenShareState
* sharing 为 true 时开启学生监控投屏,否则停止
*/
export const syncStudentScreenShareState = async (state: unknown) => {
const stateInfo = state && typeof state === 'object' ? (state as Record<string, unknown>) : {}
const screenShare = stateInfo.studentScreenShareState
if (!screenShare || typeof screenShare !== 'object') return
const share = screenShare as Record<string, unknown>
if (share.sharing !== true) {
stopClassMonitor(false)
return
}
let msg: anyObj = { ...share }
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
const port = Number(msg.port)
if (!host || !Number.isFinite(port)) {
const classId = resolveStudentScreenShareClassId(msg)
if (!classId) return
try {
const res = await studentScreenShareApi.state(classId)
if (!res?.sharing) return
msg = { ...msg, ip: res.ip, port: res.port, classId: res.classId ?? classId }
} catch (err) {
monitorWarn('首页学生监控状态补全失败:', err)
return
}
}
const sessionKey = getMonitorSessionKey(msg)
// 已在监控中:仅刷新连接参数,不先 stop 再 start与老师 screenShareState 同步一致)
if (monitorState === 1) {
if (sessionKey) monitorSessionId = sessionKey
applyStudentShareStart(msg)
return
}
monitorSessionId = sessionKey
monitorState = 1
if (!applyStudentShareStart(msg)) {
monitorState = 0
monitorSessionId = ''
}
}
export const syncClassMonitorState = (state: unknown) => {
const stateInfo = state && typeof state === 'object' ? (state as anyObj) : {}
const nextState = Number(stateInfo.monitorState ?? state)
if (nextState !== 0 && nextState !== 1) return
if (nextState === 0) {
stopClassMonitor(true)
return
}
const sessionId = getMonitorSessionId(stateInfo)
if (!sessionId) {
monitorWarn('sync monitor state ignored because sessionId is empty', stateInfo)
if (monitorState !== 1 && applyStudentShareStart(stateInfo)) {
monitorState = 1
}
return
}
startClassMonitor({
...stateInfo,
sessionId,
})
}