Răsfoiți Sursa

feat: playlist

Casper Dai 2 ani în urmă
părinte
comite
6e117dcc15

+ 11 - 3
src/api/device.js

@@ -1,5 +1,5 @@
 import store from '@/store'
-import request, { tenantRequest } from '@/utils/request'
+import request, { tenantRequest } from '@/utils/request.js'
 import {
   add,
   update,
@@ -11,11 +11,11 @@ import {
   addTenant,
   addTenantOrOrg,
   addUser
-} from './base'
+} from './base.js'
 import {
   AssetType,
   SupportedAlarmStrategies
-} from '@/constant'
+} from '@/constant.js'
 
 export function getRatiosWithUser () {
   return tenantRequest({
@@ -619,3 +619,11 @@ export function getDepartmentDeviceTreeByGroup (path, options) {
     ...options
   })
 }
+
+export function getPlaylist (data) {
+  return request({
+    url: '/orchestration/timeSplit/page',
+    method: 'POST',
+    data
+  })
+}

+ 1 - 1
src/components/dialog/ConfirmDialog/index.vue

@@ -16,7 +16,7 @@
         :cancel="onCancel"
       >
         <button
-          class="c-sibling-item o-button o-button--cancel"
+          class="c-sibling-item o-button o-button--info"
           @click="onCancel"
         >
           {{ cancelText }}

+ 13 - 12
src/components/service/external/DevicePlayer/index.vue

@@ -32,7 +32,9 @@
       v-if="controls"
       class="l-flex--row c-footer u-font-size--sm"
     >
-      <div class="l-flex__fill c-sibling-item u-ellipsis">{{ device.name }}</div>
+      <div class="l-flex__fill c-sibling-item u-ellipsis">
+        {{ device.name }}
+      </div>
       <slot
         name="controls"
         :can-play="canPlay"
@@ -66,7 +68,9 @@
                 {{ item.label }}
               </div>
             </div>
-            <span class="has-active">{{ quality.label }}</span>
+            <span class="has-active">
+              {{ quality.label }}
+            </span>
           </div>
           <i
             class="c-sibling-item el-icon-full-screen has-active"
@@ -79,18 +83,18 @@
 </template>
 
 <script>
+import {
+  GATEWAY,
+  Quality
+} from '@/constant.js'
 import {
   authCode,
   getRecordConfig,
   addRecordConfig,
   updateRecordConfig,
   getDevice
-} from '@/api/device'
-import {
-  GATEWAY,
-  Quality
-} from '@/constant'
-import playerMixin from '../player'
+} from '@/api/device.js'
+import playerMixin from '../player.js'
 
 export default {
   name: 'DevicePlayer',
@@ -161,9 +165,7 @@ export default {
   },
   methods: {
     onClick () {
-      if (this.online) {
-        this.$emit('click', this.device)
-      }
+      this.$emit('click', this.device)
     },
     onMouseMove () {
       if (this.keep) {
@@ -178,7 +180,6 @@ export default {
       }
     },
     onVideoReset () {
-      console.log('onVideoReset')
       this.recordConfig = null
       this.$playerInfo = null
     },

+ 6 - 6
src/components/service/external/player.js

@@ -79,12 +79,12 @@ export default {
       }
     },
     onVideoStatisticsInfo (res) {
-      console.log('STATISTICS_INFO', this.$lastDecodedFrames, '->', res.decodedFrames)
+      // console.log('STATISTICS_INFO', this.$lastDecodedFrames, '->', res.decodedFrames)
       if (this.$lastDecodedFrames === res.decodedFrames) {
         if (this.$lastDecodedFramesTimestamp) {
           const delta = Date.now() - this.$lastDecodedFramesTimestamp
           const time = this.waiting ? 5000 : 10000
-          console.log('frozen', delta, '->', time)
+          // console.log('frozen', delta, '->', time)
           if (delta >= time) {
             if (this.waiting) {
               this.releasePlayer()
@@ -232,7 +232,7 @@ export default {
       }
     },
     onVideoPlay () {
-      console.log('onVideoPlay')
+      // console.log('onVideoPlay')
       this.paused = false
       if (this.loading) {
         this.loading = false
@@ -242,16 +242,16 @@ export default {
       }
     },
     onVideoPause () {
-      console.log('onVideoPause')
+      // console.log('onVideoPause')
       this.paused = true
       this.waiting = false
     },
     onVideoWaiting () {
-      console.log('onVideoWaiting')
+      // console.log('onVideoWaiting')
       this.waiting = true
     },
     onVideoPlaying () {
-      console.log('onVideoPlaying')
+      // console.log('onVideoPlaying')
       this.waiting = false
     },
     onVideoError (e) {

+ 7 - 9
src/components/tree/DeviceTree/index.vue

@@ -29,18 +29,16 @@
       />
       <template v-else>
         <div class="l-flex__none l-flex--row c-sibling-item--v u-overflow--hidden">
-          <div class="l-flex__auto">
-            <div
-              v-if="canReset"
-              class="o-button"
-              @click="onReset"
-            >
-              重置
-            </div>
+          <div
+            v-if="canReset"
+            class="l-flex__none o-button c-sibling-item"
+            @click="onReset"
+          >
+            重置
           </div>
           <search-input
             v-model.trim="deviceName"
-            class="l-flex__none"
+            class="l-flex__fill c-sibling-item near"
             size="small"
             placeholder="设备名称"
             @search="onSearch"

+ 5 - 0
src/router/index.js

@@ -278,6 +278,11 @@ export const asyncRoutes = [
         component: () => import('@/views/device/timeline/index'),
         meta: { title: '排期预览' }
       },
+      {
+        path: 'playlist',
+        component: () => import('@/views/device/playlist/index'),
+        meta: { title: '播放清单' }
+      },
       {
         path: 'record',
         component: () => import('@/views/device/record/index'),

+ 1 - 1
src/scss/bem/_object.scss

@@ -38,7 +38,7 @@
     margin-right: $spacing--xs;
   }
 
-  &--cancel {
+  &--info {
     color: $gray--dark;
     background-color: $gray--light;
 

+ 405 - 0
src/views/device/playlist/index.vue

@@ -0,0 +1,405 @@
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+    horizontal
+  >
+    <device-tree
+      ref="tree"
+      class="c-sibling-item c-sidebar u-width--md"
+      shrink
+      checkbox
+      check-on-click-node
+      @change="onChange"
+    />
+    <div class="l-flex__fill l-flex c-sibling-item far c-step">
+      <div class="l-flex__fill l-flex--col c-step__column">
+        <div class="l-flex__none l-flex--row c-sibling-item--v u-height u-font-size--sm u-bold">
+          实时画面
+        </div>
+        <template v-if="hasData">
+          <div class="l-flex__fill c-sibling-item--v u-overflow-y--auto">
+            <draggable
+              class="c-record-grid"
+              handle=".o-video"
+              animation="300"
+            >
+              <device-player
+                v-for="item in devices"
+                :key="item.id"
+                :device="item"
+                controls
+                autoplay
+                retry
+                keep
+                check-online
+                @click="onChoose"
+              />
+            </draggable>
+          </div>
+        </template>
+        <template v-else>
+          <el-empty description="请选择设备" />
+        </template>
+      </div>
+      <div class="l-flex__none l-flex--col c-step__column u-width--xl">
+        <div class="l-flex__none l-flex--row c-sibling-item--v">
+          <div
+            class="l-flex__fill c-sibling-item o-tip u-font-size"
+            :class="{ active: isSelected }"
+          >
+            <span class="u-ellipsis">
+              {{ tip }}
+            </span>
+          </div>
+          <div class="l-flex__none l-flex--row c-sibling-item near">
+            <div class="l-flex__none c-sibling-item u-font-size--sm u-bold">
+              优先级
+            </div>
+            <el-select
+              v-model="priority"
+              class="c-sibling-item u-width--xs"
+              size="small"
+              @change="onConditionChange"
+            >
+              <el-option
+                v-for="option in priorityOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </div>
+        </div>
+        <div class="l-flex__none l-flex--row c-sibling-item--v">
+          <div class="l-flex__none c-sibling-item u-font-size--sm u-bold">
+            时间范围
+          </div>
+          <el-date-picker
+            v-model="dateRange"
+            class="c-sibling-item near"
+            type="datetimerange"
+            size="mini"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="timestamp"
+            :default-time="['00:00:00', '23:59:59']"
+            :clearable="false"
+            @change="onConditionChange"
+          />
+          <i
+            class="c-sibling-item near el-icon-refresh u-font-size--md has-active"
+            @click="onConditionChange"
+          />
+        </div>
+        <div
+          ref="container"
+          class="l-flex__fill c-sibling-item--v u-overflow-y--auto"
+          @scroll="checkScroll"
+        >
+          <el-table
+            :data="options.events"
+            class="c-sibling-item--v"
+            size="small"
+          >
+            <template #empty>
+              <el-empty v-if="!options.loading" />
+              <div
+                v-else
+                class="u-text--center"
+              >
+                <i class="el-icon-loading u-font-size" />
+              </div>
+            </template>
+            <el-table-column
+              width="80"
+              prop="time"
+              label="播出时间"
+            />
+            <el-table-column
+              show-overflow-tooltip
+              prop="originalName"
+              label="素材名称"
+              min-width="120"
+            />
+            <el-table-column
+              show-overflow-tooltip
+              prop="name"
+              label="编单名称"
+            />
+            <!-- <el-table-column
+              width="64"
+              prop="priority"
+              label="优先级"
+              align="center"
+            /> -->
+            <el-table-column
+              width="64"
+              prop="status"
+              label="状态"
+              align="center"
+            >
+              <template slot-scope="scope">
+                <span :class="scope.row.color">
+                  {{ scope.row.label }}
+                </span>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div
+            v-if="options.loaded && options.loading"
+            class="c-sibling-item--v u-text--center"
+          >
+            <i class="el-icon-loading" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </wrapper>
+</template>
+
+<script>
+import {
+  EventPriority,
+  EventPriorityInfo
+} from '@/constant.js'
+import { parseTime } from '@/utils'
+import { getPlaylist } from '@/api/device.js'
+import Draggable from 'vuedraggable'
+
+export default {
+  name: 'DeviceRecord',
+  components: {
+    Draggable
+  },
+  data () {
+    const now = Date.now()
+
+    return {
+      priority: EventPriority.INSERTED,
+      dateRange: [new Date(parseTime(now, '{y}/{m}/{d} 00:00:00')).getTime(), new Date(parseTime(now, '{y}/{m}/{d} 23:59:59')).getTime()],
+      priorityOptions: [
+        {
+          value: EventPriority.SCHEDULING,
+          label: EventPriorityInfo[EventPriority.SCHEDULING]
+        },
+        {
+          value: EventPriority.INSERTED,
+          label: EventPriorityInfo[EventPriority.INSERTED]
+        },
+        {
+          value: EventPriority.EMBEDDED,
+          label: EventPriorityInfo[EventPriority.EMBEDDED]
+        },
+        {
+          value: EventPriority.EMERGENT,
+          label: EventPriorityInfo[EventPriority.EMERGENT]
+        }
+      ],
+      devices: [],
+      options: {
+        id: null,
+        name: null,
+        loading: false,
+        loaded: false,
+        more: true,
+        events: [],
+        pageIndex: 1,
+        pageSize: 100
+      }
+    }
+  },
+  computed: {
+    hasData () {
+      return this.devices.length > 0
+    },
+    isSelected () {
+      return !!this.options.id
+    },
+    tip () {
+      return this.isSelected ? this.options.name : '播放清单'
+    }
+  },
+  methods: {
+    checkScroll () {
+      if (this.options.loading || !this.options.more) {
+        return
+      }
+      const container = this.$refs.container
+      const scrollHeight = container.scrollHeight
+      const scrollTop = container.scrollTop + container.clientHeight
+      if (scrollHeight - scrollTop <= 50) {
+        this.loadMore()
+      }
+    },
+    loadMore () {
+      if (this.options.loading) {
+        return
+      }
+      const options = this.options
+      options.loading = true
+      getPlaylist({
+        deviceIdList: [options.id],
+        start: this.dateRange[0],
+        util: this.dateRange[1],
+        priority: this.priority,
+        pageSize: options.pageSize,
+        pageIndex: options.pageIndex
+      }).then(({ data, totalCount }) => {
+        options.events = Object.freeze([...options.events, ...data].map(this.transformEvent))
+        options.more = totalCount > options.pageIndex * options.pageSize
+        options.loaded = true
+        options.pageIndex += 1
+      }).finally(() => {
+        options.loading = false
+      })
+    },
+    transformEvent (event) {
+      const { start, until, time } = event
+      const status = this.checkTime(Date.now(), start, until)
+      return {
+        ...event,
+        time: time || this.timestampToDatetime(event.start),
+        status,
+        ...this.getStatusInfo(status)
+      }
+    },
+    getStatusInfo (status) {
+      switch (status) {
+        case -1:
+          return {
+            label: '已播',
+            color: 'u-color--success'
+          }
+        case 0:
+          return {
+            label: '在播',
+            color: 'u-color--primary'
+          }
+        case 1:
+          return {
+            label: '未播',
+            color: 'u-color--error'
+          }
+        default:
+          return {}
+      }
+    },
+    onChoose (device) {
+      if (!device) {
+        this.currentId = null
+        this.options = {
+          id: null,
+          name: null,
+          loading: false,
+          loaded: false,
+          more: false,
+          events: [],
+          pageIndex: 1,
+          pageSize: 100
+        }
+        return
+      }
+      const { id, name } = device
+      if (this.options.id === id) {
+        return
+      }
+      this.options = {
+        id,
+        name,
+        loading: false,
+        loaded: false,
+        more: true,
+        events: [],
+        pageIndex: 1,
+        pageSize: 100
+      }
+      this.loadMore()
+    },
+    checkTime (time, start, end) {
+      if (time < start) {
+        return 1
+      }
+      if (time >= start && time <= end) {
+        return 0
+      }
+      return -1
+    },
+    formatTime (date) {
+      return date.toISOString().slice(0, 19).replace('T', ' ')
+    },
+    timestampToDatetime (timestamp) {
+      const date = new Date(parseInt(timestamp))
+      return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
+    },
+    onChange (devices) {
+      const isEmpty = this.devices.length === 0
+      if (isEmpty && devices.length === 1 || devices.length && !this.options.id) {
+        this.onChoose(devices?.[0])
+      }
+      this.devices = devices
+    },
+    onConditionChange () {
+      const { id, name } = this.options
+      if (id) {
+        this.options = {
+          id,
+          name,
+          loading: false,
+          loaded: false,
+          more: true,
+          events: [],
+          pageIndex: 1,
+          pageSize: 100
+        }
+        this.loadMore()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-record-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+  gap: $spacing--3xs;
+}
+
+.played {
+  color: green;
+}
+
+.playing {
+  color: blue;
+}
+
+.not-played {
+  color: red;
+}
+
+.o-tip {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  min-width: 60px;
+  height: $height--sm;
+  padding: 0 $padding;
+  color: #fff;
+  font-size: $font-size--sm;
+  line-height: 1;
+  border: none;
+  border-radius: $radius--sm;
+  color: $gray--dark;
+  background-color: $gray--light;
+
+  &.active {
+    color: $primary;
+    background-color: $blue--light;
+  }
+}
+</style>
+

+ 13 - 5
src/views/platform/remote-log/index.vue

@@ -28,8 +28,12 @@
             抓取说明
           </button>
         </div>
-        <div class="c-grid-form__row u-font-size--xs u-bold">当前状态:{{ logSetting.activate ? '已下发配置' : '未下发配置' }}</div>
-        <span class="c-grid-form__label u-required">抓取时长</span>
+        <div class="c-grid-form__row u-font-size--xs u-bold">
+          当前状态:{{ logSetting.activate ? '已下发配置' : '未下发配置' }}
+        </div>
+        <span class="c-grid-form__label u-required">
+          抓取时长
+        </span>
         <div class="l-flex--row">
           <el-input-number
             v-model="logSetting.duration"
@@ -39,7 +43,9 @@
             step-strictly
           />&nbsp;秒
         </div>
-        <span class="c-grid-form__label u-required">抓取指令</span>
+        <span class="c-grid-form__label u-required">
+          抓取指令
+        </span>
         <div
           class="has-info"
           data-info="多条指令以分号分隔"
@@ -50,7 +56,9 @@
             :rows="5"
           />
         </div>
-        <span class="c-grid-form__label u-required">是否重启</span>
+        <span class="c-grid-form__label u-required">
+          是否重启
+        </span>
         <el-switch
           v-model="logSetting.reboot"
           class="c-grid-form__option"
@@ -66,7 +74,7 @@
           开启抓取
         </button>
         <button
-          class="c-sibling-item o-button o-button--cancel"
+          class="c-sibling-item o-button o-button--info"
           @click="cancel"
         >
           取消

+ 1 - 1
src/views/screen/deploy/device/index.vue

@@ -33,7 +33,7 @@
         @change="onChange"
       />
       <div class="l-flex__none l-flex--col c-step__column u-width--lg">
-        <div class="c-sibling-item--v near u-font-size--sm  u-bold">
+        <div class="c-sibling-item--v near u-font-size--sm u-bold">
           上播类型
         </div>
         <el-select