|
|
@@ -551,6 +551,9 @@ const isCreator = ref(false) // 是否为创建人
|
|
|
const isFollowModeActive = ref(false) // 跟随模式是否开启
|
|
|
const isFirstEnter = ref(true) // 是否首次进入
|
|
|
|
|
|
+// 用户信息
|
|
|
+const userJson = ref<any>(null)
|
|
|
+
|
|
|
// 计算未提交作业的学生
|
|
|
const unsubmittedStudents = computed(() => {
|
|
|
if (!studentArray.value || !workArray.value) return []
|
|
|
@@ -572,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)
|
|
|
@@ -1073,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秒发送
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1187,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()
|
|
|
|
|
|
@@ -1546,6 +1593,8 @@ const handleHomeworkSubmit = async () => {
|
|
|
}
|
|
|
|
|
|
isSubmitting.value = true
|
|
|
+ let homeworkContent: string = '作业提交' // 默认作业内容
|
|
|
+ let hasSubmitWork = false // 标记是否成功提交作业
|
|
|
|
|
|
try {
|
|
|
// 获取所有iframe元素
|
|
|
@@ -1557,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
|
|
|
@@ -1579,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
|
|
|
|
|
|
@@ -1616,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')
|
|
|
@@ -1820,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('页面截图提交成功')
|
|
|
@@ -1918,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')
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2752,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)
|
|
|
|
|
|
@@ -2844,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 })
|
|
|
}
|
|
|
@@ -2882,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
|