Device.vue 9.9 KB

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