| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- <template>
- <wrapper
- auto
- margin
- padding
- background
- >
- <div class="l-flex__none l-flex c-device-detail has-bottom-padding">
- <div
- class="l-flex__none c-device-detail__screen o-program"
- :class="{ 'u-pointer': programProxy }"
- :style="programStyle"
- @click="onView"
- />
- <div class="l-flex__auto l-flex--col">
- <div class="l-flex__none c-device-detail__name u-ellipsis">{{ deivceName }}</div>
- <template v-if="programProxy">
- <div class="l-flex__none c-device-detail__program u-ellipsis">
- <span
- class="u-pointer"
- @click="onView"
- >
- {{ programName }}
- </span>
- </div>
- <div class="l-flex__none c-device-detail__time">{{ programTime }}</div>
- </template>
- </div>
- </div>
- <div
- v-loading="deviceOptions.loading"
- class="l-flex__fill l-flex--col c-timeline"
- >
- <div class="l-flex__none l-flex--row has-bottom-padding">
- <div class="l-flex__auto c-sibling-item" />
- <el-date-picker
- v-model="current"
- class="l-flex__none c-sibling-item u-pointer"
- type="date"
- placeholder="选择日期"
- :picker-options="pickerOptions"
- :editable="false"
- :clearable="false"
- @change="onTimeChange"
- />
- <search-input
- v-model.trim="deviceOptions.params.name"
- class="l-flex__none c-sibling-item"
- placeholder="设备名称"
- @search="search"
- />
- <button
- class="l-flex__none c-sibling-item near o-button"
- @click="search"
- >
- 搜索
- </button>
- </div>
- <div class="l-flex__none l-flex c-timeline__row header">
- <div class="l-flex__none c-timeline__left">
- <span class="o-priority is-priority99">高</span>
- <span class="o-priority is-priority3">中</span>
- <span class="o-priority is-priority1">低</span>
- </div>
- <div class="l-flex__auto l-flex--row c-timeline__right">
- <i
- class="l-flex__none c-sibling-item c-timeline__arrow el-icon-arrow-left u-pointer"
- :class="{ display: canPrevious }"
- @click="offsetTime(-1)"
- />
- <div class="l-flex__auto l-flex--row c-sibling-item c-timeline__time">
- <div
- v-for="time in times"
- :key="time"
- class="l-flex__none"
- >
- {{ time }}
- </div>
- </div>
- <i
- class="l-flex__none c-sibling-item c-timeline__arrow display el-icon-arrow-right u-pointer"
- @click="offsetTime(1)"
- />
- </div>
- </div>
- <div class="l-flex__self l-flex--col c-timeline__main u-relative">
- <div class="l-flex__auto u-overflow-y--auto">
- <div
- v-for="item in deviceOptions.list"
- :key="item.id"
- class="l-flex c-timeline__row"
- :class="{ selected: item.id === deviceId }"
- @click="chooseProgramProxy(item)"
- >
- <div class="l-flex__none c-timeline__left u-relative u-pointer">
- <div class="u-ellipsis">{{ item.name }}</div>
- </div>
- <div class="l-flex__auto l-flex--col c-timeline__right">
- <div
- v-if="item.options.loading"
- class="l-flex--row c-timeline__programs"
- >
- <i class="el-icon-loading has-padding--h" />加载中...
- </div>
- <div
- v-else-if="item.options.error"
- class="l-flex--row c-timeline__programs has-padding--h"
- >
- <el-link
- type="warning"
- @click.stop="getTimeline(item)"
- >
- 获取失败,点击重试
- </el-link>
- </div>
- <template v-else-if="item.options.list.length">
- <div
- v-for="(programs, index) in item.options.list"
- :key="index"
- class="c-timeline__programs l-flex--row u-relative"
- >
- <div
- v-for="program in programs"
- :key="program.key"
- class="l-flex__none l-flex--row c-event-program u-pointer"
- :class="[{ 'selected': program.event.selected }, `is-priority${program.event.priority}`]"
- :style="program.style"
- @click.stop="chooseProgramProxy(item, program)"
- >
- <i
- class="l-flex__none c-event-program__img o-program is-ratio--16_9"
- :style="program.event.style"
- />
- <div class="l-flex__auto">
- <auto-text
- class="c-event-program__time"
- :text="program.time"
- :tag="program.style.width"
- />
- <auto-text
- class="c-event-program__name"
- :text="program.event.name"
- :tag="program.style.width"
- />
- </div>
- </div>
- </div>
- </template>
- <div
- v-else
- class="l-flex--row c-timeline__programs has-padding--h"
- >
- 当前时段暂无节目
- </div>
- </div>
- </div>
- </div>
- <status-wrapper
- v-if="isAbnormal"
- :error="deviceOptions.error"
- @click="getDevices"
- />
- <div
- v-show="style"
- class="c-timeline__line"
- >
- <div
- class="c-timeline__mask"
- :style="style"
- />
- </div>
- </div>
- <pagination
- :total="deviceOptions.totalCount"
- :page.sync="deviceOptions.params.pageNum"
- :limit.sync="deviceOptions.params.pageSize"
- @pagination="getDevices"
- />
- </div>
- <material-dialog ref="materialDialog" />
- </wrapper>
- </template>
- <script>
- import {
- getDevices,
- getTimeline
- } from '@/api/device'
- import { EventPriorityInfo } from '@/constant'
- import {
- toDate,
- toDateStr,
- toTimeStr,
- toZeroPoint,
- getNearestHitDate,
- getStartDate,
- getFinishDate,
- pickMin,
- pickMax,
- getEventDescription
- } from '@/utils/event'
- import { EventCache } from '@/utils/cache'
- import { createListOptions } from '@/utils'
- export default {
- name: 'ScheduleTimeline',
- data () {
- return {
- deviceOptions: createListOptions({
- name: '',
- activate: 1,
- pageSize: 5
- }),
- style: null,
- canPrevious: false,
- startHour: 0,
- times: [],
- current: null,
- device: null,
- programProxy: null
- }
- },
- computed: {
- deviceId () {
- return this.device?.id
- },
- deivceName () {
- return this.device?.name
- },
- programName () {
- return this.programProxy?.event.name
- },
- programTime () {
- return this.programProxy?.event.time
- },
- programStyle () {
- return this.programProxy?.event.style
- },
- isAbnormal () {
- const deviceOptions = this.deviceOptions
- return deviceOptions.error || !deviceOptions.loading && deviceOptions.totalCount === 0
- },
- pickerOptions () {
- return {
- disabledDate: this.isDisableDate
- }
- }
- },
- created () {
- this.initTimes(new Date())
- this.getDevices()
- this.$timer = setInterval(this.calcLine, 1000)
- },
- beforeDestroy () {
- clearInterval(this.$timer)
- },
- methods: {
- getDevices () {
- this.device = null
- if (this.programProxy) {
- this.programProxy.event.selected = false
- }
- this.programProxy = null
- const options = this.deviceOptions
- options.error = false
- options.loading = true
- getDevices(options.params).then(({ data, totalCount }) => {
- options.list = data.map(this.transform)
- options.totalCount = totalCount
- options.list.forEach(this.getTimeline)
- }, () => {
- options.error = true
- options.list = []
- }).finally(() => {
- options.loading = false
- })
- },
- search () {
- const options = this.deviceOptions
- options.list = []
- options.totalCount = 0
- options.params.pageNum = 1
- this.getDevices()
- },
- transform (device) {
- const { id, name } = device
- return {
- id, name,
- options: {
- loading: true,
- error: false,
- events: [],
- list: []
- }
- }
- },
- getTimeline (device) {
- const options = device.options
- options.error = false
- options.loading = true
- getTimeline(device.id, { custom: true }).finally(() => {
- options.loading = false
- }).then(
- events => {
- const now = Date.now()
- options.events = this.transformEvents(events.filter(({ until }) => !until || now <= toDate(until).getTime()))
- this.calcEvents(options)
- },
- () => {
- options.error = true
- options.list = []
- }
- )
- },
- transformEvent (event) {
- return {
- ...event,
- name: event.target.name || EventPriorityInfo[event.priority],
- time: getEventDescription(event),
- startDateTime: toDate(event.start),
- endDateTime: toDate(event.until),
- style: null,
- selected: false,
- img () {
- let promise = null
- switch (this.target.type) {
- case EventTarget.RECUR:
- promise = EventCache.getImage(EventTarget.PROGRAM, this.target.programs[0]?.programId)
- break
- default:
- promise = EventCache.getImage(this.target.type, this.target.id)
- break
- }
- promise.then(img => {
- if (img) {
- this.style = {
- backgroundSize: 'contain',
- backgroundImage: `url("${img}")`
- }
- }
- })
- }
- }
- },
- transformEvents (events) {
- const map = {}
- const now = Date.now()
- for (let i = 0; i < events.length; i++) {
- const event = this.transformEvent(events[i])
- event.key = `${i}_${now}`
- if (!map[event.priority]) {
- map[event.priority] = []
- }
- map[event.priority].push(event)
- }
- return Object.keys(map)
- .sort((a, b) => a > b ? -1 : 1)
- .map(key => map[key].sort((a, b) => toDate(a.start) - toDate(b.start)))
- },
- isDisableDate (date) {
- const now = new Date()
- const min = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- const max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
- return date < min || date > max
- },
- initTimes (date) {
- const maxDate = pickMax(toDate(date), new Date())
- this.current = toZeroPoint(maxDate)
- this.startHour = Math.min(20, maxDate.getHours())
- this.refreshTimes(this.startHour)
- },
- refreshTimes (start) {
- const times = []
- for (let i = 0; i < 5; i++) {
- times.push(`${(start + i).toString().padStart(2, '0')}:00`)
- }
- this.times = times
- this.$startDateTime = toDate(this.current.getTime() + start * 3600000)
- this.$endDateTime = toDate(this.current.getTime() + (start + 4) * 3600000)
- this.calcLine()
- this.refreshTimeline()
- },
- calcLine () {
- const now = Date.now()
- this.canPrevious = now < this.$startDateTime
- if (now < this.$startDateTime || now >= this.$endDateTime) {
- this.style = null
- } else {
- this.style = {
- left: `${Math.min(100, (now - this.$startDateTime) / 144000)}%`
- }
- }
- },
- refreshTimeline () {
- this.deviceOptions.list.forEach(device => {
- this.calcEvents(device.options)
- })
- },
- offsetTime (offset) {
- const next = this.startHour + offset
- if (offset < 0) {
- const timestamp = this.current.getTime() + next * 3600000
- const now = new Date()
- now.setMinutes(0)
- now.setSeconds(0)
- now.setMilliseconds(0)
- if (timestamp >= now) {
- if (next < 0) {
- this.initTimes(timestamp)
- } else {
- this.refreshTimes(this.startHour = next)
- }
- }
- } else if (next > 20) {
- this.initTimes(this.$endDateTime)
- } else {
- this.refreshTimes(this.startHour = next)
- }
- },
- onTimeChange (val) {
- this.initTimes(val)
- },
- calcSamePriorityEvents (events) {
- const total = 144000
- const arr = []
- for (let i = 0; i < events.length; i++) {
- const event = events[i]
- const { startDateTime, endDateTime } = event
- if (endDateTime && endDateTime <= this.$startDateTime || startDateTime >= this.$endDateTime) {
- continue
- }
- const hit = getNearestHitDate(event, this.$startDateTime, this.$endDateTime)
- if (hit) {
- const startDate = getStartDate(event, hit)
- const endDate = getFinishDate(event, hit)
- arr.push({
- key: event.key,
- event,
- time: `${toDateStr(startDate)} ${toTimeStr(startDate)} - ${toDateStr(endDate)} ${toTimeStr(endDate)}`,
- style: {
- left: `${(hit - this.$startDateTime) / total}%`,
- width: `${(pickMin(this.$endDateTime, getFinishDate(event, hit)) - hit) / total}%`,
- zIndex: event.priority
- }
- })
- event.img?.()
- if (endDate >= this.$endDateTime) {
- break
- }
- }
- }
- return arr
- },
- calcEvents (options) {
- if (options.loading || options.error) {
- return
- }
- options.list = options.events.map(this.calcSamePriorityEvents).filter(events => events.length)
- },
- chooseProgramProxy (device, programProxy) {
- this.device = device
- if (this.programProxy) {
- this.programProxy.event.selected = false
- }
- if (programProxy) {
- programProxy.event.selected = true
- }
- this.programProxy = programProxy
- },
- onView () {
- if (this.programProxy) {
- this.$refs.materialDialog.showEventTarget(this.programProxy.event.target)
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .c-device-detail {
- &__screen {
- width: 352px;
- margin-right: 24px;
- }
- &__name {
- color: $black;
- font-size: 20px;
- font-weight: bold;
- line-height: 1;
- }
- &__program {
- margin-top: 20px;
- color: $blue;
- font-size: 20px;
- font-weight: bold;
- }
- &__time {
- margin-top: $spacing;
- color: $info--dark;
- font-size: 20px;
- }
- }
- .c-timeline {
- min-height: 400px;
- &__main {
- border: 1px solid $border;
- }
- &__row {
- & + & {
- border-top: 1px solid $border;
- }
- }
- &__row.header {
- .c-timeline__left {
- border-right: none;
- }
- }
- &__row.header &__right {
- color: $black;
- user-select: none;
- background-color: transparent;
- }
- &__row.selected &__left {
- color: #fff;
- background-color: #9fbfe8;
- }
- &__row.selected &__left::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- width: 4px;
- background-color: $blue;
- }
- &__left {
- display: inline-flex;
- align-items: center;
- width: 210px;
- padding: 0 10px 0 20px;
- margin-right: 1px;
- color: $black;
- font-size: 16px;
- background-color: #fafbfc;
- border-right: 1px solid $border;
- }
- &__right {
- position: relative;
- color: $info--dark;
- font-size: 14px;
- line-height: 1;
- overflow: hidden;
- }
- &__programs {
- box-sizing: content-box;
- height: 54px;
- padding: 2px 0;
- & + & {
- border-top: 1px dashed $border;
- }
- }
- &__arrow {
- visibility: hidden;
- padding: 6px;
- color: #fff;
- font-size: 12px;
- border-radius: $radius--sm;
- background-color: $blue;
- &.display {
- visibility: visible;
- }
- }
- &__time {
- justify-content: space-between;
- padding: 10px 0;
- color: $black;
- font-size: 14px;
- }
- &__line {
- position: absolute;
- top: -12px;
- left: 210px;
- right: 0;
- bottom: 0;
- pointer-events: none;
- }
- &__mask {
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- color: #ff0000;
- border-right: 1px solid currentColor;
- z-index: 999;
- &::after {
- content: "";
- position: absolute;
- top: 0;
- right: -5px;
- width: 9px;
- height: 9px;
- border-radius: 50%;
- background-color: currentColor;
- }
- }
- }
- .c-event-program {
- position: absolute;
- top: 0;
- bottom: 0;
- padding: 0 4px;
- font-size: 12px;
- overflow: hidden;
- &:hover {
- color: #fff;
- background-color: rgba($blue, 0.4);
- }
- &.selected {
- color: #fff;
- background-color: $blue;
- z-index: 99;
- }
- &__img {
- width: 96px;
- margin-right: 8px;
- }
- &__name {
- margin-top: 10px;
- }
- }
- .o-priority {
- display: inline-block;
- padding: 4px;
- font-size: $font-size--sm;
- border-radius: $radius--sm;
- & + & {
- margin-left: 4px;
- }
- }
- .o-program {
- display: inline-block;
- font-size: 0;
- border-radius: $radius--sm;
- background: rgba(#000, 0.8) url("~@/assets/program_bg.png") center center /
- 100% 100% no-repeat;
- }
- </style>
|