This commit is contained in:
M了个J 2026-05-11 15:15:37 +08:00
commit 12c6f0759b
9 changed files with 369 additions and 30 deletions

View File

@ -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({

View File

@ -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<typeof setInterval> | 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<File | null>(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
}

View File

@ -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<any>
@ -17,6 +18,7 @@ enum SOCKET_MSG_TYPE {
inspectorEnd = 'inspectorEnd',
systemMessage = 'systemMessage',
messageReceive = 'messageReceive',
monitorStart = 'monitorStart',
}
const inspectorEnd = ref<anyObj>()
@ -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()
}

View File

@ -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

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { globalStore, subjectStore } from '@/stores'
import { useSocket, useTeacherExplain } from '@/hooks'
import { $XXL, isWebEvn } from '@/utils'
import { globalStore, subjectStore, userStore } from '@/stores'
import { LogType, useLog, useSocket, useTeacherExplain } from '@/hooks'
import { $XXL, getSN, isStudyMachine, isWebEvn } from '@/utils'
import hud from '@/utils/hud'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
@ -12,8 +12,12 @@ const logo = sass.getLogo()
const router = useRouter()
const { system, init, getDict, initDevTool, destroyDevTool } = globalStore()
const { getUserInfo } = userStore()
const { getSubjectAllList } = subjectStore()
const LOCK_HOME_PAGE_REFRESH_INTERVAL = 10_000
let lockHomePageTimer: ReturnType<typeof setInterval> | null = null
let lockHomePageRefreshing = false
if (!isWebEvn) {
//
@ -82,6 +86,52 @@ const {
} = useTeacherExplain()
teacherExplainInit()
function 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
}
function updateLockScreen(lockFlag: 0 | 1) {
if (!isStudyMachine) return
system.isLock = lockFlag === 1
useLog(LogType.LockScreenTime, {
simSerialNumber: getSN(),
lockFlag,
})
}
async function refreshLockHomePage() {
if (lockHomePageRefreshing) return
lockHomePageRefreshing = true
try {
await getUserInfo()
} catch (err) {
console.warn('锁屏状态刷新首页信息失败', err)
} finally {
lockHomePageRefreshing = false
}
}
function startLockHomePageRefresh() {
if (lockHomePageTimer) return
refreshLockHomePage()
lockHomePageTimer = setInterval(refreshLockHomePage, LOCK_HOME_PAGE_REFRESH_INTERVAL)
}
function stopLockHomePageRefresh() {
if (!lockHomePageTimer) return
clearInterval(lockHomePageTimer)
lockHomePageTimer = null
}
// WebSocket
watch(
() => router.currentRoute.value.path,
@ -92,9 +142,32 @@ watch(
},
)
watch(
() => system.isLock,
isLock => {
if (isLock) {
startLockHomePageRefresh()
return
}
stopLockHomePageRefresh()
},
{ immediate: true },
)
// WebSocket questionExplainStart explaining true
// questionId
onTeacherMessage(data => {
if (data.type === 'lockScreen') {
updateLockScreen(getLockFlag(data, 1))
return
}
if (data.type === 'unLockScreen') {
updateLockScreen(getLockFlag(data, 0))
return
}
if (data.type === 'questionExplainStart') {
let msg = data.msg
if (typeof msg === 'string') {
@ -190,6 +263,10 @@ function onLoaded() {
onMounted(() => {
$XXL.closeLoading()
})
onUnmounted(() => {
stopLockHomePageRefresh()
})
</script>
<template>
<!-- @click="onPageClick" -->

View File

@ -3,7 +3,7 @@ import { userStore, globalStore, taskStore, subjectStore } from '@/stores'
import { storeToRefs } from 'pinia'
import globalApi from '@/api/global'
import { useRouter } from 'vue-router'
import { useAudio } from '@/hooks'
import { useAudio, XMessage } from '@/hooks'
import {
getLevelProgress,
$XXL,
@ -43,6 +43,7 @@ const taskData = taskStore()
const { userInfo, workToken } = storeToRefs(userStore())
const { getUserInfo, isFirstBind } = userStore()
const { system, checkUpdate } = globalStore()
const isHomePageLoading = ref(false)
const navMain = computed(() => {
// const { purchasedInspector, nextInspectorCourseDate, conductInspectorPlanId } =
@ -238,6 +239,8 @@ function getTaskRedPoint() {
const { selectSubject } = subjectStore()
const handleNav = debounce(
(data: anyObj) => {
if (isHomePageLoading.value) return
if (data.key === 'homeWork' && userInfo.value.isTeacher) {
const path = `${import.meta.env.VITE_TEACHER_ADMIN_URL}?token=${workToken.value.replace('Bearer ', '')}`
if (isWebEvn) {
@ -287,17 +290,36 @@ function handleKnown(data: anyObj) {
//
function handleToUserPage() {
if (isHomePageLoading.value) return
router.push({ path: '/system/user' })
}
//
function clickPhoto() {
if (isHomePageLoading.value) return
if (isShufaAccount(userInfo.value.account)) {
//
router.replace(SHUFA_PATH)
}
}
async function refreshHomePage() {
isHomePageLoading.value = true
try {
await getUserInfo()
if (Number(userInfo.value.workMode) === 1) {
XMessage.info('当前处于作业模式')
await router.replace('/system/homeWork')
return true
}
return false
} finally {
isHomePageLoading.value = false
}
}
let timer: any
onMounted(async () => {
//
@ -316,7 +338,8 @@ onActivated(async () => {
getPoint()
getTaskRedPoint()
await getUserInfo()
const isWorkMode = await refreshHomePage()
if (isWorkMode) return
//
await nextTick()

View File

@ -190,7 +190,7 @@ const startDoWork = () => {
></Empty>
</div>
<div class="top">
<BackBar leftText="作业" />
<BackBar leftText="作业" index />
<div class="top-history">
<!-- <mj-button type="success" tilt @click="to(`/system/homeWork/statistics`)"
>统计分析</mj-button

View File

@ -204,7 +204,7 @@ router.beforeEach(async (to, from, next) => {
// 全局的返回函数
export function globalBack(home = false) {
if (window.history.state?.position === 1 || !window.history.state.back || home) {
router.push('/')
router.replace('/')
} else {
const afterRoute = router.currentRoute.value.fullPath
router.back()

View File

@ -6,8 +6,9 @@ import loginApi from '@/api/login'
import type { accountLoginType } from '@/api/login'
import type { Option, UserData } from '@/api/global'
import type { School } from '@/api/studentManage'
import { setCache, clearCache, getSN, getManufacturer, isStudyMachine } from '@/utils'
import { useSocket } from '@/hooks'
import { setCache, clearCache, getSN, getManufacturer, isStudyMachine, isWebEvn } from '@/utils'
import { LogType, useLog, useSocket } from '@/hooks'
import { syncClassMonitorState } from '@/hooks/useClassMonitor'
import { globalStore } from '@/stores'
import { storeToRefs } from 'pinia'
import obj from '@/utils/obj'
@ -84,17 +85,42 @@ export const userStore = defineStore('user', () => {
return userInfo.value.schoolClass?.[0]?.classList?.[0]
}
const updateScreenLock = (screenLock?: unknown) => {
const lockFlag = Number(screenLock)
if (lockFlag !== 0 && lockFlag !== 1) return
const { system } = storeToRefs(globalStore())
const isLock = lockFlag === 1
if (!isStudyMachine && isLock) return
if (system.value.isLock === isLock) return
system.value.isLock = isLock
if (!isStudyMachine) return
useLog(LogType.LockScreenTime, {
simSerialNumber: getSN(),
lockFlag,
})
}
// 更新用户信息
const updateUserInfo = async (info: any) => {
if (!info) return
const hasMonitorState = Object.prototype.hasOwnProperty.call(info, 'monitorState')
info = { ...userInfo.value, ...info }
if (isStudyMachine) {
if (typeof info.screenLock !== 'undefined') {
updateScreenLock(info.screenLock)
} else if (isStudyMachine) {
// 锁屏
const { system } = storeToRefs(globalStore())
system.value.isLock = info.lockFlag === 0
}
if (hasMonitorState) {
syncClassMonitorState(info.monitorState)
}
if (typeof info.schoolClass === 'string') {
info.schoolClass = JSON.parse(info.schoolClass)
if (info.schoolClass?.length) {
@ -158,13 +184,25 @@ export const userStore = defineStore('user', () => {
setCache('token', `Bearer ${tokenValue}`)
}
const getDeviceSerialNumber = () => {
const { system } = globalStore()
return system.device.SN || getSN()
}
// 账号登录
const accountLogin = async (data: accountLoginType) => {
const res = await loginApi.psdLogin({
const loginData: accountLoginType = {
...data,
simSerialNumber: getSN(),
model: getManufacturer(),
})
}
if (isWebEvn) {
delete loginData.simSerialNumber
} else {
loginData.simSerialNumber = getDeviceSerialNumber()
}
const res = await loginApi.psdLogin(loginData)
await loginBefore()
setToken(res as any)
await loginAfter()