Sfoglia il codice sorgente

feat: support the addition of resources and websites in the takeover page

Casper Dai 3 anni fa
parent
commit
ab48347499

BIN
src/assets/logo/fujian.png


+ 9 - 6
src/views/bigscreen/ast/components/Card.vue → src/components/card/MediaCard/index.vue

@@ -1,15 +1,18 @@
 <template>
   <div
-    class="c-card u-pointer"
+    class="o-media-card u-pointer"
     @click="onClick"
     @dblclick="onDblclick"
   >
     <auto-image
-      class="c-card__image"
+      class="o-media-card__image"
       :placeholder="source.icon"
-      :src="source.thumbnailUrl"
+      :src="source.thumb"
+    />
+    <auto-text
+      class="o-media-card__name"
+      :text="source.name"
     />
-    <div class="c-card__name u-ellipsis">{{ source.name }}</div>
     <slot />
   </div>
 </template>
@@ -35,10 +38,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.c-card {
+.o-media-card {
   display: inline-block;
   position: relative;
-  padding-top: 60%;
+  padding-top: 56.25%;
   color: $info;
   background-color: rgba(0, 0, 0, 0.8);
 

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

@@ -27,6 +27,7 @@
 <script>
 export default {
   name: 'ConfirmDialog',
+  inheritAttrs: false,
   props: {
     size: {
       type: String,

+ 1 - 0
src/components/dialog/ProgramDialog/index.vue

@@ -28,6 +28,7 @@ export default {
   components: {
     Program
   },
+  inheritAttrs: false,
   data () {
     return {
       program: null,

+ 1 - 0
src/components/dialog/ScheduleDialog/index.vue

@@ -23,6 +23,7 @@
 <script>
 export default {
   name: 'ScheduleDialog',
+  inheritAttrs: false,
   data () {
     return {
       scheduleId: null,

+ 1 - 0
src/components/dialog/TableDialog/index.vue

@@ -29,6 +29,7 @@
 <script>
 export default {
   name: 'TableDialog',
+  inheritAttrs: false,
   props: {
     schema: {
       type: Object,

+ 4 - 4
src/icons/svg/audio-bg.svg

@@ -1,4 +1,4 @@
-<svg id="icon_audio" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" height="72" viewBox="0 0 120 72">
+<svg id="icon_audio" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="72" viewBox="0 0 128 72">
   <defs>
     <style>
       .cls-1 {
@@ -10,13 +10,13 @@
         fill: url(#linear-gradient);
       }
     </style>
-    <linearGradient id="linear-gradient" x1="5" y1="197" x2="115" y2="197" gradientUnits="userSpaceOnUse">
+    <linearGradient id="linear-gradient" x1="0" y1="197" x2="128" y2="197" gradientUnits="userSpaceOnUse">
       <stop offset="0" stop-color="#0480ff"/>
       <stop offset="0.399" stop-color="#ff15ff"/>
       <stop offset="0.674" stop-color="#ffc426"/>
       <stop offset="1" stop-color="#3b21ff"/>
     </linearGradient>
   </defs>
-  <rect id="矩形_1_拷贝" data-name="矩形 1 拷贝" class="cls-1" width="120" height="72"/>
-  <path id="矩形_1_拷贝_10" data-name="矩形 1 拷贝 10" class="cls-2" d="M6,192a1,1,0,0,1,1,1v19H5V193A1,1,0,0,1,6,192Zm6,12a1,1,0,0,1,1,1v7H11v-7A1,1,0,0,1,12,204Zm6-4a1,1,0,0,1,1,1v11H17V201A1,1,0,0,1,18,200Zm6-4a1,1,0,0,1,1,1v15H23V197A1,1,0,0,1,24,196Zm6,3a1,1,0,0,1,1,1v12H29V200A1,1,0,0,1,30,199Zm6-11a1,1,0,0,1,1,1v23H35V189A1,1,0,0,1,36,188Zm6,3a1,1,0,0,1,1,1v20H41V192A1,1,0,0,1,42,191Zm6-9a1,1,0,0,1,1,1v29H47V183A1,1,0,0,1,48,182Zm6,14a1,1,0,0,1,1,1v15H53V197A1,1,0,0,1,54,196Zm6,7a1,1,0,0,1,1,1v8H59v-8A1,1,0,0,1,60,203Zm6-4a1,1,0,0,1,1,1v12H65V200A1,1,0,0,1,66,199Zm6-4a1,1,0,0,1,1,1v16H71V196A1,1,0,0,1,72,195Zm6-7a1,1,0,0,1,1,1v23H77V189A1,1,0,0,1,78,188Zm6,4a1,1,0,0,1,1,1v19H83V193A1,1,0,0,1,84,192Zm6,6a1,1,0,0,1,1,1v13H89V199A1,1,0,0,1,90,198Zm6-3a1,1,0,0,1,1,1v16H95V196A1,1,0,0,1,96,195Zm6-11a1,1,0,0,1,1,1v27h-2V185A1,1,0,0,1,102,184Zm6,9a1,1,0,0,1,1,1v18h-2V194A1,1,0,0,1,108,193Zm6,5a1,1,0,0,1,1,1v13h-2V199A1,1,0,0,1,114,198Z" transform="translate(0 -140)"/>
+  <rect id="矩形_1_拷贝" data-name="矩形 1 拷贝" class="cls-1" width="128" height="72"/>
+  <path id="矩形_1_拷贝_10" data-name="矩形 1 拷贝 10" class="cls-2" d="M6,192a1,1,0,0,1,1,1v19H5V193A1,1,0,0,1,6,192Zm6,12a1,1,0,0,1,1,1v7H11v-7A1,1,0,0,1,12,204Zm6-4a1,1,0,0,1,1,1v11H17V201A1,1,0,0,1,18,200Zm6-4a1,1,0,0,1,1,1v15H23V197A1,1,0,0,1,24,196Zm6,3a1,1,0,0,1,1,1v12H29V200A1,1,0,0,1,30,199Zm6-11a1,1,0,0,1,1,1v23H35V189A1,1,0,0,1,36,188Zm6,3a1,1,0,0,1,1,1v20H41V192A1,1,0,0,1,42,191Zm6-9a1,1,0,0,1,1,1v29H47V183A1,1,0,0,1,48,182Zm6,14a1,1,0,0,1,1,1v15H53V197A1,1,0,0,1,54,196Zm6,7a1,1,0,0,1,1,1v8H59v-8A1,1,0,0,1,60,203Zm6-4a1,1,0,0,1,1,1v12H65V200A1,1,0,0,1,66,199Zm6-4a1,1,0,0,1,1,1v16H71V196A1,1,0,0,1,72,195Zm6-7a1,1,0,0,1,1,1v23H77V189A1,1,0,0,1,78,188Zm6,4a1,1,0,0,1,1,1v19H83V193A1,1,0,0,1,84,192Zm6,6a1,1,0,0,1,1,1v13H89V199A1,1,0,0,1,90,198Zm6-3a1,1,0,0,1,1,1v16H95V196A1,1,0,0,1,96,195Zm6-11a1,1,0,0,1,1,1v27h-2V185A1,1,0,0,1,102,184Zm6,9a1,1,0,0,1,1,1v18h-2V194A1,1,0,0,1,108,193Zm6,5a1,1,0,0,1,1,1v13h-2V199A1,1,0,0,1,114,198Z" transform="translate(4 -140)"/>
 </svg>

+ 4 - 4
src/icons/svg/video-bg.svg

@@ -1,4 +1,4 @@
-<svg id="icon_video_big_" data-name="icon_video_big " xmlns="http://www.w3.org/2000/svg" width="120" height="72" viewBox="0 0 120 72">
+<svg id="icon_video_big_" data-name="icon_video_big " xmlns="http://www.w3.org/2000/svg" width="128" height="72" viewBox="0 0 128 72">
   <defs>
     <style>
       .cls-1 {
@@ -15,7 +15,7 @@
       }
     </style>
   </defs>
-  <rect id="矩形_1_拷贝_7" data-name="矩形 1 拷贝 7" class="cls-1" width="120" height="72"/>
-  <rect id="icon_video_big_拷贝_2" data-name="icon_video_big  拷贝 2" class="cls-2" x="41" y="24" width="38" height="24" rx="2" ry="2"/>
-  <path id="icon_video_big_拷贝" data-name="icon_video_big  拷贝" class="cls-3" d="M66,176l-11,7V169Z" transform="translate(0 -140)"/>
+  <rect id="矩形_1_拷贝_7" data-name="矩形 1 拷贝 7" class="cls-1" width="128" height="72"/>
+  <rect id="icon_video_big_拷贝_2" data-name="icon_video_big  拷贝 2" class="cls-2" x="45" y="24" width="38" height="24" rx="2" ry="2"/>
+  <path id="icon_video_big_拷贝" data-name="icon_video_big  拷贝" class="cls-3" d="M66,176l-11,7V169Z" transform="translate(4 -140)"/>
 </svg>

+ 21 - 0
src/icons/svg/web.svg

@@ -0,0 +1,21 @@
+<svg id="icon_web" xmlns="http://www.w3.org/2000/svg" width="120" height="72" viewBox="0 0 128 72">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #0f1317;
+      }
+
+      .cls-2 {
+        fill: #141a20;
+      }
+
+      .cls-3 {
+        fill: #006eff;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <rect id="矩形_1_拷贝_7" data-name="矩形 1 拷贝 7" class="cls-1" width="128" height="72"/>
+  <rect id="icon_video_big_拷贝_2" data-name="icon_video_big  拷贝 2" class="cls-2" x="45" y="24" width="38" height="24" rx="2" ry="2"/>
+  <path id="形状_316_1" data-name="形状 316 1" class="cls-3" d="M63.218,172.558a10.306,10.306,0,0,0,2.225-1.035A7.1,7.1,0,0,1,67,175.549H63.776A10.592,10.592,0,0,0,63.218,172.558ZM61.167,169a6.971,6.971,0,0,1,3.657,1.847,9.507,9.507,0,0,1-1.934.872A10.57,10.57,0,0,0,61.167,169Zm-0.721,3.159v-2.623a9.668,9.668,0,0,1,1.572,2.416A9.254,9.254,0,0,1,60.445,172.159Zm-0.891,7.682v2.623a9.668,9.668,0,0,1-1.572-2.416A9.254,9.254,0,0,1,59.555,179.841Zm-2.439-3.39h2.439v2.488a10.4,10.4,0,0,0-1.913.261A9.731,9.731,0,0,1,57.115,176.451Zm0.527-3.651a10.4,10.4,0,0,0,1.913.261v2.488H57.115A9.731,9.731,0,0,1,57.642,172.8Zm1.913-3.264v2.623a9.254,9.254,0,0,1-1.572-.207A9.668,9.668,0,0,1,59.555,169.536Zm-4.379,1.311A6.971,6.971,0,0,1,58.833,169a10.57,10.57,0,0,0-1.724,2.719A9.507,9.507,0,0,1,55.176,170.847Zm1.049,4.7H53a7.1,7.1,0,0,1,1.558-4.026,10.306,10.306,0,0,0,2.225,1.035A10.592,10.592,0,0,0,56.224,175.549Zm0.558,3.893a10.306,10.306,0,0,0-2.225,1.035A7.1,7.1,0,0,1,53,176.451h3.224A10.592,10.592,0,0,0,56.782,179.442ZM58.833,183a6.971,6.971,0,0,1-3.657-1.847,9.507,9.507,0,0,1,1.934-.872A10.57,10.57,0,0,0,58.833,183Zm4.052-7.451H60.445v-2.488a10.4,10.4,0,0,0,1.913-.261A9.731,9.731,0,0,1,62.885,175.549ZM62.358,179.2a10.4,10.4,0,0,0-1.913-.261v-2.488h2.439A9.731,9.731,0,0,1,62.358,179.2Zm-1.913,3.264v-2.623a9.254,9.254,0,0,1,1.572.207A9.668,9.668,0,0,1,60.445,182.464Zm4.379-1.311A6.971,6.971,0,0,1,61.167,183a10.57,10.57,0,0,0,1.724-2.719A9.507,9.507,0,0,1,64.824,181.153Zm-1.049-4.7H67a7.1,7.1,0,0,1-1.558,4.026,10.306,10.306,0,0,0-2.225-1.035A10.592,10.592,0,0,0,63.776,176.451Z" transform="translate(4 -140)"/>
+</svg>

+ 4 - 0
src/scss/bem/_component.scss

@@ -12,10 +12,12 @@
   &.large {
     width: 80%;
     min-width: 960px;
+    min-height: 480px;
   }
 
   &.medium {
     width: 720px;
+    min-height: 360px;
   }
 
   &.mini {
@@ -216,6 +218,8 @@
 }
 
 .c-table {
+  min-height: 300px;
+
   &__header {
     padding-bottom: $spacing;
 

+ 2 - 2
src/scss/bem/_layout.scss

@@ -52,7 +52,7 @@
 
 .l-grid {
   display: grid;
-  grid-template-columns: repeat(4, minmax(100px, 1fr));
+  grid-template-columns: repeat(4, minmax(90px, 1fr));
   grid-template-rows: max-content;
   grid-row-gap: $spacing;
   grid-column-gap: $spacing;
@@ -60,7 +60,7 @@
   min-width: 0;
 
   &.large {
-    grid-template-columns: repeat(3, minmax(100px, 1fr));
+    grid-template-columns: repeat(3, minmax(90px, 1fr));
   }
 }
 

+ 18 - 27
src/views/bigscreen/ast/Designer.vue

@@ -234,23 +234,24 @@
         @start="onSourceDragStart"
         @end="onSourceDragEnd"
       >
-        <card
+        <media-card
           v-for="(source, index) in sources"
           :key="index"
+          class="o-card"
           :source="source"
           @click="onView"
         >
           <div
             v-if="needTypeTag"
-            class="c-card__tag"
+            class="o-card__tag"
           >{{ source.tag }}</div>
           <i
-            class="c-card__icon el-icon-delete has-active"
+            class="o-card__icon el-icon-delete has-active"
             @mousedown.stop
             @pointerdown.stop
             @click.stop="onDelAsset(index)"
           />
-        </card>
+        </media-card>
       </draggable>
       <template #footer>
         <button
@@ -282,7 +283,7 @@
       <template v-if="showServerAssets">
         <grid-table :schema="assetSchema">
           <grid-table-item v-slot="item">
-            <card
+            <media-card
               :source="item"
               @dblclick="onChoosenAsset"
             />
@@ -304,19 +305,19 @@
           :schema="assetSchema"
         >
           <grid-table-item v-slot="item">
-            <card
+            <media-card
               :source="item"
               @click="onToggleGrid"
             >
               <el-checkbox
                 v-model="item.selected"
-                class="c-card__checkbox"
+                class="o-card__checkbox"
               />
               <i
-                class="c-card__type el-icon-video-play has-active u-pointer"
+                class="o-card__play el-icon-video-play has-active u-pointer"
                 @click.stop="onView(item)"
               />
-            </card>
+            </media-card>
           </grid-table-item>
         </grid-table>
       </template>
@@ -405,8 +406,8 @@ export default {
     },
     assetSchema () {
       return {
-        condition: { originalName: '', type: this.assetType[0] },
-        list: this.getAssets,
+        condition: { originalName: '', type: this.assetType[0], status: State.AVAILABLE },
+        list: getAssets,
         transform: this.transformAsset,
         filters: (
           this.assetType.length > 1
@@ -660,18 +661,10 @@ export default {
       }
     },
     onCloseAssetsDialog () {
-      this.$choosenAssets = []
       this.widgetAttr = null
       this.showAssets = false
       this.showServerAssets = false
     },
-    getAssets (params) {
-      this.$choosenAssets = []
-      return getAssets({
-        status: State.AVAILABLE,
-        ...params
-      })
-    },
     transformAsset ({ type, originalName, keyName, thumbnail, duration, size, md5 }) {
       const asset = {
         selected: false,
@@ -688,7 +681,7 @@ export default {
         asset.duration = Number(duration) || 0
       }
       if (thumbnail) {
-        asset.thumbnailUrl = getThumbnailUrl(thumbnail)
+        asset.thumb = getThumbnailUrl(thumbnail)
       } else {
         asset.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
       }
@@ -711,9 +704,9 @@ export default {
     transformDataToSource (data) {
       const source = { ...data }
       if (data.thumbnail) {
-        source.thumbnailUrl = getThumbnailUrl(data.thumbnail)
+        source.thumb = getThumbnailUrl(data.thumbnail)
       } else if (source.type === AssetType.IMAGE) {
-        source.thumbnailUrl = getThumbnailUrl(data.keyName)
+        source.thumb = getThumbnailUrl(data.keyName)
       } else {
         switch (source.type) {
           case AssetType.VIDEO:
@@ -1094,13 +1087,11 @@ $border: #242835;
   }
 }
 
-.l-grid--info.dragging .c-card .c-card__icon {
+.l-grid--info.dragging .o-card .o-card__icon {
   display: none;
 }
 
-.c-card {
-  color: $info;
-
+.o-card {
   &:hover &__icon {
     display: inline-block;
   }
@@ -1136,7 +1127,7 @@ $border: #242835;
     z-index: 9;
   }
 
-  &__type {
+  &__play {
     position: absolute;
     top: 50%;
     left: 50%;

+ 1 - 3
src/views/bigscreen/ast/mixin.js

@@ -10,7 +10,6 @@ import {
 } from './core/utils'
 import WidgetShortcut from './components/WidgetShortcut.vue'
 import Volume from './components/Volume'
-import Card from './components/Card'
 
 export default {
   provide () {
@@ -20,8 +19,7 @@ export default {
   },
   components: {
     WidgetShortcut,
-    Volume,
-    Card
+    Volume
   },
   props: {
     program: {

+ 29 - 19
src/views/device/detail/components/DeviceTakeOver/components/Asset.vue → src/views/device/detail/components/DeviceTakeOver/components/AssetCard.vue

@@ -1,17 +1,14 @@
 <template>
-  <div
-    class="o-preload-asset"
-  >
+  <div class="o-preload-asset">
     <div
       class="o-preload-asset__img"
       :class="{ 'u-pointer': status === 2 }"
-      :style="styles"
       @click="onClick"
     >
-      <svg-icon
-        v-if="isVideo"
+      <auto-image
         class="o-preload-asset__thumb"
-        icon-class="video-thumb"
+        :placeholder="placehoder"
+        :src="src"
       />
       <i
         v-if="status === -1"
@@ -24,6 +21,7 @@
         :text="asset.name"
       />
       <i
+        v-if="canDel"
         class="l-flex__none c-slibing-item el-icon-delete has-active u-pointer"
         @click="onDel"
       />
@@ -35,7 +33,7 @@
 import { getThumbnailUrl } from '@/api/asset'
 
 export default {
-  name: 'PreloadAsset',
+  name: 'AssetCard',
   props: {
     asset: {
       type: Object,
@@ -67,14 +65,22 @@ export default {
     status () {
       return this.asset.status
     },
-    keyName () {
-      return this.asset.keyName
+    canDel () {
+      return this.status === -1 || this.status === 2
     },
-    styles () {
+    placehoder () {
+      switch (true) {
+        case this.isVideo:
+          return 'video-bg'
+        case this.isWeb:
+          return 'web'
+        default:
+          return null
+      }
+    },
+    src () {
       return this.isImage
-        ? {
-          'background-image': `url("${getThumbnailUrl(this.keyName)}")`
-        }
+        ? getThumbnailUrl(this.asset.keyName)
         : null
     }
   },
@@ -92,6 +98,9 @@ export default {
       }
     },
     onDel () {
+      if (!this.canDel) {
+        return
+      }
       this.$emit('del', this.asset)
     }
   }
@@ -105,8 +114,9 @@ export default {
   &__img {
     position: relative;
     padding-top: 56.25%;
+    color: $info;
     border-radius: $radius;
-    background-color: #000;
+    background-color: rgba(#000, 0.8);
     background-position: center center;
     background-size: contain;
     background-repeat: no-repeat;
@@ -114,10 +124,10 @@ export default {
 
   &__thumb {
     position: absolute;
-    top: 10%;
-    left: 10%;
-    width: 80%;
-    height: 80%;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
   }
 
   &__refresh {

+ 228 - 37
src/views/device/detail/components/DeviceTakeOver/index.vue

@@ -23,48 +23,129 @@
           连接设备
         </button>
       </div>
-      <el-tabs
-        v-model="active"
-        class="c-tabs has-bottom-padding"
-      >
-        <el-tab-pane
-          label="已下载"
-          name="loaded"
-        />
-        <el-tab-pane
-          label="下载中"
-          name="loading"
-        />
-        <el-tab-pane
-          label="异常"
-          name="error"
-        />
-      </el-tabs>
-      <div class="l-flex__fill l-grid--info mini u-overflow-y--auto">
-        <asset
-          v-for="asset in list"
-          :key="asset.keyName"
-          :asset="asset"
-          @play="onPlay"
-          @reload="onReload"
-          @del="onDel"
-        />
-      </div>
+      <template v-if="valid">
+        <div class="l-flex--row c-sibling-item--v">
+          <button
+            class="c-sibling-item o-button"
+            @click="onAddAsset"
+          >
+            <i class="o-button__icon el-icon-circle-plus-outline" />
+            缓存资源
+          </button>
+          <button
+            class="c-sibling-item o-button"
+            @click="onAddWeb"
+          >
+            <i class="o-button__icon el-icon-circle-plus-outline" />
+            缓存网页
+          </button>
+        </div>
+        <el-tabs
+          v-model="active"
+          class="c-sibling-item--v c-tabs has-bottom-padding"
+        >
+          <el-tab-pane
+            name="loaded"
+            label="已下载"
+          />
+          <el-tab-pane
+            label="下载中"
+            name="loading"
+          />
+          <el-tab-pane
+            label="异常"
+            name="error"
+          />
+        </el-tabs>
+        <div class="l-flex__fill l-grid--info mini u-overflow-y--auto">
+          <asset-card
+            v-for="asset in list"
+            :key="asset.keyName"
+            :asset="asset"
+            @play="onPlay"
+            @reload="onReload"
+            @del="onDel"
+          />
+        </div>
+        <confirm-dialog
+          ref="assetsDialog"
+          title="资源列表"
+          size
+          append-to-body
+          @confirm="onSaveAssets"
+          @cancel="showAssets = false"
+        >
+          <template v-if="showAssets">
+            <grid-table
+              ref="gridTable"
+              :schema="assetSchema"
+            >
+              <grid-table-item v-slot="item">
+                <media-card
+                  class="o-card"
+                  :source="item"
+                  @click="onToggleGrid"
+                >
+                  <el-checkbox
+                    v-model="item.selected"
+                    class="o-card__checkbox"
+                  />
+                  <i
+                    class="o-card__icon el-icon-video-play has-active u-pointer"
+                    @click.stop="onView(item)"
+                  />
+                </media-card>
+              </grid-table-item>
+            </grid-table>
+          </template>
+        </confirm-dialog>
+        <confirm-dialog
+          ref="webDialog"
+          title="网页配置"
+          @confirm="onSaveWeb"
+        >
+          <div class="c-grid-form medium u-align-self--center">
+            <span class="c-grid-form__label required">名称:</span>
+            <el-input
+              v-model.trim="web.name"
+              class="c-grid-form__option"
+              clearable
+            />
+            <span class="c-grid-form__label required">网址:</span>
+            <el-input
+              v-model.trim="web.keyName"
+              class="c-grid-form__option"
+              clearable
+            />
+          </div>
+        </confirm-dialog>
+        <preview-dialog ref="previewDialog" />
+      </template>
     </div>
   </div>
 </template>
 
 <script>
+import {
+  getAssets,
+  getThumbnailUrl
+} from '@/api/asset'
+import {
+  State,
+  AssetType
+} from '@/constant'
 import {
   Topic,
+  MediaType,
+  MediaStatus,
   takeOver
 } from './takeover'
-import Asset from './components/Asset'
+import AssetCard from './components/AssetCard'
 
 export default {
   name: 'DeviceTakeOver',
   components: {
-    Asset
+    AssetCard
   },
   props: {
     device: {
@@ -82,18 +163,33 @@ export default {
       valid: false,
       messages: [],
       active: 'loaded',
-      assets: []
+      assets: [],
+      showAssets: false,
+      sources: [],
+      assetSchema: {
+        condition: { originalName: '', type: AssetType.IMAGE, status: State.RESOLVED },
+        list: getAssets,
+        transform: this.transformAsset,
+        filters: [
+          { key: 'type', type: 'select', options: [
+            { value: AssetType.IMAGE, label: '图片' },
+            { value: AssetType.VIDEO, label: '视频' }
+          ] },
+          { key: 'originalName', type: 'search', placeholder: '媒资名称' }
+        ]
+      },
+      web: {}
     }
   },
   computed: {
     list () {
       switch (this.active) {
         case 'loaded':
-          return this.assets.filter(({ status }) => status === 2)
+          return this.assets.filter(({ status }) => status === MediaStatus.LOADED)
         case 'loading':
-          return this.assets.filter(({ status }) => status === 0 || status === 1)
+          return this.assets.filter(({ status }) => status === MediaStatus.WAITING || status === MediaStatus.LOADING)
         case 'error':
-          return this.assets.filter(({ status }) => status === -1)
+          return this.assets.filter(({ status }) => status === MediaStatus.ERROR)
         default:
           return this.assets
       }
@@ -137,6 +233,7 @@ export default {
     onClose () {
       this.valid = false
       this.loading = false
+      this.showAssets = false
       this.$client = null
       this.$clientProxy = null
     },
@@ -162,15 +259,85 @@ export default {
     onPlay ({ type, name, keyName }) {
       this.$clientProxy.play({ type, name, keyName })
     },
+    transformAsset ({ type, originalName, keyName, thumbnail, size, md5 }) {
+      const asset = {
+        selected: false,
+        type,
+        name: originalName,
+        keyName,
+        size,
+        md5,
+        thumbnail
+      }
+      if (thumbnail) {
+        asset.thumb = getThumbnailUrl(thumbnail)
+      } else {
+        asset.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
+      }
+      return asset
+    },
+    onAddAsset () {
+      this.showAssets = true
+      this.$refs.assetsDialog.show()
+    },
+    onToggleGrid (asset) {
+      asset.selected = !asset.selected
+    },
+    onSaveAssets (done) {
+      const assets = this.$refs.gridTable.getData().filter(({ selected }) => selected)
+      if (assets.length) {
+        this.sources = this.sources.concat(assets)
+        this.$clientProxy.preload(assets.map(({ type, name, keyName, size, md5 }) => {
+          return { type, name, keyName, size, md5 }
+        }))
+      }
+      this.showAssets = false
+      done()
+    },
+    onView ({ type, keyName }) {
+      this.$refs.previewDialog.show({ type, url: keyName })
+    },
+    onAddWeb () {
+      this.web = {
+        name: '',
+        keyName: ''
+      }
+      this.$refs.webDialog.show()
+    },
+    onSaveWeb (done) {
+      const { name, keyName } = this.web
+      if (!name) {
+        this.$message({
+          type: 'warning',
+          message: '名称不能为空'
+        })
+        return
+      }
+      if (!keyName) {
+        this.$message({
+          type: 'warning',
+          message: '网址不能为空'
+        })
+        return
+      }
+      this.$clientProxy.preload([{ type: MediaType.WEB, name, keyName }])
+      done()
+    },
     onReload (asset) {
       const { status, ...data } = asset
       this.$clientProxy.preload([data])
     },
     onReloadReply (assets) {
-      assets.forEach(({ keyName }) => {
-        const index = this.assets.findIndex(asset => asset.keyName === keyName)
+      assets.forEach(asset => {
+        const targetKeyName = asset.keyName
+        const index = this.assets.findIndex(({ keyName }) => targetKeyName === keyName)
         if (~index) {
-          this.assets[index].status = 0
+          this.assets[index].status = MediaStatus.WAITING
+        } else {
+          this.assets.push({
+            status: asset.type === MediaType.WEB ? MediaStatus.LOADED : MediaStatus.WAITING,
+            ...asset
+          })
         }
       })
     },
@@ -189,7 +356,10 @@ export default {
     onDownload ({ success, asset: { keyName } }) {
       const index = this.assets.findIndex(asset => asset.keyName === keyName)
       if (~index) {
-        this.assets[index].status = success ? 2 : -1
+        this.assets[index].status = success ? MediaStatus.LOADED : MediaStatus.ERROR
+      } else if (!this.loading) {
+        this.loading = true
+        this.$clientProxy.pull()
       }
     }
   }
@@ -206,4 +376,25 @@ export default {
   line-height: 24px;
   border-right: 1px solid $gray--light;
 }
+
+.o-card {
+  &__checkbox {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    pointer-events: none;
+    z-index: 9;
+  }
+
+  &__icon {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    padding: 2px;
+    font-size: 24px;
+    border-radius: 50%;
+    background-color: rgba(#000, 0.4);
+    transform: translate(-50%, -50%);
+  }
+}
 </style>

+ 32 - 3
src/views/device/detail/components/DeviceTakeOver/takeover.js

@@ -1,5 +1,9 @@
 import { getAssets } from '@/api/asset'
 import store from '@/store'
+import {
+  State,
+  AssetType
+} from '@/constant'
 import {
   createClient,
   decodePayload,
@@ -17,6 +21,19 @@ export const Topic = {
   DOWNLOAD: 'download'
 }
 
+export const MediaType = {
+  IMAGE: 1,
+  VIDEO: 2,
+  WEB: 3
+}
+
+export const MediaStatus = {
+  ERROR: -1,
+  WAITING: 0,
+  LOADING: 1,
+  LOADED: 2
+}
+
 export function takeOver (device, { onMessage, onCreated, onClose, debug }) {
   const { id, productId } = device
 
@@ -83,14 +100,26 @@ export function takeOver (device, { onMessage, onCreated, onClose, debug }) {
       }
       this[event](messageId, payload)
     },
-    preload (messageId) {
+    preload (messageId, payload) {
       onMqttMessage(Topic.PRELOAD, { messageId })
+      setTimeout(() => {
+        payload.assets.forEach(asset => {
+          if (asset.type !== MediaType.WEB) {
+            onMqttMessage(Topic.DOWNLOAD, {
+              messageId: createMessageId,
+              asset,
+              complete: true,
+              success: [true, false][Math.random() * 2 | 0]
+            })
+          }
+        })
+      }, 2000)
     },
     delete (messageId) {
       onMqttMessage(Topic.DEL, { messageId, failures: [] })
     },
     pull (messageId) {
-      getAssets({ pageNum: 1, pageSize: 10, type: 1, status: 2 }).then(({ data }) => {
+      getAssets({ pageNum: 1, pageSize: 10, type: [AssetType.IMAGE, AssetType.VIDEO][Math.random() * 2 | 0], status: State.RESOLVED }).then(({ data }) => {
         onMqttMessage(Topic.PULL, {
           messageId,
           assets: data.map(({ type, originalName, keyName, size, md5 }) => {
@@ -100,7 +129,7 @@ export function takeOver (device, { onMessage, onCreated, onClose, debug }) {
               keyName,
               size,
               md5,
-              status: [-1, 1, 2, 0][Math.random() * 3 | 0]
+              status: [MediaStatus.ERROR, MediaStatus.LOADING, MediaStatus.LOADED, MediaStatus.WAITING][Math.random() * 3 | 0]
             }
           })
         }, true, true)

+ 1 - 1
src/views/device/detail/index.vue

@@ -105,7 +105,7 @@ export default {
         this.accessSet.has(Access.MANAGE_DEVICE) ? { key: 'DeviceInvoke', label: '设备操控' } : null,
         __SENSOR__ ? { key: 'Sensors', label: '传感器' } : null,
         { key: 'DeviceExternal', label: '全链路监测' },
-        { key: 'DeviceTakeOver', label: '接管' }
+        this.accessSet.has(Access.MANAGE_GROUP) ? { key: 'DeviceTakeOver', label: '接管' } : null
       ].filter(Boolean)
     }
   },

+ 1 - 1
src/views/realm/debug/simulator/index.vue

@@ -66,7 +66,7 @@ export default {
   name: 'Simulate',
   data () {
     return {
-      sn: '0YK0Y42101000003',
+      sn: '',
       valid: false,
       loading: false,
       messages: []