feat: 直播中调用查询直播状态接口

This commit is contained in:
阿梦 2026-05-15 11:25:17 +08:00
parent 3ba6221bd2
commit efe57930aa
5 changed files with 138 additions and 7 deletions

View File

@ -1,5 +1,5 @@
VITE_WS_URL = ws://43.136.52.196:27001/device # VITE_WS_URL = ws://43.136.52.196:27001/device
# VITE_WS_URL = ws://ai.xuexiaole.com/ws/device VITE_WS_URL = ws://ai.lingxixue.com/ws/device
VITE_OSS_URL = https://lxx-web-1313840333.cos.ap-chengdu.myqcloud.com VITE_OSS_URL = https://lxx-web-1313840333.cos.ap-chengdu.myqcloud.com
VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_SERVER_URL = http://127.0.0.1:9053/api/main VITE_SERVER_URL = http://127.0.0.1:9053/api/main

View File

@ -0,0 +1,20 @@
import { get } from '../request'
export type TeacherScreenShareState = {
classId: number
ip: string
lastHeartbeatAt: number
port: number
sharing: boolean
startedAt: number
ttlSeconds: number
}
const teacherScreenShareApi = {
/** 查询当前班级老师投屏状态(直播中轮询) */
state: async (classId: string) => {
return await get<TeacherScreenShareState>('/school/teacherScreenShare/state', { classId })
},
}
export default teacherScreenShareApi

View File

