Device.vue 9.9 KB

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