import { defineStore } from 'pinia' import { ref, computed } from 'vue' import * as api from '@/api/screen' export const useScreenStore = defineStore('screen', () => { // 机器人状态 const robotStatus = ref({ batteryLevel: 85, networkStatus: 'online', workStatus: 'idle', chargeStatus: 'not_charging', faultFlag: false }) // 播放方案 const playPlan = ref(null) const currentMediaIndex = ref(0) const isPlaying = ref(true) // 播放方案版本号(用于检测方案是否变化) const playPlanVersion = ref('') // 待切换的新播放方案(version 变化时暂存) const pendingPlayPlan = ref(null) // 当前播放方案是否已完成至少一轮(所有素材都播放过一次) const currentPlanCycleComplete = ref(false) // 待机页模式: 'welcome' | 'playlist' const idleMode = ref('welcome') // 是否有播放方案(计算属性 + 手动覆盖) const hasPlayPlan = ref(false) // 强制模式覆盖(开发调试用) const forceMode = ref(null) // 播报状态 const broadcastState = ref({ broadcasting: false, title: '', content: '', startTime: null, endTime: null }) // 当前播报状态(新版,用于播报插播) const currentBroadcast = ref({ broadcasting: false, taskId: null, contentId: null, taskName: '', title: '', content: '', contentType: '', level: 'normal', audioUrl: '', audioDuration: null, playMode: 'once', startTime: null, estimatedEndTime: null, version: '' }) // 播报轮询定时器 const broadcastPollingTimer = ref(null) // 播放方案轮询定时器(降级方案) const playPlanPollingTimer = ref(null) // WebSocket 连接状态 const wsConnected = ref(false) // 是否使用 WebSocket 模式 const useWebSocketMode = ref(true) // 是否正在播放播报音频 const isBroadcastAudioPlaying = ref(false) // 语音指令 const latestCommand = ref(null) // 全局提示 const globalAlert = ref({ show: false, type: 'info', title: '', message: '', duration: 3000 }) // 音量 const volume = ref(80) const muted = ref(false) // 屏幕配置 const screenConfig = ref({ robotName: '智能迎宾机器人', logoUrl: '', idleTimeout: 60, theme: 'default' }) // 待机页主题配置 const screenTheme = ref(null) // 计算属性 const isIdle = computed(() => robotStatus.value.workStatus === 'idle') const isCharging = computed(() => robotStatus.value.chargeStatus === 'charging') const hasFault = computed(() => robotStatus.value.faultFlag) const networkOk = computed(() => robotStatus.value.networkStatus === 'online') // 是否正在播报(有播报状态、audioUrl 有值) const isBroadcasting = computed(() => { const b = currentBroadcast.value if (!b || b.broadcasting !== true || !b.audioUrl) return false return true }) const currentMedia = computed(() => { if (!playPlan.value || !playPlan.value.items || playPlan.value.items.length === 0) { return null } return playPlan.value.items[currentMediaIndex.value] || null }) // 实际的待机页展示模式 const actualIdleMode = computed(() => { if (forceMode.value !== null) { return forceMode.value } return hasPlayPlan.value ? 'playlist' : 'welcome' }) // 方法 async function fetchScreenConfig() { try { const res = await api.getScreenConfig() const data = res && res.data !== undefined ? res.data : res screenConfig.value = data } catch (e) { console.error('Failed to fetch screen config:', e) } } async function fetchScreenTheme() { try { const res = await api.getScreenTheme() const data = res && res.data !== undefined ? res.data : res screenTheme.value = data } catch (e) { console.error('Failed to fetch screen theme:', e) } } async function fetchRobotStatus() { try { const res = await api.getRobotStatus() const data = res && res.data !== undefined ? res.data : res robotStatus.value = data } catch (e) { console.error('Failed to fetch robot status:', e) } } async function fetchPlayPlan() { try { const res = await api.getPlayPlan() // 处理 RuoYi 风格的响应结构 { msg, code, data } const data = res && res.data !== undefined ? res.data : res // 检查版本是否变化(支持 version 字段或不带 version 的兼容处理) const newVersion = data?.version || '' const hasVersionField = data && 'version' in data // 如果是首次加载(playPlanVersion 为空),直接应用方案 if (!playPlanVersion.value) { playPlan.value = data playPlanVersion.value = newVersion pendingPlayPlan.value = null currentMediaIndex.value = 0 hasPlayPlan.value = !!( data && data.enabled !== false && Array.isArray(data.items) && data.items.length > 0 ) console.log('[Store] 首次加载播放方案', hasPlayPlan.value ? '有播放方案' : '无播放方案') return } // 后续轮询:检查是否有 version 字段 if (!hasVersionField) { // 接口没有 version 字段,保持当前播放进度不变 console.log('[Store] 接口无 version 字段,保持当前播放状态') return } // 有 version 字段时,进行版本对比 const isVersionChanged = newVersion && playPlanVersion.value !== newVersion if (isVersionChanged) { // 版本变化,暂存新方案 pendingPlayPlan.value = data console.log('[Store] 播放方案版本变化,暂存待切换方案:', newVersion) } else { // 版本未变化,保持当前播放进度不变 console.log('[Store] 播放方案版本未变化,保持当前播放进度') } } catch (e) { console.error('[Store] 播放方案加载失败:', e) playPlan.value = null pendingPlayPlan.value = null hasPlayPlan.value = false } } /** * 切换到待生效的新播放方案 * 在当前方案完成一轮后调用 */ function applyPendingPlayPlan() { if (pendingPlayPlan.value) { console.log('[Store] 应用待切换的播放方案') playPlan.value = pendingPlayPlan.value playPlanVersion.value = pendingPlayPlan.value?.version || '' pendingPlayPlan.value = null currentMediaIndex.value = 0 currentPlanCycleComplete.value = false hasPlayPlan.value = !!( playPlan.value && playPlan.value.enabled !== false && Array.isArray(playPlan.value.items) && playPlan.value.items.length > 0 ) } } /** * 标记当前方案已完成一轮播放 */ function markCurrentPlanCycleComplete() { if (!currentPlanCycleComplete.value && pendingPlayPlan.value) { currentPlanCycleComplete.value = true console.log('[Store] 当前方案已完成一轮,可切换新方案') } } /** * 丢弃待切换的播放方案(当原方案被取消时) */ function discardPendingPlayPlan() { if (pendingPlayPlan.value) { console.log('[Store] 丢弃待切换的播放方案') pendingPlayPlan.value = null } } // ============================================ // WebSocket 驱动方法 // ============================================ /** * 处理 WebSocket 推送的播放方案变化 * 由 websocket store 调用 */ function handlePlayPlanChanged(data) { console.log('[Store] WebSocket 收到播放方案变化:', data) if (!data || data.enabled === false) { // 方案禁用 handlePlayPlanDisabled() return } const newVersion = data?.version || '' // 如果是首次加载,直接应用方案 if (!playPlanVersion.value) { playPlan.value = data playPlanVersion.value = newVersion pendingPlayPlan.value = null currentMediaIndex.value = 0 hasPlayPlan.value = !!( data && data.enabled !== false && Array.isArray(data.items) && data.items.length > 0 ) console.log('[Store] WebSocket 首次加载播放方案') return } // 后续更新:检查版本是否变化 const isVersionChanged = newVersion && playPlanVersion.value !== newVersion if (isVersionChanged) { // 版本变化,暂存新方案 pendingPlayPlan.value = data console.log('[Store] WebSocket 播放方案版本变化,暂存待切换方案:', newVersion) } } /** * 处理 WebSocket 推送的播放方案禁用 */ function handlePlayPlanDisabled() { console.log('[Store] WebSocket 收到播放方案禁用') pendingPlayPlan.value = null playPlan.value = null playPlanVersion.value = '' hasPlayPlan.value = false currentMediaIndex.value = 0 } /** * 处理 WebSocket 推送的播报开始 */ function handleBroadcastStarted(data) { console.log('[Store] WebSocket 收到播报开始:', data) if (!data) return currentBroadcast.value = data } /** * 处理 WebSocket 推送的播报结束 */ function handleBroadcastEnded() { console.log('[Store] WebSocket 收到播报结束') currentBroadcast.value = emptyBroadcast() } /** * 设置 WebSocket 连接状态 */ function setWsConnected(connected) { wsConnected.value = connected console.log('[Store] WebSocket 连接状态:', connected ? '已连接' : '已断开') } /** * 开始播放方案轮询(降级方案) * 当 WebSocket 不可用时启用 */ function startPlayPlanPolling() { if (playPlanPollingTimer.value) return // 立即请求一次 fetchPlayPlan() // 每 30 秒轮询一次(降级方案,频率较低) playPlanPollingTimer.value = setInterval(() => { fetchPlayPlan() }, 30000) } /** * 停止播放方案轮询 */ function stopPlayPlanPolling() { if (playPlanPollingTimer.value) { clearInterval(playPlanPollingTimer.value) playPlanPollingTimer.value = null } } async function fetchBroadcastState() { try { const res = await api.getBroadcastState() const data = res && res.data !== undefined ? res.data : res broadcastState.value = data } catch (e) { console.error('Failed to fetch broadcast state:', e) } } /** * 生成播报唯一 key * 使用时间戳确保每次推送都是新的 key,配合后端频率控制 */ function getBroadcastKey(broadcast) { if (!broadcast) return '' return [ broadcast.taskId || '', broadcast.contentId || '', Date.now() // 使用时间戳让每次都是新的 key ].join('_') } /** * 获取空的播报状态对象 */ function emptyBroadcast() { return { broadcasting: false, taskId: null, contentId: null, taskName: '', title: '', content: '', contentType: '', level: 'normal', audioUrl: '', audioDuration: null, playMode: 'once', startTime: null, estimatedEndTime: null, version: '' } } /** * 获取当前播报状态(播报插播专用) */ async function fetchCurrentBroadcast() { try { const res = await api.getCurrentBroadcast() const data = res && res.data !== undefined ? res.data : res // 音频正在播放时,不清空当前播报状态 if (isBroadcastAudioPlaying.value) { console.log('[Broadcast] 音频正在播放,忽略后端的 broadcasting=false 状态') return } currentBroadcast.value = data || emptyBroadcast() console.log('[Broadcast] 播报状态更新:', data) } catch (e) { console.warn('[Store] 获取当前播报状态失败:', e) } } /** * 开始播报状态轮询(每 2 秒一次) */ function startBroadcastPolling() { // 避免重复启动 if (broadcastPollingTimer.value) { return } // 立即请求一次 fetchCurrentBroadcast() // 每 2 秒轮询一次 broadcastPollingTimer.value = setInterval(() => { fetchCurrentBroadcast() }, 2000) } /** * 停止播报状态轮询 */ function stopBroadcastPolling() { if (broadcastPollingTimer.value) { clearInterval(broadcastPollingTimer.value) broadcastPollingTimer.value = null } } /** * 设置播报音频播放状态 */ function setBroadcastAudioPlaying(value) { isBroadcastAudioPlaying.value = value } /** * 本地标记播报结束(用于 audio ended/error 后隐藏浮层) */ function markBroadcastFinishedLocally() { console.log('[Broadcast] 标记播报为已完成') currentBroadcast.value = emptyBroadcast() } async function fetchLatestCommand() { try { const res = await api.getLatestCommand() if (res && res.commandId !== latestCommand.value?.commandId) { latestCommand.value = res return res } } catch (e) { console.error('Failed to fetch latest command:', e) } return null } async function ackCommand(commandId) { try { await api.ackCommand(commandId) } catch (e) { console.error('Failed to ack command:', e) } } function nextMedia() { if (playPlan.value && playPlan.value.items && playPlan.value.items.length > 0) { currentMediaIndex.value = (currentMediaIndex.value + 1) % playPlan.value.items.length } } function showAlert(options) { const { type = 'info', title = '', message = '', duration = 3000 } = options globalAlert.value = { show: true, type, title, message, duration } if (duration > 0) { setTimeout(() => { globalAlert.value.show = false }, duration) } } function hideAlert() { globalAlert.value.show = false } function setVolume(val) { volume.value = Math.max(0, Math.min(100, val)) } function toggleMute() { muted.value = !muted.value } function pausePlay() { isPlaying.value = false } function resumePlay() { isPlaying.value = true } function resetToIdle() { robotStatus.value.workStatus = 'idle' } // 切换待机页模式(开发调试用) function toggleIdleMode() { forceMode.value = actualIdleMode.value === 'welcome' ? 'playlist' : 'welcome' } // 重置强制模式 function resetForceMode() { forceMode.value = null } return { // 状态 robotStatus, playPlan, currentMediaIndex, isPlaying, playPlanVersion, pendingPlayPlan, currentPlanCycleComplete, broadcastState, currentBroadcast, broadcastPollingTimer, playPlanPollingTimer, wsConnected, useWebSocketMode, isBroadcastAudioPlaying, latestCommand, globalAlert, volume, muted, screenConfig, screenTheme, idleMode, hasPlayPlan, forceMode, // 计算属性 isIdle, isCharging, hasFault, networkOk, currentMedia, actualIdleMode, isBroadcasting, getBroadcastKey, // 方法 fetchScreenConfig, fetchScreenTheme, fetchRobotStatus, fetchPlayPlan, applyPendingPlayPlan, discardPendingPlayPlan, markCurrentPlanCycleComplete, fetchBroadcastState, fetchCurrentBroadcast, startBroadcastPolling, stopBroadcastPolling, setBroadcastAudioPlaying, markBroadcastFinishedLocally, emptyBroadcast, fetchLatestCommand, ackCommand, nextMedia, showAlert, hideAlert, setVolume, toggleMute, pausePlay, resumePlay, resetToIdle, toggleIdleMode, resetForceMode, // WebSocket 驱动方法 handlePlayPlanChanged, handlePlayPlanDisabled, handleBroadcastStarted, handleBroadcastEnded, setWsConnected, startPlayPlanPolling, stopPlayPlanPolling } })