Device.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <template>
  2. <div
  3. class="o-device has-padding u-pointer"
  4. @click="onClick"
  5. >
  6. <div class="l-flex__none l-flex--row o-device__block o-device__header">
  7. <i
  8. class="l-flex__none o-device__status"
  9. :class="statusClass"
  10. />
  11. <auto-text
  12. class="l-flex__fill"
  13. :text="name"
  14. />
  15. <template v-if="isActivated && isOnline">
  16. <i
  17. v-if="isShotting"
  18. class="l-flex__none el-icon-loading"
  19. @click.stop
  20. />
  21. <i
  22. v-else
  23. class="l-flex__none o-device__shot"
  24. @click.stop="screenshot"
  25. />
  26. </template>
  27. <div
  28. class="l-flex__none o-device__tip"
  29. :class="statusClass"
  30. >
  31. <span class="u-color--white">{{ statusTip }}</span>
  32. </div>
  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. </div>
  83. </template>
  84. <script>
  85. import { getTimeline } from '@/api/calendar'
  86. import { parseTime } from '@/utils'
  87. import {
  88. listen,
  89. unlisten
  90. } from '@/utils/mqtt'
  91. import { getName } from '@/utils/cache'
  92. import {
  93. getAndCheck,
  94. screenshot,
  95. reset,
  96. stop
  97. } from '@/utils/screenshot'
  98. export default {
  99. name: 'DeviceCard',
  100. props: {
  101. device: {
  102. type: Object,
  103. default: null
  104. }
  105. },
  106. data () {
  107. return {
  108. isShotting: false,
  109. shot: null,
  110. timeline: [],
  111. loadingTimeline: false,
  112. current: null,
  113. next: null
  114. }
  115. },
  116. computed: {
  117. name () {
  118. return this.device.name
  119. },
  120. isActivated () {
  121. return this.device.activate === 2
  122. },
  123. isOnline () {
  124. return this.device.onlineStatus === 1
  125. },
  126. statusClass () {
  127. return this.isActivated
  128. ? this.isOnline
  129. ? 'u-color--success dark'
  130. : 'u-color--error light'
  131. : this.device.activate
  132. ? 'u-color--primary'
  133. : 'u-color--warning'
  134. },
  135. statusTip () {
  136. return this.isActivated
  137. ? this.isOnline
  138. ? '在线'
  139. : '离线'
  140. : this.device.activate
  141. ? '已激活'
  142. : '未激活'
  143. },
  144. address () {
  145. return `地址:${this.device.remark}`
  146. },
  147. styles () {
  148. return this.isActivated && this.isOnline && this.shot ? {
  149. backgroundImage: `url("${this.shot}")`
  150. } : null
  151. },
  152. nextInfo () {
  153. return this.next ? `下一场:${this.next.startDate} ${this.next.startTime} ${this.next.name}` : ''
  154. }
  155. },
  156. created () {
  157. if (this.isActivated) {
  158. listen(this.onMessage)
  159. this.getTimeline()
  160. if (this.isOnline) {
  161. getAndCheck(this.device, this.onScreenshotUpdate)
  162. } else {
  163. reset(this.device.id)
  164. }
  165. }
  166. this.$timer = -1
  167. },
  168. beforeDestroy () {
  169. if (this.isActivated) {
  170. unlisten(this.onMessage)
  171. if (this.isOnline) {
  172. stop(this.device.id)
  173. }
  174. }
  175. clearTimeout(this.$timer)
  176. },
  177. methods: {
  178. screenshot () {
  179. screenshot(this.device.id)
  180. },
  181. onScreenshotUpdate ({ waiting, base64 }) {
  182. this.isShotting = waiting
  183. this.shot = waiting ? null : base64
  184. },
  185. onClick () {
  186. this.$router.push({
  187. name: 'device-detail',
  188. params: { id: this.device.id }
  189. })
  190. },
  191. onMessage (topic, message) {
  192. if (message) {
  193. const result = new RegExp(`${this.device.productId}/${this.device.id}/(.+)`).exec(topic)
  194. if (result) {
  195. switch (result[1]) {
  196. case 'calendar/update':
  197. this.onCalendarUpdate(message)
  198. break
  199. default:
  200. break
  201. }
  202. }
  203. }
  204. },
  205. onCalendarUpdate (message) {
  206. clearTimeout(this.$timer)
  207. try {
  208. message = JSON.parse(message)
  209. this.timeline = (message.eventDetail || []).map(this.createItem)
  210. this.checkTimeline()
  211. } catch {
  212. this.getTimeline()
  213. }
  214. },
  215. createItem ({ programCalendarId, type, startTimestamp, endTimestamp }) {
  216. const startDateTime = new Date(Number(startTimestamp))
  217. const endDateTime = endTimestamp ? new Date(Number(endTimestamp)) : null
  218. return {
  219. type, startDateTime, endDateTime,
  220. id: programCalendarId,
  221. name: null,
  222. startDate: parseTime(startDateTime, '{y}.{m}.{d}'),
  223. startTime: parseTime(startDateTime, '{h}:{i}:{s}'),
  224. endDate: endDateTime ? parseTime(endDateTime - 1000, '{y}.{m}.{d}') : '',
  225. endTime: endDateTime ? parseTime(endDateTime - 1000, '{h}:{i}:{s}') : ''
  226. }
  227. },
  228. getTimeline () {
  229. this.loadingTimeline = true
  230. getTimeline(this.device.id, { custom: true }).then(({ data }) => {
  231. this.timeline = (JSON.parse(data.eventDetail) || []).map(this.createItem)
  232. this.checkTimeline()
  233. }).catch(({ isCancel }) => {
  234. if (!isCancel) {
  235. this.$timer = setTimeout(this.getTimeline, 2000)
  236. }
  237. })
  238. },
  239. checkTimeline () {
  240. this.loadingTimeline = true
  241. const now = Date.now()
  242. const current = this.timeline.findIndex(({ startDateTime, endDateTime }) => {
  243. return now >= startDateTime && (!endDateTime || now <= endDateTime)
  244. })
  245. this.current = this.timeline[current]
  246. this.next = this.current && this.timeline[current + 1]
  247. this.next && this.getName(this.next).then(name => {
  248. this.next.name = name
  249. })
  250. this.getDetail()
  251. },
  252. finishTimeline () {
  253. this.loadingTimeline = false
  254. const time = this.current ? this.current.endDateTime : this.next ? this.next.startDateTime : null
  255. clearTimeout(this.$timer)
  256. if (time) {
  257. this.$timer = setTimeout(this.checkTimeline, time - Date.now())
  258. }
  259. },
  260. getName (item) {
  261. if (item.id) {
  262. if (item.name) {
  263. return Promise.resolve(item.name)
  264. }
  265. return getName(item.type, item.id)
  266. }
  267. return Promise.resolve('未知')
  268. },
  269. getDetail () {
  270. if (this.current) {
  271. this.getName(this.current).then(name => {
  272. this.current.name = name
  273. this.finishTimeline()
  274. }, ({ isCancel }) => {
  275. if (!isCancel) {
  276. if (this.current.count == null) {
  277. this.current.count = 1
  278. } else if (this.current.count < 3) {
  279. this.current.count += 1
  280. } else {
  281. this.current.name = '未知'
  282. this.finishTimeline()
  283. return
  284. }
  285. this.$timer = setTimeout(this.getDetail, 2000)
  286. }
  287. })
  288. } else {
  289. this.finishTimeline()
  290. }
  291. }
  292. }
  293. }
  294. </script>
  295. <style lang="scss" scoped>
  296. .o-device {
  297. display: inline-flex;
  298. flex-direction: column;
  299. color: $black;
  300. line-height: 1;
  301. border-radius: $radius;
  302. background-color: #fff;
  303. &__block + &__block {
  304. margin-top: $spacing;
  305. }
  306. &__header {
  307. justify-self: flex-start;
  308. height: 24px;
  309. font-size: 16px;
  310. font-weight: bold;
  311. }
  312. &__status {
  313. display: inline-block;
  314. width: 12px;
  315. height: 12px;
  316. margin-right: 6px;
  317. border-radius: 50%;
  318. background-color: currentColor;
  319. }
  320. &__shot {
  321. display: inline-block;
  322. width: 24px;
  323. height: 24px;
  324. background: url("~@/assets/icon_screenshot.png") 0 0 / 100% 100% no-repeat;
  325. }
  326. &__tip {
  327. display: inline-block;
  328. position: relative;
  329. left: 16px;
  330. padding: 2px 8px 2px 10px;
  331. margin-left: -10px;
  332. font-size: 12px;
  333. line-height: 1;
  334. border-radius: 9px 0 0 9px;
  335. background-color: currentColor;
  336. }
  337. &__preview {
  338. padding-top: 50%;
  339. background-position: center center;
  340. background-size: contain;
  341. background-repeat: no-repeat;
  342. }
  343. &__info {
  344. justify-content: center;
  345. height: 100px;
  346. }
  347. &__current {
  348. align-self: stretch;
  349. font-size: 20px;
  350. font-weight: bold;
  351. text-align: center;
  352. }
  353. &__time {
  354. margin: $spacing 0 24px;
  355. font-size: 20px;
  356. }
  357. &__line {
  358. display: inline-block;
  359. width: 20px;
  360. margin: 0 10px;
  361. border-bottom: 1px solid currentColor;
  362. }
  363. &__hms {
  364. position: relative;
  365. font-weight: bold;
  366. }
  367. &__ymd {
  368. position: absolute;
  369. top: 100%;
  370. left: 50%;
  371. color: $gray;
  372. font-size: 12px;
  373. font-weight: normal;
  374. transform: translate(-50%, 4px);
  375. }
  376. &__next {
  377. align-self: stretch;
  378. color: $gray;
  379. font-size: 12px;
  380. text-align: center;
  381. }
  382. &__current + &__next {
  383. margin-top: 24px;
  384. }
  385. &__footer {
  386. font-size: 12px;
  387. font-weight: bold;
  388. }
  389. }
  390. .o-shot {
  391. display: inline-block;
  392. width: 480px;
  393. height: 270px;
  394. object-fit: contain;
  395. }
  396. </style>