Device.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <template>
  2. <div
  3. class="o-device has-padding"
  4. :class="{ 'u-pointer': isOnline }"
  5. @click="askStatus"
  6. >
  7. <div class="l-flex__none l-flex--row o-device__block o-device__header">
  8. <i
  9. class="l-flex__none o-device__status dark"
  10. :class="statusClass"
  11. />
  12. <auto-text
  13. class="l-flex__fill"
  14. :text="name"
  15. />
  16. <template v-if="isOnline">
  17. <i
  18. v-if="isShotting"
  19. class="l-flex__none el-icon-loading"
  20. />
  21. <i
  22. v-else
  23. class="l-flex__none o-device__shot"
  24. @click.stop="screenshot"
  25. />
  26. </template>
  27. <span
  28. class="l-flex__none o-device__tip"
  29. :class="statusType"
  30. >
  31. {{ statusTip }}
  32. </span>
  33. </div>
  34. <div
  35. v-if="shot"
  36. class="l-flex__fill o-device__block o-device__preview"
  37. :style="styles"
  38. />
  39. <div
  40. v-if="isActivated"
  41. class="l-flex__auto l-flex--col center o-device__block o-device__info"
  42. >
  43. <i
  44. v-if="loadingTimeline"
  45. class="l-flex__none el-icon-loading"
  46. />
  47. <template v-else>
  48. <template v-if="current">
  49. <auto-text
  50. class="l-flex__none o-device__current"
  51. :text="current.name"
  52. />
  53. <div class="l-flex__auto l-flex--row o-device__time">
  54. <span class="o-device__hms">
  55. {{ current.startTime }}
  56. <span class="o-device__ymd">{{ current.startDate }}</span>
  57. </span>
  58. <span class="o-device__line" />
  59. <span class="o-device__hms">
  60. {{ current.endTime }}
  61. <span class="o-device__ymd">{{ current.endDate }}</span>
  62. </span>
  63. </div>
  64. </template>
  65. <span
  66. v-else
  67. class="o-device__current"
  68. >
  69. 当前暂无节目
  70. </span>
  71. <auto-text
  72. v-if="next"
  73. class="l-flex__none o-device__next"
  74. :text="nextInfo"
  75. />
  76. </template>
  77. </div>
  78. <auto-text
  79. class="l-flex__none o-device__block o-device__footer"
  80. :text="address"
  81. />
  82. <el-dialog
  83. :title="name"
  84. :visible.sync="show"
  85. custom-class="c-dialog"
  86. :before-close="handleClose"
  87. append-to-body
  88. >
  89. <div class="u-text-center">
  90. <i
  91. v-if="loading"
  92. class="el-icon-loading"
  93. />
  94. <template v-else>
  95. <div class="has-bottom-padding">
  96. <button
  97. class="c-sibling-item o-button"
  98. @click="ask"
  99. >
  100. 检测状态
  101. </button>
  102. <button
  103. class="c-sibling-item o-button"
  104. @click="restart"
  105. >
  106. 重启
  107. </button>
  108. </div>
  109. <template v-if="message">
  110. <div v-if="message.type === 's'">{{ message.value }}</div>
  111. </template>
  112. </template>
  113. </div>
  114. </el-dialog>
  115. </div>
  116. </template>
  117. <script>
  118. import { getProgram } from '@/api/program'
  119. import {
  120. getSchedule,
  121. getTimeline
  122. } from '@/api/calendar'
  123. import { parseTime } from '@/utils'
  124. import {
  125. listen,
  126. unlisten,
  127. publish
  128. } from '@/utils/mqtt'
  129. const deviceCache = {}
  130. export default {
  131. name: 'DeviceCard',
  132. props: {
  133. device: {
  134. type: Object,
  135. default: null
  136. }
  137. },
  138. data () {
  139. return {
  140. show: false,
  141. loading: false,
  142. message: null,
  143. isShotting: false,
  144. shot: null,
  145. timeline: [],
  146. loadingTimeline: false,
  147. current: null,
  148. next: null
  149. }
  150. },
  151. computed: {
  152. name () {
  153. return this.device.name
  154. },
  155. isActivated () {
  156. return this.device.activate === 2
  157. },
  158. isOnline () {
  159. return this.device.onlineStatus === 1
  160. },
  161. statusClass () {
  162. return this.isActivated
  163. ? this.isOnline
  164. ? 'u-color--success'
  165. : 'u-color--error'
  166. : this.device.activate
  167. ? 'u-color--primary'
  168. : 'u-color--warning'
  169. },
  170. statusType () {
  171. return this.isActivated
  172. ? this.isOnline
  173. ? 'success'
  174. : 'error'
  175. : this.device.activate
  176. ? 'primary'
  177. : 'warning'
  178. },
  179. statusTip () {
  180. return this.isActivated
  181. ? this.isOnline
  182. ? '在线'
  183. : '离线'
  184. : this.device.activate
  185. ? '已激活'
  186. : '未激活'
  187. },
  188. address () {
  189. return `地址:${this.device.remark}`
  190. },
  191. styles () {
  192. return this.isOnline && this.shot ? {
  193. backgroundImage: `url("${this.shot}"`
  194. } : null
  195. },
  196. nextInfo () {
  197. return this.next ? `下一场:${this.next.startDate} ${this.next.startTime} ${this.next.name}` : ''
  198. }
  199. },
  200. mounted () {
  201. if (this.isActivated) {
  202. listen(this.onMessage)
  203. this.getTimeline()
  204. }
  205. this.$timer = -1
  206. },
  207. beforeDestroy () {
  208. if (this.isActivated) {
  209. unlisten(this.onMessage)
  210. if (this.show) {
  211. this.handleClose()
  212. }
  213. }
  214. clearTimeout(this.$timer)
  215. },
  216. methods: {
  217. askStatus () {
  218. if (!this.isOnline) {
  219. return
  220. }
  221. this.show = true
  222. },
  223. handleClose () {
  224. this.loading = false
  225. this.message = null
  226. this.show = false
  227. },
  228. onMessage (topic, message) {
  229. if (message) {
  230. const result = new RegExp(`${this.device.productId}/${this.device.id}/(.+)`).exec(topic)
  231. if (result) {
  232. switch (result[1]) {
  233. case 'status/reply':
  234. this.onAskReply(message)
  235. break
  236. case 'screenshot/reply':
  237. this.onScreenshotReply(message)
  238. break
  239. case 'calendar/update':
  240. this.onCalendarUpdate(message)
  241. break
  242. default:
  243. break
  244. }
  245. }
  246. }
  247. },
  248. publish (invoke) {
  249. return publish(`${this.device.productId}/${this.device.id}/${invoke}/ask`, JSON.stringify({ timestamp: Date.now() }))
  250. },
  251. ask () {
  252. this.publish('status').then(() => {
  253. this.loading = true
  254. this.message = null
  255. }, () => {
  256. this.$message({
  257. type: 'warning',
  258. message: '正在连接,请稍后再试'
  259. })
  260. })
  261. },
  262. onAskReply (message) {
  263. this.loading = false
  264. try {
  265. message = JSON.parse(message)
  266. switch (message.status) {
  267. case 1:
  268. this.message = { type: 's', value: '未播放节目,处于默认状态' }
  269. break
  270. case 2:
  271. this.message = { type: 's', value: '正在播放节目' }
  272. break
  273. case 3:
  274. this.message = { type: 's', value: '解析节目异常,请重新发布' }
  275. break
  276. default:
  277. this.message = { type: 's', value: '未知' }
  278. break
  279. }
  280. } catch {
  281. this.message = { type: 's', value: '解析异常,请重试' }
  282. }
  283. },
  284. screenshot () {
  285. this.publish('screenshot').then(() => {
  286. this.isShotting = true
  287. this.shot = null
  288. }, () => {
  289. this.$message({
  290. type: 'warning',
  291. message: '正在连接,请稍后再试'
  292. })
  293. })
  294. },
  295. onScreenshotReply (message) {
  296. this.isShotting = false
  297. this.shot = `data:image/jpeg;base64,${message.replace(/\s/g, '')}`
  298. },
  299. restart () {
  300. this.publish('restart').then(() => {
  301. this.loading = true
  302. this.message = null
  303. }, () => {
  304. this.$message({
  305. type: 'warning',
  306. message: '正在连接,请稍后再试'
  307. })
  308. })
  309. },
  310. onRestartReply () {
  311. this.loading = false
  312. },
  313. onCalendarUpdate (message) {
  314. clearTimeout(this.$timer)
  315. try {
  316. message = JSON.parse(message)
  317. this.timeline = (message.eventDetail || []).map(this.createItem)
  318. this.checkTimeline()
  319. } catch {
  320. this.getTimeline()
  321. }
  322. },
  323. createItem ({ programCalendarId, type, startTimestamp, endTimestamp }) {
  324. const startDateTime = new Date(Number(startTimestamp))
  325. const endDateTime = endTimestamp ? new Date(Number(endTimestamp)) : null
  326. return {
  327. type, startDateTime, endDateTime,
  328. id: programCalendarId,
  329. name: null,
  330. startDate: parseTime(startDateTime, '{y}.{m}.{d}'),
  331. startTime: parseTime(startDateTime, '{h}:{i}:{s}'),
  332. endDate: endDateTime ? parseTime(endDateTime, '{y}.{m}.{d}') : '',
  333. endTime: endDateTime ? parseTime(endDateTime, '{h}:{i}:{s}') : ''
  334. }
  335. },
  336. getTimeline () {
  337. this.loadingTimeline = true
  338. getTimeline(this.device.id, { custom: true }).then(({ data }) => {
  339. this.timeline = (JSON.parse(data.eventDetail) || []).map(this.createItem)
  340. this.checkTimeline()
  341. }).catch(({ isCancel }) => {
  342. if (!isCancel) {
  343. this.$timer = setTimeout(this.getTimeline, 2000)
  344. }
  345. })
  346. },
  347. checkTimeline () {
  348. this.loadingTimeline = true
  349. const now = Date.now()
  350. const current = this.timeline.findIndex(({ startDateTime, endDateTime }) => {
  351. return now >= startDateTime && (!endDateTime || now <= endDateTime)
  352. })
  353. this.current = this.timeline[current]
  354. this.next = this.current && this.timeline[current + 1]
  355. this.next && this.getName(this.next).then(name => {
  356. this.next.name = name
  357. })
  358. this.getDetail()
  359. },
  360. finishTimeline () {
  361. this.loadingTimeline = false
  362. const time = this.current ? this.current.endDateTime : this.next ? this.next.startDateTime : null
  363. clearTimeout(this.$timer)
  364. if (time) {
  365. this.$timer = setTimeout(this.checkTimeline, time - Date.now())
  366. }
  367. },
  368. getName (item) {
  369. if (item.id) {
  370. if (item.name) {
  371. return Promise.resolve(item.name)
  372. }
  373. if (deviceCache[item.id]) {
  374. return Promise.resolve(deviceCache[item.id])
  375. }
  376. return (item.type === 1 ? getProgram : getSchedule)(item.id, { custom: true }).then(({ name }) => {
  377. deviceCache[item.id] = name
  378. return name
  379. })
  380. }
  381. return Promise.resolve('未知')
  382. },
  383. getDetail () {
  384. if (this.current) {
  385. this.getName(this.current).then(name => {
  386. this.current.name = name
  387. this.finishTimeline()
  388. }).catch(({ isCancel }) => {
  389. if (!isCancel) {
  390. this.$timer = setTimeout(this.getDetail, 2000)
  391. }
  392. })
  393. } else {
  394. this.finishTimeline()
  395. }
  396. }
  397. }
  398. }
  399. </script>
  400. <style lang="scss" scoped>
  401. .o-device {
  402. display: inline-flex;
  403. flex-direction: column;
  404. // height: 220px;
  405. color: $black;
  406. line-height: 1;
  407. border-radius: $radius;
  408. background-color: #fff;
  409. background-size: contain;
  410. &__block + &__block {
  411. margin-top: $spacing;
  412. }
  413. &__header {
  414. justify-self: flex-start;
  415. height: 24px;
  416. font-size: 16px;
  417. font-weight: bold;
  418. }
  419. &__status {
  420. display: inline-block;
  421. width: 12px;
  422. height: 12px;
  423. margin-right: 6px;
  424. border-radius: 50%;
  425. background-color: currentColor;
  426. }
  427. &__shot {
  428. display: inline-block;
  429. width: 24px;
  430. height: 24px;
  431. background: url("~@/assets/icon_screenshot.png") 0 0 / 100% 100% no-repeat;
  432. }
  433. &__tip {
  434. display: inline-block;
  435. position: relative;
  436. left: 16px;
  437. padding: 2px 8px 2px 10px;
  438. margin-left: -10px;
  439. color: #fff;
  440. font-size: 12px;
  441. line-height: 1;
  442. border-radius: 9px 0 0 9px;
  443. background-color: $success--dark;
  444. &.primary {
  445. background-color: $primary;
  446. }
  447. &.error {
  448. background-color: $error;
  449. }
  450. &.warning {
  451. background: $warning;
  452. }
  453. }
  454. &__preview {
  455. padding-top: 50%;
  456. background-position: center center;
  457. background-size: contain;
  458. background-repeat: no-repeat;
  459. }
  460. &__info {
  461. justify-content: center;
  462. height: 100px;
  463. }
  464. &__current {
  465. align-self: stretch;
  466. font-size: 20px;
  467. font-weight: bold;
  468. text-align: center;
  469. }
  470. &__time {
  471. margin: $spacing 0 24px;
  472. font-size: 20px;
  473. }
  474. &__line {
  475. display: inline-block;
  476. width: 20px;
  477. margin: 0 10px;
  478. border-bottom: 1px solid currentColor;
  479. }
  480. &__hms {
  481. position: relative;
  482. font-weight: bold;
  483. }
  484. &__ymd {
  485. position: absolute;
  486. top: 100%;
  487. left: 50%;
  488. color: $gray;
  489. font-size: 12px;
  490. font-weight: normal;
  491. transform: translate(-50%, 4px);
  492. }
  493. &__next {
  494. align-self: stretch;
  495. color: $gray;
  496. font-size: 12px;
  497. text-align: center;
  498. }
  499. &__current + &__next {
  500. margin-top: 24px;
  501. }
  502. &__footer {
  503. font-size: 12px;
  504. font-weight: bold;
  505. }
  506. }
  507. .o-shot {
  508. display: inline-block;
  509. width: 480px;
  510. height: 270px;
  511. object-fit: contain;
  512. }
  513. </style>