Explorar el Código

refactor: timeline

Casper Dai hace 2 años
padre
commit
1baf686caa
Se han modificado 2 ficheros con 1032 adiciones y 497 borrados
  1. 683 0
      src/views/device/timeline/bak.vue
  2. 349 497
      src/views/device/timeline/index.vue

+ 683 - 0
src/views/device/timeline/bak.vue

@@ -0,0 +1,683 @@
+<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"
+                      :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;
+
+  &::before {
+    content: "";
+    display: inline-block;
+    padding-top: $padding--16_9;
+  }
+}
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 349 - 497
src/views/device/timeline/index.vue


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio