|
|
@@ -369,7 +369,8 @@ import CountdownTimer from '@/views/Screen/CountdownTimer.vue'
|
|
|
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
|
|
import useImport from '@/hooks/useImport'
|
|
|
import message from '@/utils/message'
|
|
|
-import api from '@/services/course'
|
|
|
+import api, { API_URL } from '@/services/course'
|
|
|
+import axios from '@/services/config'
|
|
|
import ShotWorkModal from './components/ShotWorkModal.vue'
|
|
|
import QAWorkModal from './components/QAWorkModal.vue'
|
|
|
import ChoiceWorkModal from './components/ChoiceWorkModal.vue'
|
|
|
@@ -382,6 +383,21 @@ import { Refresh } from '@icon-park/vue-next'
|
|
|
import answerTheResult from './components/answerTheResult.vue'
|
|
|
import choiceQuestionDetailDialog from './components/choiceQuestionDetailDialog.vue'
|
|
|
|
|
|
+// 生成标准 UUID v4 格式(36位,符合 [0-9a-fA-F-] 格式)
|
|
|
+const generateUUID = (): string => {
|
|
|
+ // 优先使用浏览器原生 API
|
|
|
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
|
+ return crypto.randomUUID()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 降级方案:手动生成 UUID v4
|
|
|
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
|
+ const r = (Math.random() * 16) | 0
|
|
|
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
|
+ return v.toString(16)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
// 导入图片资源
|
|
|
import homeworkIcon from '@/assets/img/homework.png'
|
|
|
@@ -533,6 +549,10 @@ const studentArray = ref<any>([])
|
|
|
// 跟随模式相关状态
|
|
|
const isCreator = ref(false) // 是否为创建人
|
|
|
const isFollowModeActive = ref(false) // 跟随模式是否开启
|
|
|
+const isFirstEnter = ref(true) // 是否首次进入
|
|
|
+
|
|
|
+// 用户信息
|
|
|
+const userJson = ref<any>(null)
|
|
|
|
|
|
// 计算未提交作业的学生
|
|
|
const unsubmittedStudents = computed(() => {
|
|
|
@@ -555,6 +575,8 @@ const providerSocket = ref<WebsocketProvider | null>(null)
|
|
|
const writingBoardSyncDataURL = ref<string | null>(null)
|
|
|
const writingBoardSyncBlackboard = ref<boolean | null>(null)
|
|
|
const mId = ref<string | null>(null)
|
|
|
+// 画图延迟发送定时器
|
|
|
+const drawingDelayTimer = ref<NodeJS.Timeout | null>(null)
|
|
|
|
|
|
// WebSocket重连相关变量
|
|
|
const reconnectAttempts = ref(0)
|
|
|
@@ -564,6 +586,9 @@ const reconnectTimer = ref<NodeJS.Timeout | null>(null)
|
|
|
const isConnecting = ref(false)
|
|
|
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected'>('disconnected')
|
|
|
|
|
|
+// 同步数据最大保留时间(40分钟)
|
|
|
+const SYNC_DATA_MAX_AGE = 40 * 60 * 1000 // 40分钟 = 40 * 60 * 1000毫秒
|
|
|
+
|
|
|
// 切换选择题题目
|
|
|
const changeWorkIndex = (type:number) => {
|
|
|
if (answerTheResultRef.value && answerTheResultRef.value.changeWorkIndex) {
|
|
|
@@ -1053,15 +1078,23 @@ const handleDrawingEnd = (dataURL: string) => {
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
- // 广播消息(包含当前小黑板状态)
|
|
|
- const currentBlackboard = yWritingBoardState.value?.get('blackboard') || false
|
|
|
- sendMessage({
|
|
|
- type: 'writing_board_update',
|
|
|
- slideId: currentSlide.value.id,
|
|
|
- dataURL: dataURL,
|
|
|
- blackboard: currentBlackboard,
|
|
|
- courseid: props.courseid
|
|
|
- })
|
|
|
+
|
|
|
+ // 延迟5秒后广播消息,避免频繁发送
|
|
|
+ if (drawingDelayTimer.value) {
|
|
|
+ clearTimeout(drawingDelayTimer.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ drawingDelayTimer.value = setTimeout(() => {
|
|
|
+ const currentBlackboard = yWritingBoardState.value?.get('blackboard') || false
|
|
|
+ sendMessage({
|
|
|
+ type: 'writing_board_update',
|
|
|
+ slideId: currentSlide.value.id,
|
|
|
+ dataURL: dataURL,
|
|
|
+ blackboard: currentBlackboard,
|
|
|
+ courseid: props.courseid
|
|
|
+ })
|
|
|
+ drawingDelayTimer.value = null
|
|
|
+ }, 5000) // 延迟5秒发送
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1167,6 +1200,40 @@ const clearLaserState = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 清空所有同步状态(仅创建人)
|
|
|
+const clearAllSyncStates = () => {
|
|
|
+ try {
|
|
|
+ if (props.type == '1' && isCreator.value && docSocket.value) {
|
|
|
+ console.log('🧹 创建老师退出,清空所有同步状态')
|
|
|
+ docSocket.value.transact(() => {
|
|
|
+ // 清空消息
|
|
|
+ const messageArray = docSocket.value?.getArray?.('message')
|
|
|
+ if (messageArray) {
|
|
|
+ messageArray.delete(0, messageArray.length)
|
|
|
+ }
|
|
|
+ // 清空计时器状态
|
|
|
+ const timerStateMap = docSocket.value?.getMap?.('timerState')
|
|
|
+ if (timerStateMap) {
|
|
|
+ timerStateMap.clear()
|
|
|
+ }
|
|
|
+ // 清空激光笔状态
|
|
|
+ const laserStateMap = docSocket.value?.getMap?.('laserState')
|
|
|
+ if (laserStateMap) {
|
|
|
+ laserStateMap.clear()
|
|
|
+ }
|
|
|
+ // 清空画图状态
|
|
|
+ const writingBoardStateMap = docSocket.value?.getMap?.('writingBoardState')
|
|
|
+ if (writingBoardStateMap) {
|
|
|
+ writingBoardStateMap.clear()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.warn('清空所有同步状态失败', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 获取导入导出功能
|
|
|
const { readJSON, exportJSON2, getFile } = useImport()
|
|
|
|
|
|
@@ -1526,6 +1593,8 @@ const handleHomeworkSubmit = async () => {
|
|
|
}
|
|
|
|
|
|
isSubmitting.value = true
|
|
|
+ let homeworkContent: string = '作业提交' // 默认作业内容
|
|
|
+ let hasSubmitWork = false // 标记是否成功提交作业
|
|
|
|
|
|
try {
|
|
|
// 获取所有iframe元素
|
|
|
@@ -1537,8 +1606,6 @@ const handleHomeworkSubmit = async () => {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- let hasSubmitWork = false
|
|
|
-
|
|
|
for (let i = 0; i < iframes.length; i++) {
|
|
|
const iframe = iframes[i] as HTMLIFrameElement
|
|
|
const iframeSrc = iframe.src
|
|
|
@@ -1559,6 +1626,16 @@ const handleHomeworkSubmit = async () => {
|
|
|
// 支持同步和异步submitWork
|
|
|
const result = await iframeWindow.submitWork(...submitArgs)
|
|
|
console.log('submitWork同步执行完成')
|
|
|
+ // 尝试从结果中获取作业内容
|
|
|
+ if (result && typeof result === 'object') {
|
|
|
+ homeworkContent = JSON.stringify(result)
|
|
|
+ }
|
|
|
+ else if (result) {
|
|
|
+ homeworkContent = String(result)
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ homeworkContent = 'workPage作业提交'
|
|
|
+ }
|
|
|
message.success('作业提交成功')
|
|
|
hasSubmitWork = true
|
|
|
|
|
|
@@ -1596,6 +1673,7 @@ const handleHomeworkSubmit = async () => {
|
|
|
const file = new File([blob], `ai_work_${Date.now()}.json`, { type: 'application/json' })
|
|
|
const fileUrl = await uploadFile(file)
|
|
|
console.log('文件上传成功,链接:', fileUrl)
|
|
|
+ homeworkContent = fileUrl // 保存AI作业内容
|
|
|
|
|
|
// 使用上传后的链接提交作业
|
|
|
await submitWork(iframeSlideIndex, '72', fileUrl, '20')
|
|
|
@@ -1800,6 +1878,7 @@ const handleHomeworkSubmit = async () => {
|
|
|
|
|
|
const imageFile = base64ToFile(imageData, `screenshot_${Date.now()}.png`)
|
|
|
const imageUrl = await uploadFile(imageFile)
|
|
|
+ homeworkContent = imageUrl // 保存截图URL作为作业内容
|
|
|
// 提交截图
|
|
|
await submitWork(slideIndex.value, '73', imageUrl, '1') // 73表示截图工具,21表示图片类型
|
|
|
message.success('页面截图提交成功')
|
|
|
@@ -1898,10 +1977,17 @@ const handleHomeworkSubmit = async () => {
|
|
|
console.error('作业提交过程中出错:', error)
|
|
|
message.error('作业提交失败')
|
|
|
isSubmitting.value = false
|
|
|
+ addOp3(1, new Date().getTime(), { courseid: props.courseid, homeworkContent }, 'error')
|
|
|
}
|
|
|
finally {
|
|
|
// isSubmitting.value = false
|
|
|
getWork(true)
|
|
|
+ if (hasSubmitWork) {
|
|
|
+ addOp3(1, new Date().getTime(), { courseid: props.courseid, homeworkContent }, 'success')
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ addOp3(1, new Date().getTime(), { courseid: props.courseid, homeworkContent: '未找到可用的作业提交功能' }, 'error')
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2082,6 +2168,72 @@ const handleViewportSizeUpdated = (event: any) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const pptJsonFileid = ref<string>('')
|
|
|
+
|
|
|
+// 上传文件
|
|
|
+const uploadFile2 = async (file: File, pptid: string): Promise<void> => {
|
|
|
+ try {
|
|
|
+ const uuid = generateUUID()
|
|
|
+ const formData = new FormData()
|
|
|
+ const timestamp = Date.now()
|
|
|
+ const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
|
|
|
+ const baseName = file.name.slice(0, -(finalExtension.length + 1))
|
|
|
+
|
|
|
+ formData.append(
|
|
|
+ 'file',
|
|
|
+ new File([file], `${baseName}${timestamp}.${finalExtension}`)
|
|
|
+ )
|
|
|
+ formData.append('collection_ids', JSON.stringify([]))
|
|
|
+ formData.append('id', uuid)
|
|
|
+ formData.append('metadata', JSON.stringify({ title: file.name }))
|
|
|
+ formData.append('ingestion_mode', 'fast')
|
|
|
+ formData.append('run_with_orchestration', 'true')
|
|
|
+
|
|
|
+ // 同步知识库
|
|
|
+ await axios.post(
|
|
|
+ 'https://r2rserver.cocorobo.cn/v3/documents',
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const ptype = '1' // 根据实际业务定义类型
|
|
|
+ const fileid = uuid // 如果需要唯一fileid可以和pptid保持一致或按需更改
|
|
|
+
|
|
|
+ await axios.post(`${API_URL}addPPTFile`, [{
|
|
|
+ pptid: pptid,
|
|
|
+ ptype: ptype,
|
|
|
+ fileid: fileid,
|
|
|
+ classid: '',
|
|
|
+ task: '',
|
|
|
+ tool: ''
|
|
|
+ }])
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ console.error(err)
|
|
|
+ throw err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const checkPPTFile = async (jsonObj: any) => {
|
|
|
+ const res = await api.getPPTFile(props.courseid as string, props.cid as string)
|
|
|
+ console.log(res)
|
|
|
+ const data1 = res[0]
|
|
|
+ const data2 = res[1]
|
|
|
+ const data3 = res[2]
|
|
|
+ console.log(data1, data2, data3)
|
|
|
+ if (res[0].length) {
|
|
|
+ pptJsonFileid.value = data1[0].fileid
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ const pptJsonFile = new File([jsonObj], courseDetail.value.courseName + '.json', { type: 'application/json' })
|
|
|
+ uploadFile2(pptJsonFile, props.courseid as string)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const getCourseDetail = async () => {
|
|
|
isLoading.value = true
|
|
|
try {
|
|
|
@@ -2104,6 +2256,7 @@ const getCourseDetail = async () => {
|
|
|
jsonStr = new TextDecoder('utf-8').decode(uint8Array)
|
|
|
try {
|
|
|
const jsonObj = JSON.parse(jsonStr)
|
|
|
+ checkPPTFile(jsonObj)
|
|
|
importJSON(jsonObj)
|
|
|
}
|
|
|
catch (e) {
|
|
|
@@ -2358,15 +2511,76 @@ const checkIsCreator = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-
|
|
|
/**
|
|
|
* 初始化消息监听
|
|
|
*/
|
|
|
const messageInit = () => {
|
|
|
+
|
|
|
+
|
|
|
if (docSocket.value && !yMessage.value) {
|
|
|
console.log('获取message', docSocket.value, yMessage.value)
|
|
|
yMessage.value = docSocket.value.getArray('message')
|
|
|
yMessage.value.observe((e: any) => {
|
|
|
+ // 执行清空数据
|
|
|
+ // 数据同步完成后,清理超过40分钟的消息数据
|
|
|
+ const messages = yMessage.value.toArray()
|
|
|
+ console.log('messages', messages)
|
|
|
+ console.log('messagesLength', messages.length)
|
|
|
+ // 如果是首次进入且是创建者,清空所有同步状态
|
|
|
+ if ((isFirstEnter.value || messages.length > 2000) && isCreator.value && docSocket.value) {
|
|
|
+ console.log('🧹 首次进入且为创建者或消息条数超2000,保留最新2000条消息,其他同步状态全部清空')
|
|
|
+ docSocket.value.transact(() => {
|
|
|
+ // 只保留最新2000条消息
|
|
|
+ const messageArray = docSocket.value?.getArray?.('message')
|
|
|
+ if (messageArray && messageArray.length > 2000) {
|
|
|
+ messageArray.delete(0, messageArray.length - 2000)
|
|
|
+ }
|
|
|
+ // 清空计时器状态
|
|
|
+ const timerStateMap = docSocket.value?.getMap?.('timerState')
|
|
|
+ if (timerStateMap) {
|
|
|
+ timerStateMap.clear()
|
|
|
+ }
|
|
|
+ // 清空激光笔状态
|
|
|
+ const laserStateMap = docSocket.value?.getMap?.('laserState')
|
|
|
+ if (laserStateMap) {
|
|
|
+ laserStateMap.clear()
|
|
|
+ }
|
|
|
+ // 清空画图状态
|
|
|
+ const writingBoardStateMap = docSocket.value?.getMap?.('writingBoardState')
|
|
|
+ if (writingBoardStateMap) {
|
|
|
+ writingBoardStateMap.clear()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 标记已不再是首次进入
|
|
|
+ isFirstEnter.value = false
|
|
|
+ }
|
|
|
+ if (messages.length > 0) {
|
|
|
+ const now = Date.now()
|
|
|
+ const messagesToKeep: any[] = []
|
|
|
+
|
|
|
+ for (let i = messages.length - 1; i >= 0; i--) {
|
|
|
+ const message = messages[i]
|
|
|
+ if (message && typeof message === 'object' && message.timestamp) {
|
|
|
+ const messageTime = new Date(message.timestamp).getTime()
|
|
|
+ if (now - messageTime <= SYNC_DATA_MAX_AGE) {
|
|
|
+ messagesToKeep.unshift(message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有需要清理的消息
|
|
|
+ if (messagesToKeep.length < messages.length) {
|
|
|
+ // 修复报错:docSocket.value 可能为 null
|
|
|
+ if (docSocket.value) {
|
|
|
+ docSocket.value.transact(() => {
|
|
|
+ yMessage.value.delete(0, messages.length)
|
|
|
+ messagesToKeep.forEach((msg: any) => yMessage.value.push([msg]))
|
|
|
+ })
|
|
|
+ console.log(`🧹 清理了 ${messages.length - messagesToKeep.length} 条超过40分钟的消息`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
e.changes.added.forEach((i: any) => {
|
|
|
const message = i.content.getContent()[0]
|
|
|
console.log('yMessage', message)
|
|
|
@@ -2604,13 +2818,112 @@ const handlePageUnload = () => {
|
|
|
if (isCreator.value && timerIndicator.value.visible && props.type === '1') {
|
|
|
sendMessage({ type: 'timer_stop', courseid: props.courseid })
|
|
|
}
|
|
|
- // 创建老师刷新/关闭页面时,清空激光笔和画图共享状态
|
|
|
+ // 创建老师刷新/关闭页面时,清空所有同步状态
|
|
|
if (isCreator.value && props.type === '1') {
|
|
|
- clearLaserState()
|
|
|
- clearWritingBoardState()
|
|
|
+ clearAllSyncStates()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理画图延迟发送定时器
|
|
|
+ if (drawingDelayTimer.value) {
|
|
|
+ clearTimeout(drawingDelayTimer.value)
|
|
|
+ drawingDelayTimer.value = null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 检测浏览器类型
|
|
|
+const detectBrowser = () => {
|
|
|
+ const ua = navigator.userAgent
|
|
|
+
|
|
|
+ // 按优先级顺序检测
|
|
|
+ if (ua.includes('Edg/') || ua.includes('Edge/')) {
|
|
|
+ return 'Microsoft Edge'
|
|
|
+ }
|
|
|
+ if (ua.includes('Firefox')) {
|
|
|
+ return 'Mozilla Firefox'
|
|
|
+ }
|
|
|
+ if (ua.includes('Trident') || ua.includes('MSIE')) {
|
|
|
+ return 'Internet Explorer'
|
|
|
+ }
|
|
|
+ if (ua.includes('360EE')) {
|
|
|
+ return '360 Browser (极速模式)'
|
|
|
+ }
|
|
|
+ if (ua.includes('360SE')) {
|
|
|
+ return '360 Browser (安全模式)'
|
|
|
+ }
|
|
|
+ if (ua.includes('SLBrowser')) {
|
|
|
+ return 'QQ Browser'
|
|
|
+ }
|
|
|
+ if (ua.includes('UCBrowser')) {
|
|
|
+ return 'UC Browser'
|
|
|
+ }
|
|
|
+ if (ua.includes('Opera') || ua.includes('OPR/')) {
|
|
|
+ return 'Opera'
|
|
|
+ }
|
|
|
+ if (ua.includes('Chrome') && !ua.includes('Edg/')) {
|
|
|
+ return 'Google Chrome'
|
|
|
+ }
|
|
|
+ if (ua.includes('Safari/') && !ua.includes('Chrome')) {
|
|
|
+ return 'Safari'
|
|
|
+ }
|
|
|
+ return 'Other Browser'
|
|
|
+}
|
|
|
+
|
|
|
+// 用户数据上报功能
|
|
|
+const addOp3 = async (userTime: any, loadTime: any, object: any, status: any) => {
|
|
|
+ if (!props.userid) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!userJson.value || !userJson.value.accountNumber) {
|
|
|
+ const res = await axios.get('https://pbl.cocorobo.cn/api/pbl/selectUser', {
|
|
|
+ params: { userid: props.userid }
|
|
|
+ })
|
|
|
+ userJson.value = res[0][0]
|
|
|
+ console.log(userJson.value)
|
|
|
+ console.log(res[0][0])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.log(e)
|
|
|
+ return addOp3(userTime, loadTime, object, status)
|
|
|
+ }
|
|
|
+
|
|
|
+ const _time = new Date()
|
|
|
+ .toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
|
|
|
+ .replace(/\//g, '-')
|
|
|
+
|
|
|
+ const browser = detectBrowser()
|
|
|
+ const params = {
|
|
|
+ userid: props.userid,
|
|
|
+ username: userJson.value.username,
|
|
|
+ accountNumber: userJson.value.accountNumber,
|
|
|
+ org: userJson.value.orgName,
|
|
|
+ school: userJson.value.schoolName,
|
|
|
+ role: userJson.value.type === '1' ? '老师' : '学生',
|
|
|
+ browser,
|
|
|
+ userTime: userTime === '1' ? _time : userTime, // 使用时间 1次的就1 其次传秒
|
|
|
+ loadTime, // load的时间没有就''
|
|
|
+ object: JSON.stringify(object), // 执行信息传json
|
|
|
+ status // 成功返回success。失败返回error的信息
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('params', params)
|
|
|
+
|
|
|
+ axios
|
|
|
+ .post('https://pbl.cocorobo.cn/api/mongo/updateUserData2', [params])
|
|
|
+ .then(res => {
|
|
|
+ if (res.status === 1) {
|
|
|
+ console.log('保存成功')
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ console.log('保存失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(e => {
|
|
|
+ console.log('保存失败')
|
|
|
+ console.log(e)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
document.addEventListener('keydown', handleKeydown)
|
|
|
|
|
|
@@ -2696,9 +3009,8 @@ onMounted(() => {
|
|
|
// visibilitychange 事件(适用于 iframe 嵌套场景,当外层页面返回时触发)
|
|
|
const handleVisibilityChange = () => {
|
|
|
if (document.hidden && isCreator.value) {
|
|
|
- // 页面被隐藏时,清空激光笔和画图状态
|
|
|
- clearLaserState()
|
|
|
- clearWritingBoardState()
|
|
|
+ // 页面被隐藏时,清空所有同步状态
|
|
|
+ clearAllSyncStates()
|
|
|
if (timerIndicator.value.visible) {
|
|
|
sendMessage({ type: 'timer_stop', courseid: props.courseid })
|
|
|
}
|
|
|
@@ -2734,12 +3046,18 @@ onUnmounted(() => {
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
reconnectTimer.value = null
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (providerSocket.value) {
|
|
|
providerSocket.value.destroy()
|
|
|
providerSocket.value = null
|
|
|
}
|
|
|
|
|
|
+ // 清理画图延迟发送定时器
|
|
|
+ if (drawingDelayTimer.value) {
|
|
|
+ clearTimeout(drawingDelayTimer.value)
|
|
|
+ drawingDelayTimer.value = null
|
|
|
+ }
|
|
|
+
|
|
|
// 清理页面卸载相关的事件监听器
|
|
|
if ((window as any).__pptistStudentUnloadHandlers) {
|
|
|
const handlers = (window as any).__pptistStudentUnloadHandlers
|