| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- <template>
- <div
- class="o-device has-padding"
- :class="{ 'u-pointer': isOnline }"
- @click="askStatus"
- >
- <div class="l-flex__none l-flex--row o-device__block o-device__header">
- <i
- class="l-flex__none o-device__status dark"
- :class="statusClass"
- />
- <auto-text
- class="l-flex__fill"
- :text="name"
- />
- <template v-if="isOnline">
- <i
- v-if="isShotting"
- class="l-flex__none el-icon-loading"
- />
- <i
- v-else
- class="l-flex__none o-device__shot"
- @click.stop="screenshot"
- />
- </template>
- <span
- class="l-flex__none o-device__tip"
- :class="statusType"
- >
- {{ statusTip }}
- </span>
- </div>
- <div
- v-if="shot"
- class="l-flex__fill o-device__block o-device__preview"
- :style="styles"
- />
- <div
- v-if="isActivated"
- class="l-flex__auto l-flex--col center o-device__block o-device__info"
- >
- <i
- v-if="loadingTimeline"
- class="l-flex__none el-icon-loading"
- />
- <template v-else>
- <template v-if="current">
- <auto-text
- class="l-flex__none o-device__current"
- :text="current.name"
- />
- <div class="l-flex__auto l-flex--row o-device__time">
- <span class="o-device__hms">
- {{ current.startTime }}
- <span class="o-device__ymd">{{ current.startDate }}</span>
- </span>
- <span class="o-device__line" />
- <span class="o-device__hms">
- {{ current.endTime }}
- <span class="o-device__ymd">{{ current.endDate }}</span>
- </span>
- </div>
- </template>
- <span
- v-else
- class="o-device__current"
- >
- 当前暂无节目
- </span>
- <auto-text
- v-if="next"
- class="l-flex__none o-device__next"
- :text="nextInfo"
- />
- </template>
- </div>
- <auto-text
- class="l-flex__none o-device__block o-device__footer"
- :text="address"
- />
- <el-dialog
- :title="name"
- :visible.sync="show"
- custom-class="c-dialog"
- :before-close="handleClose"
- append-to-body
- >
- <div class="u-text-center">
- <i
- v-if="loading"
- class="el-icon-loading"
- />
- <template v-else>
- <div class="has-bottom-padding">
- <button
- class="c-sibling-item o-button"
- @click="ask"
- >
- 检测状态
- </button>
- <button
- class="c-sibling-item o-button"
- @click="restart"
- >
- 重启
- </button>
- </div>
- <template v-if="message">
- <div v-if="message.type === 's'">{{ message.value }}</div>
- </template>
- </template>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { getProgram } from '@/api/program'
- import {
- getSchedule,
- getTimeline
- } from '@/api/calendar'
- import { parseTime } from '@/utils'
- import {
- listen,
- unlisten,
- publish
- } from '@/utils/mqtt'
- const deviceCache = {}
- export default {
- name: 'DeviceCard',
- props: {
- device: {
- type: Object,
- default: null
- }
- },
- data () {
- return {
- show: false,
- loading: false,
- message: null,
- isShotting: false,
- shot: null,
- timeline: [],
- loadingTimeline: false,
- current: null,
- next: null
- }
- },
- computed: {
- name () {
- return this.device.name
- },
- isActivated () {
- return this.device.activate === 2
- },
- isOnline () {
- return this.device.onlineStatus === 1
- },
- statusClass () {
- return this.isActivated
- ? this.isOnline
- ? 'u-color--success'
- : 'u-color--error'
- : this.device.activate
- ? 'u-color--primary'
- : 'u-color--warning'
- },
- statusType () {
- return this.isActivated
- ? this.isOnline
- ? 'success'
- : 'error'
- : this.device.activate
- ? 'primary'
- : 'warning'
- },
- statusTip () {
- return this.isActivated
- ? this.isOnline
- ? '在线'
- : '离线'
- : this.device.activate
- ? '已激活'
- : '未激活'
- },
- address () {
- return `地址:${this.device.remark}`
- },
- styles () {
- return this.isOnline && this.shot ? {
- backgroundImage: `url("${this.shot}"`
- } : null
- },
- nextInfo () {
- return this.next ? `下一场:${this.next.startDate} ${this.next.startTime} ${this.next.name}` : ''
- }
- },
- mounted () {
- if (this.isActivated) {
- listen(this.onMessage)
- this.getTimeline()
- }
- this.$timer = -1
- },
- beforeDestroy () {
- if (this.isActivated) {
- unlisten(this.onMessage)
- if (this.show) {
- this.handleClose()
- }
- }
- clearTimeout(this.$timer)
- },
- methods: {
- askStatus () {
- if (!this.isOnline) {
- return
- }
- this.show = true
- },
- handleClose () {
- this.loading = false
- this.message = null
- this.show = false
- },
- onMessage (topic, message) {
- if (message) {
- const result = new RegExp(`${this.device.productId}/${this.device.id}/(.+)`).exec(topic)
- if (result) {
- switch (result[1]) {
- case 'status/reply':
- this.onAskReply(message)
- break
- case 'screenshot/reply':
- this.onScreenshotReply(message)
- break
- case 'calendar/update':
- this.onCalendarUpdate(message)
- break
- default:
- break
- }
- }
- }
- },
- publish (invoke) {
- return publish(`${this.device.productId}/${this.device.id}/${invoke}/ask`, JSON.stringify({ timestamp: Date.now() }))
- },
- ask () {
- this.publish('status').then(() => {
- this.loading = true
- this.message = null
- }, () => {
- this.$message({
- type: 'warning',
- message: '正在连接,请稍后再试'
- })
- })
- },
- onAskReply (message) {
- this.loading = false
- try {
- message = JSON.parse(message)
- switch (message.status) {
- case 1:
- this.message = { type: 's', value: '未播放节目,处于默认状态' }
- break
- case 2:
- this.message = { type: 's', value: '正在播放节目' }
- break
- case 3:
- this.message = { type: 's', value: '解析节目异常,请重新发布' }
- break
- default:
- this.message = { type: 's', value: '未知' }
- break
- }
- } catch {
- this.message = { type: 's', value: '解析异常,请重试' }
- }
- },
- screenshot () {
- this.publish('screenshot').then(() => {
- this.isShotting = true
- this.shot = null
- }, () => {
- this.$message({
- type: 'warning',
- message: '正在连接,请稍后再试'
- })
- })
- },
- onScreenshotReply (message) {
- this.isShotting = false
- this.shot = `data:image/jpeg;base64,${message.replace(/\s/g, '')}`
- },
- restart () {
- this.publish('restart').then(() => {
- this.loading = true
- this.message = null
- }, () => {
- this.$message({
- type: 'warning',
- message: '正在连接,请稍后再试'
- })
- })
- },
- onRestartReply () {
- this.loading = false
- },
- onCalendarUpdate (message) {
- clearTimeout(this.$timer)
- try {
- message = JSON.parse(message)
- this.timeline = (message.eventDetail || []).map(this.createItem)
- this.checkTimeline()
- } catch {
- this.getTimeline()
- }
- },
- createItem ({ programCalendarId, type, startTimestamp, endTimestamp }) {
- const startDateTime = new Date(Number(startTimestamp))
- const endDateTime = endTimestamp ? new Date(Number(endTimestamp)) : null
- return {
- type, startDateTime, endDateTime,
- id: programCalendarId,
- name: null,
- startDate: parseTime(startDateTime, '{y}.{m}.{d}'),
- startTime: parseTime(startDateTime, '{h}:{i}:{s}'),
- endDate: endDateTime ? parseTime(endDateTime, '{y}.{m}.{d}') : '',
- endTime: endDateTime ? parseTime(endDateTime, '{h}:{i}:{s}') : ''
- }
- },
- getTimeline () {
- this.loadingTimeline = true
- getTimeline(this.device.id, { custom: true }).then(({ data }) => {
- this.timeline = (JSON.parse(data.eventDetail) || []).map(this.createItem)
- this.checkTimeline()
- }).catch(({ isCancel }) => {
- if (!isCancel) {
- this.$timer = setTimeout(this.getTimeline, 2000)
- }
- })
- },
- checkTimeline () {
- this.loadingTimeline = true
- const now = Date.now()
- const current = this.timeline.findIndex(({ startDateTime, endDateTime }) => {
- return now >= startDateTime && (!endDateTime || now <= endDateTime)
- })
- this.current = this.timeline[current]
- this.next = this.current && this.timeline[current + 1]
- this.next && this.getName(this.next).then(name => {
- this.next.name = name
- })
- this.getDetail()
- },
- finishTimeline () {
- this.loadingTimeline = false
- const time = this.current ? this.current.endDateTime : this.next ? this.next.startDateTime : null
- clearTimeout(this.$timer)
- if (time) {
- this.$timer = setTimeout(this.checkTimeline, time - Date.now())
- }
- },
- getName (item) {
- if (item.id) {
- if (item.name) {
- return Promise.resolve(item.name)
- }
- if (deviceCache[item.id]) {
- return Promise.resolve(deviceCache[item.id])
- }
- return (item.type === 1 ? getProgram : getSchedule)(item.id, { custom: true }).then(({ name }) => {
- deviceCache[item.id] = name
- return name
- })
- }
- return Promise.resolve('未知')
- },
- getDetail () {
- if (this.current) {
- this.getName(this.current).then(name => {
- this.current.name = name
- this.finishTimeline()
- }).catch(({ isCancel }) => {
- if (!isCancel) {
- this.$timer = setTimeout(this.getDetail, 2000)
- }
- })
- } else {
- this.finishTimeline()
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .o-device {
- display: inline-flex;
- flex-direction: column;
- // height: 220px;
- color: $black;
- line-height: 1;
- border-radius: $radius;
- background-color: #fff;
- background-size: contain;
- &__block + &__block {
- margin-top: $spacing;
- }
- &__header {
- justify-self: flex-start;
- height: 24px;
- font-size: 16px;
- font-weight: bold;
- }
- &__status {
- display: inline-block;
- width: 12px;
- height: 12px;
- margin-right: 6px;
- border-radius: 50%;
- background-color: currentColor;
- }
- &__shot {
- display: inline-block;
- width: 24px;
- height: 24px;
- background: url("~@/assets/icon_screenshot.png") 0 0 / 100% 100% no-repeat;
- }
- &__tip {
- display: inline-block;
- position: relative;
- left: 16px;
- padding: 2px 8px 2px 10px;
- margin-left: -10px;
- color: #fff;
- font-size: 12px;
- line-height: 1;
- border-radius: 9px 0 0 9px;
- background-color: $success--dark;
- &.primary {
- background-color: $primary;
- }
- &.error {
- background-color: $error;
- }
- &.warning {
- background: $warning;
- }
- }
- &__preview {
- padding-top: 50%;
- background-position: center center;
- background-size: contain;
- background-repeat: no-repeat;
- }
- &__info {
- justify-content: center;
- height: 100px;
- }
- &__current {
- align-self: stretch;
- font-size: 20px;
- font-weight: bold;
- text-align: center;
- }
- &__time {
- margin: $spacing 0 24px;
- font-size: 20px;
- }
- &__line {
- display: inline-block;
- width: 20px;
- margin: 0 10px;
- border-bottom: 1px solid currentColor;
- }
- &__hms {
- position: relative;
- font-weight: bold;
- }
- &__ymd {
- position: absolute;
- top: 100%;
- left: 50%;
- color: $gray;
- font-size: 12px;
- font-weight: normal;
- transform: translate(-50%, 4px);
- }
- &__next {
- align-self: stretch;
- color: $gray;
- font-size: 12px;
- text-align: center;
- }
- &__current + &__next {
- margin-top: 24px;
- }
- &__footer {
- font-size: 12px;
- font-weight: bold;
- }
- }
- .o-shot {
- display: inline-block;
- width: 480px;
- height: 270px;
- object-fit: contain;
- }
- </style>
|