feat: 监控和讲解增加心跳
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled

This commit is contained in:
shawko 2026-05-11 18:29:55 +08:00
parent 228ee4ba68
commit 07ec772e9b
5 changed files with 83 additions and 4 deletions

View File

@ -144,6 +144,12 @@ class ClassroomMonitorService {
status, status,
}) })
} }
async heartbeat(classId: number | string) {
return await post(
`/school/classMonitor/heartbeat?classId=${encodeURIComponent(String(classId))}`,
)
}
} }
export const classroomMonitorService = new ClassroomMonitorService() export const classroomMonitorService = new ClassroomMonitorService()

View File

@ -89,6 +89,13 @@ class TaskMgrService extends BasicService {
return await post<any>(`/school/questionExplain/share`, data) return await post<any>(`/school/questionExplain/share`, data)
} }
// 题目讲解心跳
async questionExplainHeartbeat(classId: string) {
return await post<any>(
`/school/questionExplain/heartbeat?classId=${encodeURIComponent(classId)}`,
)
}
// 获取试卷详情 // 获取试卷详情
// async getpaperReleaseRecordDetail(recordId: any) { // async getpaperReleaseRecordDetail(recordId: any) {
// return await get<any>(`/school/paperReleaseRecord/get/${recordId}`) // return await get<any>(`/school/paperReleaseRecord/get/${recordId}`)

View File