@ -8,10 +8,12 @@
* 4. 30 * 4. 30
* 5. * 5.
* 6. 线 * 6. 线
* 7. 30s /school/teacherScreenShare/statesharing false
*/ */
import { ref, shallowRef } from 'vue' import { ref, shallowRef } from 'vue'
import questionExplainApi from '@/api/subject/questionExplain' import questionExplainApi from '@/api/subject/questionExplain'
import { getCache } from '@/utils' import teacherScreenShareApi from '@/api/subject/teacherScreenShare'
import { getCache, $XXL } from '@/utils'
import { userStore } from '@/stores' import { userStore } from '@/stores'
import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor' import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor'
@ -20,6 +22,9 @@ import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor'
/** 心跳发送间隔(毫秒) */ /** 心跳发送间隔(毫秒) */
const HEARTBEAT_INTERVAL = 30_000 const HEARTBEAT_INTERVAL = 30_000
/** 接收老师投屏时,查询服务端投屏状态的间隔(毫秒) */
const TEACHER_SCREEN_SHARE_STATE_POLL_INTERVAL = 30_000
/** 断线重连延迟(毫秒) */ /** 断线重连延迟(毫秒) */
const RECONNECT_DELAY = 5_000 const RECONNECT_DELAY = 5_000
@ -58,6 +63,11 @@ let reconnectAttempts = 0
let isManualClose = false let isManualClose = false
let visibilityHandler: (() => void) | null = null let visibilityHandler: (() => void) | null = null
/** 投屏状态轮询定时器(仅在收到 teacherScreenShareStart 后存在) */
let screenSharePollTimer: ReturnType<typeof setInterval> | null = null
/** 当前轮询对应的班级 id用于丢弃过期异步结果 */
let screenSharePollClassId: string | null = null
/** 外部注册的消息处理回调集合 */ /** 外部注册的消息处理回调集合 */
const messageHandlers: Set<MessageHandler> = new Set() const messageHandlers: Set<MessageHandler> = new Set()
@ -70,6 +80,100 @@ const lastMessage = shallowRef<TeacherWsMessage | null>(null)
/** 班级信息(响应式) */ /** 班级信息(响应式) */
const classInfo = ref<any>(null) const classInfo = ref<any>(null)
const TEACHER_SCREEN_SHARE_TYPES = new Set(['teacherScreenShareStart', 'teacherScreenShareStop'])
/** 兼容顶层或嵌套在 `data` 下的投屏消息 */
function resolveTeacherScreenSharePayload(data: TeacherWsMessage): TeacherWsMessage | null {
if (TEACHER_SCREEN_SHARE_TYPES.has(data.type)) return data
const inner = (data as Record<string, unknown>).data
if (
inner &&
typeof inner === 'object' &&
TEACHER_SCREEN_SHARE_TYPES.has((inner as TeacherWsMessage).type)
) {
return inner as TeacherWsMessage
}
return null
}
function parseMsgObject(msg: unknown): Record<string, unknown> | null {
if (msg && typeof msg === 'object') return msg as Record<string, unknown>
if (typeof msg === 'string') {
try {
return JSON.parse(msg) as Record<string, unknown>
} catch {
return null
}
}
return null
}
function stopTeacherScreenSharePolling() {
if (screenSharePollTimer) {
clearInterval(screenSharePollTimer)
screenSharePollTimer = null
}
screenSharePollClassId = null
}
async function fetchTeacherScreenShareStateOnce(classId: string) {
try {
const res = await teacherScreenShareApi.state(classId)
if (screenSharePollClassId !== classId) return
if (res?.sharing === false) {
$XXL.stopTeacherShareScreen()
stopTeacherScreenSharePolling()
}
} catch (err) {
console.warn('[TeacherExplain] 投屏状态查询失败:', err)
}
}
/** 仅在直播中轮询;直播结束或未直播不启动 */
function startTeacherScreenSharePolling(classId: string) {
const cid = String(classId).trim()
if (!cid) return
stopTeacherScreenSharePolling()
screenSharePollClassId = cid
screenSharePollTimer = setInterval(() => {
fetchTeacherScreenShareStateOnce(cid).catch(() => {})
}, TEACHER_SCREEN_SHARE_STATE_POLL_INTERVAL)
}
/** 老师投屏开始/结束:调原生桥接 */
function handleTeacherScreenShareMessage(data: TeacherWsMessage) {
const root = resolveTeacherScreenSharePayload(data)
if (!root) return
if (root.type === 'teacherScreenShareStart') {
const msg = parseMsgObject(root.msg)
if (!msg) return
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
const port = Number(msg.port)
if (!host || !Number.isFinite(port)) return
const u = userStore().userInfo
$XXL.startTeacherShareScreen({
host,
port,
name: String(u?.name || u?.nickName || ''),
id: String(u?.userId ?? ''),
})
const pollClassId =
msg.classId !== undefined && msg.classId !== null && String(msg.classId).trim() !== ''
? String(msg.classId)
: classInfo.value?.classId !== undefined && classInfo.value?.classId !== null
? String(classInfo.value.classId)
: ''
if (pollClassId) startTeacherScreenSharePolling(pollClassId)
return
}
if (root.type === 'teacherScreenShareStop') {
stopTeacherScreenSharePolling()
$XXL.stopTeacherShareScreen()
}
}
// ======================== 内部方法 ======================== // ======================== 内部方法 ========================
/** 获取带 token 的 WebSocket 连接地址 */ /** 获取带 token 的 WebSocket 连接地址 */
@ -196,6 +300,7 @@ function handleMessage(data: TeacherWsMessage) {
if (data.type === 'pong') return if (data.type === 'pong') return
handleClassMonitorMessage(data) handleClassMonitorMessage(data)
handleTeacherScreenShareMessage(data)
// 调用所有已注册的外部消息回调 // 调用所有已注册的外部消息回调
messageHandlers.forEach(handler => { messageHandlers.forEach(handler => {
@ -288,6 +393,7 @@ async function init() {
function close() { function close() {
isManualClose = true isManualClose = true
stopHeartbeat() stopHeartbeat()
stopTeacherScreenSharePolling()
stopClassMonitor() stopClassMonitor()
clearReconnectTimer() clearReconnectTimer()
stopVisibilityListener() stopVisibilityListener()

View File

@ -104,6 +104,11 @@ export const $XXL = {
writeError: (error: string) => packSync('writeError', error), writeError: (error: string) => packSync('writeError', error),
uninstallApp: (pkgName: string) => packSync('uninstallApp', pkgName), uninstallApp: (pkgName: string) => packSync('uninstallApp', pkgName),
openExternalURL: (url: string) => packSync('openExternalURL', url), openExternalURL: (url: string) => packSync('openExternalURL', url),
/** 老师投屏开始:参数为 JSON 字符串,与原生 `startTeacherShareScreen` 约定一致 */
startTeacherShareScreen: (config: { host: string; port: number; name: string; id: string }) =>
packSync('startTeacherShareScreen', JSON.stringify(config)),
/** 老师投屏结束 */
stopTeacherShareScreen: () => packSync('stopTeacherShareScreen'),
// openApp: (config: anyObj) => pack('openApp', config), // openApp: (config: anyObj) => pack('openApp', config),
} }

View File

@ -148,16 +148,16 @@ export default defineConfig({
open: true, open: true,
proxy: { proxy: {
'/api/campus': { '/api/campus': {
target: 'http://43.136.52.196:9053', // target: 'http://43.136.52.196:9053',
// target: 'http://test.web.xuexiaole.com', // target: 'http://test.web.xuexiaole.com',
// target: 'https://ai.xuexiaole.com', target: 'https://ai.lingxixue.com',
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace('/api/campus', '/'), rewrite: path => path.replace('/api/campus', '/'),
}, },
'/api': { '/api': {
target: 'http://43.136.52.196:9053', // target: 'http://43.136.52.196:9053',
// target: 'http://test.web.xuexiaole.com', // target: 'http://test.web.xuexiaole.com',
// target: 'https://ai.xuexiaole.com', target: 'https://ai.lingxixue.com',
changeOrigin: true, changeOrigin: true,
}, },
}, },