diff --git a/src/api/global.ts b/src/api/global.ts index f6bd581..8a5d058 100644 --- a/src/api/global.ts +++ b/src/api/global.ts @@ -1,7 +1,6 @@ import str from '@/utils/str' import { http } from './http' import { get } from './request' -import { $XXL } from '@/utils' export type Option = { value: any @@ -99,6 +98,11 @@ export interface UserData { smartUserIdentifier: string schoolPersonType: number lockFlag: number + screenLock?: 0 | 1 + // 作业模式 0: 未开启 1: 已开启 + workMode: number + // 监控状态 0: 未开启 1: 已开启 + monitorState: number isTeacher: boolean // 是否为可以接受任务的学员 isTaskStudent: boolean @@ -206,6 +210,14 @@ const globalApi = { data, }), + // 上报课堂监控截图 + reportSnapshot: (data: { sessionId: string; url: string; userId: string }) => + http.request({ + url: '/school/classMonitor/reportSnapshot', + method: 'POST', + data, + }), + // 用户视频表-id查询 getUserVideoById: (id: any) => http.request({ diff --git a/src/hooks/useClassMonitor.ts b/src/hooks/useClassMonitor.ts new file mode 100644 index 0000000..f23babe --- /dev/null +++ b/src/hooks/useClassMonitor.ts @@ -0,0 +1,161 @@ +import html2canvas from 'html2canvas' +import { storeToRefs } from 'pinia' +import globalApi from '@/api/global' +import { userStore } from '@/stores' +import { $XXL, sys_type, sys_type_study_machine } from '@/utils' +import sass from '@/utils/sass' +import { useTips } from './useTips' + +let monitorTimer: ReturnType | null = null +let monitorSessionId = '' +let isUploadingMonitorSnapshot = false +let monitorState: 0 | 1 = 0 + +const MONITOR_SNAPSHOT_INTERVAL = 10000 +const MONITOR_MSG_TYPE = 'monitorStart' + +const normalizeUploadPath = (res: any) => { + return String( + res?.data?.filePath || res?.data?.url || res?.filePath || res?.url || res?.data?.path || '', + ) + .replace(/[`]/g, '') + .replace(/http:\/\//gi, 'https://') + .trim() +} + +const uploadNativeScreenshot = async () => { + const { token } = storeToRefs(userStore()) + const { data: filePath } = $XXL.getScreenshot() || {} + if (!filePath) return '' + + const res = await $XXL.uploadFile({ + url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`, + header: { + Authorization: token.value, + 'X-Tenant-Id': sass.platform.tenantId, + }, + filePath, + fileKey: 'file', + }) + return normalizeUploadPath(res) +} + +const createWebScreenshotFile = async () => { + const canvas = await html2canvas(document.body, { + useCORS: true, + backgroundColor: null, + x: window.scrollX, + y: window.scrollY, + width: window.innerWidth, + height: window.innerHeight, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + scale: window.devicePixelRatio || 1, + }) + + return await new Promise(resolve => { + canvas.toBlob(blob => { + if (!blob) return resolve(null) + resolve(new File([blob], `monitor-${Date.now()}.png`, { type: 'image/png' })) + }, 'image/png') + }) +} + +const uploadWebScreenshot = async () => { + const file = await createWebScreenshotFile() + if (!file) return '' + + const formData = new FormData() + formData.append('file', file) + const { data: res } = await globalApi.uploadFile(formData) + return normalizeUploadPath(res) +} + +const uploadMonitorScreenshot = async () => { + if (sys_type === sys_type_study_machine) { + return await uploadNativeScreenshot() + } + return await uploadWebScreenshot() +} + +const reportMonitorSnapshot = async (sessionId: string) => { + const { userInfo } = storeToRefs(userStore()) + if (isUploadingMonitorSnapshot || monitorSessionId !== sessionId) return + + isUploadingMonitorSnapshot = true + try { + const url = await uploadMonitorScreenshot() + if (!url || monitorSessionId !== sessionId) return + + await globalApi.reportSnapshot({ + sessionId, + url, + userId: userInfo.value.userId, + }) + } catch (err) { + console.log(err) + } finally { + isUploadingMonitorSnapshot = false + } +} + +export const stopClassMonitor = (showTip = false) => { + const isMonitoring = !!monitorTimer || !!monitorSessionId || monitorState === 1 + if (monitorTimer) { + clearInterval(monitorTimer) + monitorTimer = null + } + if (showTip && isMonitoring) { + useTips({ + tips: '已结束课堂监控', + showCancel: false, + }) + } + monitorSessionId = '' + monitorState = 0 +} + +const startClassMonitor = (msg: anyObj) => { + const sessionId = msg?.sessionId + if (!sessionId) return + if (monitorTimer && monitorSessionId === sessionId) return + + stopClassMonitor() + monitorSessionId = sessionId + monitorState = 1 + useTips({ + tips: '已进入课堂监控状态', + showCancel: false, + }) + reportMonitorSnapshot(sessionId) + monitorTimer = setInterval(() => reportMonitorSnapshot(sessionId), MONITOR_SNAPSHOT_INTERVAL) +} + +export const handleClassMonitorMessage = (data: anyObj) => { + const messageType = data?.type || data?.msg?.type + if (messageType !== MONITOR_MSG_TYPE) return false + + const msg = data?.msg || data + const monitoring = msg?.monitoring + if (monitoring === true || monitoring === 'true') { + startClassMonitor(msg) + return true + } + if (monitoring === false || monitoring === 'false') { + stopClassMonitor(true) + return true + } + return false +} + +export const syncClassMonitorState = (state: unknown) => { + const nextState = Number(state) + if (nextState !== 0 && nextState !== 1) return + + if (nextState === 0) { + stopClassMonitor(true) + return + } + + monitorState = 1 +} diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index b54918b..fe28673 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -1,10 +1,11 @@ import { useWebSocket } from '@vueuse/core' import { userStore, globalStore, urgeStore } from '@/stores' import type { UseWebSocketReturn } from '@vueuse/core' -import { getSN, $XXL, sys_type, sys_type_study_machine, isNotStudyMachine } from '@/utils' +import { getSN, $XXL, isNotStudyMachine } from '@/utils' import { useLog, LogType, useTips } from '@/hooks' import { useRouter } from 'vue-router' import { storeToRefs } from 'pinia' +import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor' let SOCKET: UseWebSocketReturn @@ -17,6 +18,7 @@ enum SOCKET_MSG_TYPE { inspectorEnd = 'inspectorEnd', systemMessage = 'systemMessage', messageReceive = 'messageReceive', + monitorStart = 'monitorStart', } const inspectorEnd = ref() @@ -30,7 +32,6 @@ export const useSocket = () => { const SN = getSN() const init = () => { - return if (import.meta.env.MODE === 'test') return if (!token.value) return stop() @@ -79,8 +80,28 @@ export const useSocket = () => { // console.log(ws, event); } + const getLockFlag = (data: anyObj, fallback: 0 | 1): 0 | 1 => { + const locked = data.msg?.locked + if (locked === true || locked === 'true') return 1 + if (locked === false || locked === 'false') return 0 + + const lockFlag = Number(locked) + return lockFlag === 0 || lockFlag === 1 ? lockFlag : fallback + } + + const updateLockScreen = (lockFlag: 0 | 1) => { + if (isNotStudyMachine) return + + system.value.isLock = lockFlag === 1 + useLog(LogType.LockScreenTime, { + simSerialNumber: SN, + lockFlag, + }) + } + const onMessage = (ws: WebSocket, event: MessageEvent) => { const data = JSON.parse(event.data) + const messageType = data.type || data.msg?.type // 确认接收 sendMsg({ @@ -89,7 +110,7 @@ export const useSocket = () => { }) // 全局消息提醒 - if (data.type === SOCKET_MSG_TYPE.systemMessage) { + if (messageType === SOCKET_MSG_TYPE.systemMessage) { useTips({ tips: data.msg, showCancel: false, @@ -98,29 +119,31 @@ export const useSocket = () => { } // if (isWeb) return; - switch (data.type) { + if ( + messageType !== SOCKET_MSG_TYPE.lockScreen && + messageType !== SOCKET_MSG_TYPE.unLockScreen && + typeof data.msg?.locked !== 'undefined' + ) { + updateLockScreen(getLockFlag(data, 0)) + return + } + + switch (messageType) { case SOCKET_MSG_TYPE.lockScreen: - if (isNotStudyMachine) return // 执行锁屏 - system.value.isLock = true - useLog(LogType.LockScreenTime, { - simSerialNumber: SN, - lockFlag: 1, - }) + updateLockScreen(getLockFlag(data, 1)) break case SOCKET_MSG_TYPE.unLockScreen: - if (isNotStudyMachine) return // 执行解锁 - system.value.isLock = false - useLog(LogType.LockScreenTime, { - simSerialNumber: SN, - lockFlag: 0, - }) + updateLockScreen(getLockFlag(data, 0)) break case SOCKET_MSG_TYPE.desktopPreview: if (isNotStudyMachine) return onDesktopPreview() break + case SOCKET_MSG_TYPE.monitorStart: + handleClassMonitorMessage(data) + break case SOCKET_MSG_TYPE.inspectorStart: // 开始督学 // console.log('开始督学'); @@ -197,6 +220,7 @@ export const useSocket = () => { } const stop = () => { + stopClassMonitor() SOCKET?.close() } diff --git a/src/hooks/useTeacherExplain.ts b/src/hooks/useTeacherExplain.ts index 9c7a3ed..05c6c12 100644 --- a/src/hooks/useTeacherExplain.ts +++ b/src/hooks/useTeacherExplain.ts @@ -13,6 +13,7 @@ import { ref, shallowRef } from 'vue' import questionExplainApi from '@/api/subject/questionExplain' import { getCache } from '@/utils' import { userStore } from '@/stores' +import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor' // ======================== 常量配置 ======================== @@ -194,6 +195,8 @@ function handleMessage(data: TeacherWsMessage) { // 心跳响应不做业务处理 if (data.type === 'pong') return + handleClassMonitorMessage(data) + // 调用所有已注册的外部消息回调 messageHandlers.forEach(handler => { try { @@ -285,6 +288,7 @@ async function init() { function close() { isManualClose = true stopHeartbeat() + stopClassMonitor() clearReconnectTimer() stopVisibilityListener() reconnectAttempts = 0 diff --git a/src/layouts/index.vue b/src/layouts/index.vue index 3cc307b..85635df 100644 --- a/src/layouts/index.vue +++ b/src/layouts/index.vue @@ -1,7 +1,7 @@