@ -43,7 +43,9 @@ const studentSnapshotPreviewList = computed(() => {
return studentSnapshots.value.map((item) => item.url).filter(Boolean) return studentSnapshots.value.map((item) => item.url).filter(Boolean)
}) })
const SNAPSHOT_REFRESH_INTERVAL = 5000 const SNAPSHOT_REFRESH_INTERVAL = 5000
const MONITOR_HEARTBEAT_INTERVAL = 60 * 1000
let snapshotRefreshTimer: ReturnType<typeof setInterval> | undefined let snapshotRefreshTimer: ReturnType<typeof setInterval> | undefined
let heartbeatTimer: ReturnType<typeof setInterval> | undefined
onMounted(() => { onMounted(() => {
loadClassList() loadClassList()
@ -51,6 +53,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopSnapshotAutoRefresh() stopSnapshotAutoRefresh()
stopMonitorHeartbeat()
}) })
async function loadClassList() { async function loadClassList() {
@ -64,6 +67,7 @@ async function loadClassList() {
async function handleViewMonitor(item: ClassroomItem) { async function handleViewMonitor(item: ClassroomItem) {
stopSnapshotAutoRefresh() stopSnapshotAutoRefresh()
stopMonitorHeartbeat()
selectedClass.value = item selectedClass.value = item
currentMonitor.value = undefined currentMonitor.value = undefined
snapshots.value = [] snapshots.value = []
@ -79,8 +83,10 @@ async function loadMonitorDetail(classId = selectedClass.value?.id) {
if (data?.monitoring) { if (data?.monitoring) {
await loadSnapshots(classId) await loadSnapshots(classId)
startSnapshotAutoRefresh() startSnapshotAutoRefresh()
startMonitorHeartbeat()
} else { } else {
stopSnapshotAutoRefresh() stopSnapshotAutoRefresh()
stopMonitorHeartbeat()
snapshots.value = [] snapshots.value = []
} }
} finally { } finally {
@ -130,12 +136,31 @@ function startSnapshotAutoRefresh() {
}, SNAPSHOT_REFRESH_INTERVAL) }, SNAPSHOT_REFRESH_INTERVAL)
} }
function startMonitorHeartbeat() {
if (heartbeatTimer) return
heartbeatTimer = setInterval(() => {
if (!isMonitoring.value || !selectedClass.value?.id) {
stopMonitorHeartbeat()
return
}
void classroomMonitorService
.heartbeat(selectedClass.value.id)
.catch(() => undefined)
}, MONITOR_HEARTBEAT_INTERVAL)
}
function stopSnapshotAutoRefresh() { function stopSnapshotAutoRefresh() {
if (!snapshotRefreshTimer) return if (!snapshotRefreshTimer) return
clearInterval(snapshotRefreshTimer) clearInterval(snapshotRefreshTimer)
snapshotRefreshTimer = undefined snapshotRefreshTimer = undefined
} }
function stopMonitorHeartbeat() {
if (!heartbeatTimer) return
clearInterval(heartbeatTimer)
heartbeatTimer = undefined
}
async function updateMonitorStatus(status: 0 | 1) { async function updateMonitorStatus(status: 0 | 1) {
if (!selectedClass.value?.id) return if (!selectedClass.value?.id) return
const option = status === 1 ? '开启' : '停止' const option = status === 1 ? '开启' : '停止'
@ -151,6 +176,7 @@ async function updateMonitorStatus(status: 0 | 1) {
function backToClassList() { function backToClassList() {
stopSnapshotAutoRefresh() stopSnapshotAutoRefresh()
stopMonitorHeartbeat()
selectedClass.value = undefined selectedClass.value = undefined
currentMonitor.value = undefined currentMonitor.value = undefined
snapshots.value = [] snapshots.value = []

View File

@ -1,5 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue' import {
computed,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
ref,
} from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { taskMgrService } from '#/api/service/question-bank/task-mgr' import { taskMgrService } from '#/api/service/question-bank/task-mgr'
@ -80,6 +87,7 @@ onMounted(() => {
}) })
onActivated(() => { onActivated(() => {
stopQuestionExplainHeartbeat()
phase.value = 'detail' phase.value = 'detail'
countdown.value = 3 countdown.value = 3
currentIndex.value = 0 currentIndex.value = 0
@ -92,6 +100,8 @@ type ExplainPhase = 'countdown' | 'detail' | 'explaining'
const phase = ref<ExplainPhase>('detail') const phase = ref<ExplainPhase>('detail')
const countdown = ref(3) const countdown = ref(3)
let countdownTimer: null | ReturnType<typeof setInterval> = null let countdownTimer: null | ReturnType<typeof setInterval> = null
const QUESTION_EXPLAIN_HEARTBEAT_INTERVAL = 60 * 1000
let questionExplainHeartbeatTimer: ReturnType<typeof setInterval> | undefined
function startCountdown() { function startCountdown() {
document.documentElement.requestFullscreen?.().catch(() => {}) document.documentElement.requestFullscreen?.().catch(() => {})
@ -121,6 +131,11 @@ function clearCountdownTimer() {
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearCountdownTimer() clearCountdownTimer()
stopQuestionExplainHeartbeat()
})
onDeactivated(() => {
stopQuestionExplainHeartbeat()
}) })
// ======================== ======================== // ======================== ========================
@ -260,12 +275,34 @@ async function enterExplaining() {
collapseAnswerAnalysis() collapseAnswerAnalysis()
phase.value = 'explaining' phase.value = 'explaining'
currentIndex.value = 0 currentIndex.value = 0
await callExplainApi(1) const started = await callExplainApi(1)
if (started) {
startQuestionExplainHeartbeat()
}
}
function startQuestionExplainHeartbeat() {
if (questionExplainHeartbeatTimer) return
questionExplainHeartbeatTimer = setInterval(() => {
if (phase.value !== 'explaining' || !classId) {
stopQuestionExplainHeartbeat()
return
}
void taskMgrService
.questionExplainHeartbeat(classId)
.catch(() => undefined)
}, QUESTION_EXPLAIN_HEARTBEAT_INTERVAL)
}
function stopQuestionExplainHeartbeat() {
if (!questionExplainHeartbeatTimer) return
clearInterval(questionExplainHeartbeatTimer)
questionExplainHeartbeatTimer = undefined
} }
async function callExplainApi(status: number, overrideQuestionId?: string) { async function callExplainApi(status: number, overrideQuestionId?: string) {
const questionId = overrideQuestionId || getTitleKey(currentQuestion.value) const questionId = overrideQuestionId || getTitleKey(currentQuestion.value)
if (!questionId) return if (!questionId) return false
const recordId = resourceId const recordId = resourceId
try { try {
await taskMgrService.questionExplainShare({ await taskMgrService.questionExplainShare({
@ -274,8 +311,10 @@ async function callExplainApi(status: number, overrideQuestionId?: string) {
recordId, recordId,
status, status,
}) })
return true
} catch { } catch {
hud.error(status === 1 ? '开始讲解失败' : '结束讲解失败') hud.error(status === 1 ? '开始讲解失败' : '结束讲解失败')
return false
} }
} }
@ -307,6 +346,7 @@ function endExplaining() {
title: '提示', title: '提示',
zIndex: 999_999, zIndex: 999_999,
onConfirm: async () => { onConfirm: async () => {
stopQuestionExplainHeartbeat()
const lastQuestion = paperDetail.value[paperDetail.value.length - 1] const lastQuestion = paperDetail.value[paperDetail.value.length - 1]
const lastQuestionId = getTitleKey(lastQuestion) const lastQuestionId = getTitleKey(lastQuestion)
await callExplainApi(0, lastQuestionId) await callExplainApi(0, lastQuestionId)

View File

@ -34,7 +34,7 @@ export default defineConfig(async () => {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://43.136.52.196:9053', target: 'http://43.136.52.196:9053',
// target: 'https://admin.xuexiaole.com', // target: 'https://admin.lingxixue.com',
changeOrigin: true, changeOrigin: true,
}, },
}, },