| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616 |
- 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
- }
- })
|