screen.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import { defineStore } from 'pinia'
  2. import { ref, computed } from 'vue'
  3. import * as api from '@/api/screen'
  4. export const useScreenStore = defineStore('screen', () => {
  5. // 机器人状态
  6. const robotStatus = ref({
  7. batteryLevel: 85,
  8. networkStatus: 'online',
  9. workStatus: 'idle',
  10. chargeStatus: 'not_charging',
  11. faultFlag: false
  12. })
  13. // 播放方案
  14. const playPlan = ref(null)
  15. const currentMediaIndex = ref(0)
  16. const isPlaying = ref(true)
  17. // 播放方案版本号(用于检测方案是否变化)
  18. const playPlanVersion = ref('')
  19. // 待切换的新播放方案(version 变化时暂存)
  20. const pendingPlayPlan = ref(null)
  21. // 当前播放方案是否已完成至少一轮(所有素材都播放过一次)
  22. const currentPlanCycleComplete = ref(false)
  23. // 待机页模式: 'welcome' | 'playlist'
  24. const idleMode = ref('welcome')
  25. // 是否有播放方案(计算属性 + 手动覆盖)
  26. const hasPlayPlan = ref(false)
  27. // 强制模式覆盖(开发调试用)
  28. const forceMode = ref(null)
  29. // 播报状态
  30. const broadcastState = ref({
  31. broadcasting: false,
  32. title: '',
  33. content: '',
  34. startTime: null,
  35. endTime: null
  36. })
  37. // 语音指令
  38. const latestCommand = ref(null)
  39. // 全局提示
  40. const globalAlert = ref({
  41. show: false,
  42. type: 'info',
  43. title: '',
  44. message: '',
  45. duration: 3000
  46. })
  47. // 音量
  48. const volume = ref(80)
  49. const muted = ref(false)
  50. // 屏幕配置
  51. const screenConfig = ref({
  52. robotName: '智能迎宾机器人',
  53. logoUrl: '',
  54. idleTimeout: 60,
  55. theme: 'default'
  56. })
  57. // 待机页主题配置
  58. const screenTheme = ref(null)
  59. // 计算属性
  60. const isIdle = computed(() => robotStatus.value.workStatus === 'idle')
  61. const isCharging = computed(() => robotStatus.value.chargeStatus === 'charging')
  62. const hasFault = computed(() => robotStatus.value.faultFlag)
  63. const networkOk = computed(() => robotStatus.value.networkStatus === 'online')
  64. const currentMedia = computed(() => {
  65. if (!playPlan.value || !playPlan.value.items || playPlan.value.items.length === 0) {
  66. return null
  67. }
  68. return playPlan.value.items[currentMediaIndex.value] || null
  69. })
  70. // 实际的待机页展示模式
  71. const actualIdleMode = computed(() => {
  72. if (forceMode.value !== null) {
  73. return forceMode.value
  74. }
  75. return hasPlayPlan.value ? 'playlist' : 'welcome'
  76. })
  77. // 方法
  78. async function fetchScreenConfig() {
  79. try {
  80. const res = await api.getScreenConfig()
  81. const data = res && res.data !== undefined ? res.data : res
  82. screenConfig.value = data
  83. } catch (e) {
  84. console.error('Failed to fetch screen config:', e)
  85. }
  86. }
  87. async function fetchScreenTheme() {
  88. try {
  89. const res = await api.getScreenTheme()
  90. const data = res && res.data !== undefined ? res.data : res
  91. screenTheme.value = data
  92. } catch (e) {
  93. console.error('Failed to fetch screen theme:', e)
  94. }
  95. }
  96. async function fetchRobotStatus() {
  97. try {
  98. const res = await api.getRobotStatus()
  99. const data = res && res.data !== undefined ? res.data : res
  100. robotStatus.value = data
  101. } catch (e) {
  102. console.error('Failed to fetch robot status:', e)
  103. }
  104. }
  105. async function fetchPlayPlan() {
  106. try {
  107. const res = await api.getPlayPlan()
  108. // 处理 RuoYi 风格的响应结构 { msg, code, data }
  109. const data = res && res.data !== undefined ? res.data : res
  110. // 检查版本是否变化(支持 version 字段或不带 version 的兼容处理)
  111. const newVersion = data?.version || ''
  112. const hasVersionField = data && 'version' in data
  113. // 如果是首次加载(playPlanVersion 为空),直接应用方案
  114. if (!playPlanVersion.value) {
  115. playPlan.value = data
  116. playPlanVersion.value = newVersion
  117. pendingPlayPlan.value = null
  118. currentMediaIndex.value = 0
  119. hasPlayPlan.value = !!(
  120. data &&
  121. data.enabled !== false &&
  122. Array.isArray(data.items) &&
  123. data.items.length > 0
  124. )
  125. console.log('[Store] 首次加载播放方案', hasPlayPlan.value ? '有播放方案' : '无播放方案')
  126. return
  127. }
  128. // 后续轮询:检查是否有 version 字段
  129. if (!hasVersionField) {
  130. // 接口没有 version 字段,保持当前播放进度不变
  131. console.log('[Store] 接口无 version 字段,保持当前播放状态')
  132. return
  133. }
  134. // 有 version 字段时,进行版本对比
  135. const isVersionChanged = newVersion && playPlanVersion.value !== newVersion
  136. if (isVersionChanged) {
  137. // 版本变化,暂存新方案
  138. pendingPlayPlan.value = data
  139. console.log('[Store] 播放方案版本变化,暂存待切换方案:', newVersion)
  140. } else {
  141. // 版本未变化,保持当前播放进度不变
  142. console.log('[Store] 播放方案版本未变化,保持当前播放进度')
  143. }
  144. } catch (e) {
  145. console.error('[Store] 播放方案加载失败:', e)
  146. playPlan.value = null
  147. pendingPlayPlan.value = null
  148. hasPlayPlan.value = false
  149. }
  150. }
  151. /**
  152. * 切换到待生效的新播放方案
  153. * 在当前方案完成一轮后调用
  154. */
  155. function applyPendingPlayPlan() {
  156. if (pendingPlayPlan.value) {
  157. console.log('[Store] 应用待切换的播放方案')
  158. playPlan.value = pendingPlayPlan.value
  159. playPlanVersion.value = pendingPlayPlan.value?.version || ''
  160. pendingPlayPlan.value = null
  161. currentMediaIndex.value = 0
  162. currentPlanCycleComplete.value = false
  163. hasPlayPlan.value = !!(
  164. playPlan.value &&
  165. playPlan.value.enabled !== false &&
  166. Array.isArray(playPlan.value.items) &&
  167. playPlan.value.items.length > 0
  168. )
  169. }
  170. }
  171. /**
  172. * 标记当前方案已完成一轮播放
  173. */
  174. function markCurrentPlanCycleComplete() {
  175. if (!currentPlanCycleComplete.value && pendingPlayPlan.value) {
  176. currentPlanCycleComplete.value = true
  177. console.log('[Store] 当前方案已完成一轮,可切换新方案')
  178. }
  179. }
  180. /**
  181. * 丢弃待切换的播放方案(当原方案被取消时)
  182. */
  183. function discardPendingPlayPlan() {
  184. if (pendingPlayPlan.value) {
  185. console.log('[Store] 丢弃待切换的播放方案')
  186. pendingPlayPlan.value = null
  187. }
  188. }
  189. async function fetchBroadcastState() {
  190. try {
  191. const res = await api.getBroadcastState()
  192. const data = res && res.data !== undefined ? res.data : res
  193. broadcastState.value = data
  194. } catch (e) {
  195. console.error('Failed to fetch broadcast state:', e)
  196. }
  197. }
  198. async function fetchLatestCommand() {
  199. try {
  200. const res = await api.getLatestCommand()
  201. if (res && res.commandId !== latestCommand.value?.commandId) {
  202. latestCommand.value = res
  203. return res
  204. }
  205. } catch (e) {
  206. console.error('Failed to fetch latest command:', e)
  207. }
  208. return null
  209. }
  210. async function ackCommand(commandId) {
  211. try {
  212. await api.ackCommand(commandId)
  213. } catch (e) {
  214. console.error('Failed to ack command:', e)
  215. }
  216. }
  217. function nextMedia() {
  218. if (playPlan.value && playPlan.value.items && playPlan.value.items.length > 0) {
  219. currentMediaIndex.value = (currentMediaIndex.value + 1) % playPlan.value.items.length
  220. }
  221. }
  222. function showAlert(options) {
  223. const { type = 'info', title = '', message = '', duration = 3000 } = options
  224. globalAlert.value = { show: true, type, title, message, duration }
  225. if (duration > 0) {
  226. setTimeout(() => {
  227. globalAlert.value.show = false
  228. }, duration)
  229. }
  230. }
  231. function hideAlert() {
  232. globalAlert.value.show = false
  233. }
  234. function setVolume(val) {
  235. volume.value = Math.max(0, Math.min(100, val))
  236. }
  237. function toggleMute() {
  238. muted.value = !muted.value
  239. }
  240. function pausePlay() {
  241. isPlaying.value = false
  242. }
  243. function resumePlay() {
  244. isPlaying.value = true
  245. }
  246. function resetToIdle() {
  247. robotStatus.value.workStatus = 'idle'
  248. }
  249. // 切换待机页模式(开发调试用)
  250. function toggleIdleMode() {
  251. forceMode.value = actualIdleMode.value === 'welcome' ? 'playlist' : 'welcome'
  252. }
  253. // 重置强制模式
  254. function resetForceMode() {
  255. forceMode.value = null
  256. }
  257. return {
  258. // 状态
  259. robotStatus,
  260. playPlan,
  261. currentMediaIndex,
  262. isPlaying,
  263. playPlanVersion,
  264. pendingPlayPlan,
  265. currentPlanCycleComplete,
  266. broadcastState,
  267. latestCommand,
  268. globalAlert,
  269. volume,
  270. muted,
  271. screenConfig,
  272. screenTheme,
  273. idleMode,
  274. hasPlayPlan,
  275. forceMode,
  276. // 计算属性
  277. isIdle,
  278. isCharging,
  279. hasFault,
  280. networkOk,
  281. currentMedia,
  282. actualIdleMode,
  283. // 方法
  284. fetchScreenConfig,
  285. fetchScreenTheme,
  286. fetchRobotStatus,
  287. fetchPlayPlan,
  288. applyPendingPlayPlan,
  289. discardPendingPlayPlan,
  290. markCurrentPlanCycleComplete,
  291. fetchBroadcastState,
  292. fetchLatestCommand,
  293. ackCommand,
  294. nextMedia,
  295. showAlert,
  296. hideAlert,
  297. setVolume,
  298. toggleMute,
  299. pausePlay,
  300. resumePlay,
  301. resetToIdle,
  302. toggleIdleMode,
  303. resetForceMode
  304. }
  305. })