Parcourir la source

refactor(program): program ast pages style

Casper Dai il y a 3 ans
Parent
commit
757de6b8a9
36 fichiers modifiés avec 807 ajouts et 694 suppressions
  1. 0 3
      src/components/dialog/PreviewDialog/index.vue
  2. 1 1
      src/scss/base/_cover.scss
  3. 8 4
      src/scss/bem/_component.scss
  4. 16 32
      src/scss/bem/_object.scss
  5. 296 337
      src/views/bigscreen/ast/Designer.vue
  6. 116 149
      src/views/bigscreen/ast/Viewer.vue
  7. 1 1
      src/views/bigscreen/ast/components/Card.vue
  8. 1 2
      src/views/bigscreen/ast/components/Volume.vue
  9. 211 0
      src/views/bigscreen/ast/components/WidgetShortcut.vue
  10. 2 24
      src/views/bigscreen/ast/core/components/DynamicItem.vue
  11. 0 6
      src/views/bigscreen/ast/core/config-json/base.js
  12. 3 3
      src/views/bigscreen/ast/core/config-json/image.js
  13. 3 3
      src/views/bigscreen/ast/core/config-json/live.js
  14. 3 3
      src/views/bigscreen/ast/core/config-json/marquee.js
  15. 3 3
      src/views/bigscreen/ast/core/config-json/text.js
  16. 3 3
      src/views/bigscreen/ast/core/config-json/time.js
  17. 3 3
      src/views/bigscreen/ast/core/config-json/video.js
  18. 3 3
      src/views/bigscreen/ast/core/config-json/weather.js
  19. 3 3
      src/views/bigscreen/ast/core/config-json/web.js
  20. 10 0
      src/views/bigscreen/ast/core/constant.js
  21. 34 20
      src/views/bigscreen/ast/core/utils.js
  22. 2 1
      src/views/bigscreen/ast/core/widget/CImage.vue
  23. 4 3
      src/views/bigscreen/ast/core/widget/CLive.vue
  24. 3 1
      src/views/bigscreen/ast/core/widget/CMarquee.vue
  25. 9 2
      src/views/bigscreen/ast/core/widget/CText.vue
  26. 7 2
      src/views/bigscreen/ast/core/widget/CTime.vue
  27. 2 1
      src/views/bigscreen/ast/core/widget/CVideo.vue
  28. 7 2
      src/views/bigscreen/ast/core/widget/CWeather.vue
  29. 5 5
      src/views/bigscreen/ast/core/widget/CWeb.vue
  30. 4 27
      src/views/bigscreen/ast/core/widget/Widget.vue
  31. 3 40
      src/views/bigscreen/ast/core/widget/WidgetViewer.vue
  32. 32 0
      src/views/bigscreen/ast/core/widget/widget.js
  33. 6 4
      src/views/bigscreen/ast/mixin.js
  34. 1 1
      src/views/device/detail/components/DeviceInvoke/index.vue
  35. 1 1
      src/views/device/detail/components/DeviceRuntime/index.vue
  36. 1 1
      src/views/device/detail/components/external/Sensors/index.vue

+ 0 - 3
src/components/dialog/PreviewDialog/index.vue

@@ -61,9 +61,6 @@ export default {
     show (source) {
       this.source = source
       this.showDetail = true
-    },
-    onClose () {
-      this.$emit('close')
     }
   }
 }

+ 1 - 1
src/scss/base/_cover.scss

@@ -4,7 +4,7 @@
 }
 
 ::-webkit-scrollbar-track-piece {
-  background-color: #d3dce6;
+  background-color: transparent;
 }
 
 ::-webkit-scrollbar-thumb {

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

@@ -167,19 +167,23 @@
 
 .c-info-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+  grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
   grid-template-rows: max-content;
   grid-row-gap: $spacing;
   grid-column-gap: $spacing;
   align-items: start;
 
-  &.less {
-    grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
+  &.medium {
+    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
   }
 
-  &.more {
+  &.small {
     grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
   }
+
+  &.mini {
+    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+  }
 }
 
 .c-grid-form {

+ 16 - 32
src/scss/bem/_object.scss

@@ -15,6 +15,11 @@
   user-select: none;
   cursor: pointer;
 
+  &.mini {
+    height: 32px;
+    padding: 0 12px;
+  }
+
   &:hover {
     background-color: rgba($blue, .8);
   }
@@ -85,44 +90,23 @@
 
 .o-next {
   display: inline-block;
-  position: relative;
-  width: 2em;
-  height: 2em;
-  font-size: 10px;
-  border: .2em solid currentcolor;
-  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  text-align: center;
 
   &::before {
     content: '';
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 50%;
-    margin-top: auto;
-    margin-bottom: auto;
-    margin-left: -.4em;
-    width: 0;
-    height: 0;
-    border-top: .45em solid transparent;
-    border-bottom: .45em solid transparent;
-    border-left: .6em solid currentcolor;
+    display: inline-block;
+    border-top: .5em solid transparent;
+    border-bottom: .5em solid transparent;
+    border-left: .68em solid currentcolor;
   }
 
   &::after {
-    box-sizing: border-box;
-    content: "";
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 50%;
-    margin-top: auto;
-    margin-bottom: auto;
-    margin-left: .05em;
-    width: 0;
-    height: 0;
-    border-top: .45em solid transparent;
-    border-bottom: .45em solid transparent;
-    border-left: .6em solid currentcolor;
+    content: '';
+    display: inline-block;
+    height: 1em;
+    border-left: .1em solid currentcolor;
   }
 }
 

+ 296 - 337
src/views/bigscreen/ast/Designer.vue

@@ -2,7 +2,7 @@
   <div
     v-loading.lock="loading"
     element-loading-background="rgba(0, 0, 0, 0.8)"
-    class="c-designer"
+    class="l-flex--col c-designer"
     @contextmenu.prevent
   >
     <audio
@@ -13,113 +13,96 @@
       @ended="onAudioEnded"
       @error="onAudioError"
     />
-    <div class="c-designer__side c-side">
-      <div class="c-side__tool">
-        <div
-          class="c-side__item"
-          :class="{ active: tabIndex === 0 }"
-          @click="tabIndex = 0"
-        >
-          工具栏
-        </div>
-        <div
-          v-if="hasWidgets"
-          class="c-side__item"
-          :class="{ active: tabIndex === 1 }"
-          @click="tabIndex = 1"
-        >
-          图层
-        </div>
-      </div>
+    <div class="l-flex__none l-flex--row c-designer__header">
+      <i
+        class="c-designer__shortcut el-icon-arrow-left u-bold u-pointer"
+        @click="onBack"
+      />
+      <span class="c-designer__name u-bold u-ellipsis">{{ program.name }}</span>
+      <span>{{ program.resolutionRatio }}</span>
+      <div class="l-flex__fill c-sibling-item" />
       <div
-        v-show="tabIndex === 0"
-        class="c-side__content"
-        @dragstart="widgetOnDragStart"
-        @dragend="widgetOnDragEnd"
+        v-if="hasNext"
+        class="l-flex--row center c-sibling-item c-designer__shortcut u-pointer"
+        @click="switchBgm"
       >
-        <div
-          v-for="cfg in widgetConfigs"
-          :key="cfg.key"
-          :data-type="cfg.type"
-          class="c-widget"
-          draggable
-        >
-          <span class="c-widget__icon">
-            <i :class="cfg.icon" />
-          </span>
-          <span class="c-widget__text u-ellipsis">{{ cfg.label }}</span>
-        </div>
+        <i class="o-next" />
       </div>
       <div
-        v-show="tabIndex === 1"
-        class="c-side__content"
+        v-if="hasAudio"
+        class="l-flex--row center c-sibling-item c-designer__shortcut u-pointer"
+        @click="toggleMute"
       >
-        <div
-          v-for="(layer, index) in layers"
-          :key="layer.id"
-          class="c-widget"
-          :class="{ active: layer.id === selectedWidgetId }"
-          @mousedown="onLayerClick($event, index)"
-        >
-          <span class="c-widget__icon">
-            <i :class="layer.icon" />
-          </span>
-          <span
-            class="c-widget__text"
-            :title="layer.name"
-          >
-            {{ layer.name }}
-          </span>
-        </div>
+        <volume :muted="muted" />
       </div>
+      <button
+        v-if="hasWidgets"
+        class="c-sibling-item o-button mini"
+        @click="onClear"
+      >
+        <i class="o-button__icon el-icon-delete" />
+        清空
+      </button>
+      <button
+        class="c-sibling-item o-button mini"
+        @click="onSave"
+      >
+        <i class="o-button__icon iconfont iconsave" />
+        保存
+      </button>
     </div>
-    <div class="c-designer__main">
-      <div class="l-flex--row c-designer__tool">
-        <i
-          class="c-designer__btn el-icon-arrow-left u-bold"
-          @click="onBack"
-        />
-        <div class="l-flex--row">
-          <span class="c-sibling-item c-designer__name u-bold u-ellipsis">{{ program.name }}</span>
-          <span class="c-sibling-item c-designer__ratio">{{ program.resolutionRatio }}</span>
+    <div class="l-flex__fill l-flex">
+      <div class="c-designer__side left c-side">
+        <div class="c-side__tool">
+          <div class="c-side__item">
+            组件
+          </div>
         </div>
-        <el-tooltip
-          key="save"
-          class="c-designer__btn"
-          content="保存"
-        >
-          <i
-            class="iconfont iconsave"
-            @click="onSave"
-          />
-        </el-tooltip>
-        <el-tooltip
-          v-if="hasWidgets"
-          key="del"
-          class="c-designer__btn"
-          content="清空"
-        >
-          <i
-            class="iconfont iconlajitong"
-            @click="onClear"
-          />
-        </el-tooltip>
-        <el-tooltip
-          key="scale"
-          class="c-designer__btn"
-          content="缩放"
-          :hide-after="2000"
+        <el-scrollbar
+          class="c-side__scrollbar"
+          native
         >
-          <div
-            class="o-scale-slider"
-            @click="toScale"
-          >
+          <div class="c-side__content mini">
             <div
-              class="l-flex--row o-scale-slider__wrapper"
-              :class="{ expand: dragScale }"
+              v-for="(widget, index) in layers"
+              ref="widgetElements"
+              :key="widget.id"
+              class="o-layer"
+              :class="{ active: widget.id === selectedWidgetId }"
+              @mousedown="onLayerClick($event, index)"
             >
+              <widget-shortcut
+                class="dark"
+                :widget="widget"
+                @view="onViewAsset"
+              />
+            </div>
+            <div
+              v-if="node"
+              ref="rootElement"
+              key="root"
+              class="o-layer"
+              :class="{ active: !selectedWidgetId }"
+              @click="onRootClick"
+            >
+              <widget-shortcut
+                class="dark"
+                :widget="node"
+                :custom-style="backgroundStyles"
+                source-key="bgm"
+                background
+                @view="onViewAsset"
+              />
+            </div>
+          </div>
+        </el-scrollbar>
+      </div>
+      <div class="c-designer__main">
+        <div class="l-flex--row c-designer__tool">
+          <div class="o-scale-slider">
+            <div class="l-flex--row o-scale-slider__wrapper">
               <i
-                class="el-icon-zoom-out"
+                class="el-icon-zoom-out u-pointer"
                 @click="scaleDown"
               />
               <el-slider
@@ -127,109 +110,99 @@
                 class="o-scale-slider__slider"
                 :min="minScale"
                 :max="maxScale"
-                @change="toScale"
               />
               <i
-                class="el-icon-zoom-in"
+                class="el-icon-zoom-in u-pointer"
                 @click="scaleUp"
               />
             </div>
-            <svg-icon
-              v-if="dragScale"
-              icon-class="exit-fullscreen"
-              @click.stop="hideScale"
-            />
-            <svg-icon
-              v-else
-              icon-class="fullscreen"
-            />
           </div>
-        </el-tooltip>
-        <el-tooltip
-          v-if="hasAudio"
-          key="audio"
-          class="c-designer__btn"
-          content="音效"
-        >
-          <div @click="toggleMute">
-            <volume :muted="muted" />
-          </div>
-        </el-tooltip>
-        <el-tooltip
-          v-if="hasNext"
-          key="toggle-audio"
-          class="c-designer__btn"
-          content="切歌"
-        >
-          <div @click="switchBgm">
-            <i class="o-next" />
+          <div
+            class="l-flex__none l-flex--row"
+            @dragstart="widgetOnDragStart"
+            @dragend="widgetOnDragEnd"
+          >
+            <div
+              v-for="cfg in widgetConfigs"
+              :key="cfg.key"
+              :data-type="cfg.type"
+              class="o-widget-cfg"
+              draggable
+            >
+              <span class="o-widget-cfg__icon">
+                <i :class="cfg.icon" />
+              </span>
+              <span class="o-widget-cfg__text">{{ cfg.label }}</span>
+            </div>
           </div>
-        </el-tooltip>
-      </div>
-      <div
-        ref="wrapper"
-        class="c-designer__content"
-        @mousedown="onScreenClick"
-      >
+        </div>
         <div
-          class="c-designer__wrapper"
-          :style="wrapperStyles"
+          ref="wrapper"
+          class="c-designer__content"
+          @mousedown="onRootClick"
         >
           <div
-            ref="canvas"
-            class="c-designer__canvas"
-            :style="[transformStyles, styles]"
-            @dragover.prevent
-            @drop.prevent="widgetOnDrop"
+            class="c-designer__wrapper"
+            :style="wrapperStyles"
           >
             <div
-              class="c-designer__background has-bg"
-              :style="backgroundStyles"
-            />
-            <div
-              v-show="grid"
-              class="c-designer__grid"
-            />
-            <widget
-              v-for="(item, index) in widgets"
-              :key="item.id"
-              ref="widgets"
-              :scale="100 / scale"
-              :node="item"
-              :root="node"
-              @focus="onWidgetFocus(index)"
-              @blur="onWidgetBlur"
-              @menu="onWidgetMenu($event, index)"
-            />
+              ref="canvas"
+              class="c-designer__canvas"
+              :style="[transformStyles, styles]"
+              @dragover.prevent
+              @drop.prevent="widgetOnDrop"
+            >
+              <div
+                class="c-designer__background has-bg"
+                :style="backgroundStyles"
+              />
+              <div
+                v-show="grid"
+                class="c-designer__grid"
+              />
+              <widget
+                v-for="(item, index) in widgets"
+                :key="item.id"
+                ref="widgets"
+                :scale="100 / scale"
+                :node="item"
+                :root="node"
+                @focus="onWidgetFocus(index)"
+                @blur="onWidgetBlur"
+                @menu="onWidgetMenu($event, index)"
+              />
+            </div>
           </div>
         </div>
       </div>
-    </div>
-    <div class="c-designer__side large c-side">
-      <div class="c-side__tool">
+      <div class="c-designer__side right c-side">
+        <div class="c-side__tool">
+          <div
+            v-for="(tab, index) in dynamicOptions"
+            :key="index"
+            class="c-side__item u-pointer"
+            :class="{ active: optionIndex === index }"
+            @click="optionIndex = index"
+          >
+            {{ tab.label }}
+          </div>
+        </div>
         <div
           v-for="(tab, index) in dynamicOptions"
+          v-show="optionIndex === index"
           :key="index"
-          class="c-side__item"
+          class="c-side__scrollbar c-side__content u-overflow-y--auto"
           :class="{ active: optionIndex === index }"
-          @click="optionIndex = index"
-        >{{ tab.label }}</div>
-      </div>
-      <div
-        v-for="(tab, index) in dynamicOptions"
-        v-show="optionIndex === index"
-        :key="index"
-        class="c-side__content"
-        :class="{ active: optionIndex === index }"
-      >
-        <dynamic-item
-          v-for="item in tab.list"
-          :key="item.key"
-          :root="node"
-          :node="widget"
-          :attr="item"
-          @choose="onEditData"
-        />
+        >
+          <dynamic-item
+            v-for="item in tab.list"
+            :key="item.key"
+            :root="node"
+            :node="widget"
+            :attr="item"
+            @choose="onEditData"
+          />
+        </div>
       </div>
     </div>
     <content-menu
@@ -245,14 +218,14 @@
     <el-dialog
       :visible.sync="showAssets"
       :title="assetDialogType"
-      custom-class="c-dialog"
+      custom-class="c-dialog large"
       :close-on-click-modal="false"
       :before-close="onCloseAssetsDialog"
     >
       <draggable
         v-if="showAssets"
         v-model="sources"
-        class="c-grid"
+        class="c-info-grid mini"
         :class="{ dragging }"
         animation="300"
         @start="onSourceDragStart"
@@ -345,12 +318,14 @@
         </grid-table>
       </template>
     </confirm-dialog>
-    <preview-dialog ref="previewDialog" />
+    <preview-dialog
+      ref="previewDialog"
+      @close="onClosePreview"
+    />
   </div>
 </template>
 
 <script>
-import Draggable from 'vuedraggable'
 import domToImage from 'dom-to-image'
 import { updateProgram } from '@/api/program'
 import {
@@ -365,13 +340,13 @@ import {
   widgets,
   normalize,
   getOptions,
-  getIcon,
   copy,
   fix,
   getDuration,
   toJSON
 } from './core/utils'
 import mixin from './mixin'
+import Draggable from 'vuedraggable'
 import Widget from './core/widget/Widget.vue'
 import ContentMenu from './core/components/ContentMenu.vue'
 import DynamicItem from './core/components/DynamicItem.vue'
@@ -379,8 +354,8 @@ import DynamicItem from './core/components/DynamicItem.vue'
 export default {
   name: 'BigScreenDesigner',
   components: {
-    Widget,
     Draggable,
+    Widget,
     ContentMenu,
     DynamicItem
   },
@@ -389,8 +364,7 @@ export default {
     return {
       loading: false,
       snapping: false,
-      padding: 10,
-      tabIndex: 0,
+      padding: 16,
       optionIndex: 0,
       widgetConfigs: widgets,
       grid: false,
@@ -424,16 +398,7 @@ export default {
       return []
     },
     layers () {
-      const layers = []
-      for (let i = this.widgets.length - 1; i >= 0; i--) {
-        const { id, type, layerName } = this.widgets[i]
-        layers.push({
-          id: id,
-          icon: getIcon(type),
-          name: layerName
-        })
-      }
-      return layers
+      return this.widgets.slice().reverse()
     },
     assetType () {
       if (this.widgetAttr?.type === 'data') {
@@ -450,14 +415,14 @@ export default {
         case AssetType.AUDIO:
           return '音频'
         default:
-          return '数据'
+          return '媒资'
       }
     },
     assetDialogTitle () {
       return `请选择${this.assetDialogType}`
     },
     selectedWidgetId () {
-      return this.widget.id
+      return this.widget?.id
     },
     wrapperStyles () {
       return this.node ? {
@@ -477,6 +442,15 @@ export default {
         id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(true)
       }
       this.optionIndex = 0
+    },
+    widget () {
+      this.node && setTimeout(() => {
+        if (this.selectedWidgetIndex === -1) {
+          this.$refs.rootElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
+        } else {
+          this.$refs.widgetElements.find(element => element.classList.contains('active')).scrollIntoView({ behavior: 'smooth', block: 'start' })
+        }
+      })
     }
   },
   methods: {
@@ -485,7 +459,6 @@ export default {
         '清空后所有数据将丢失',
         '清空画布'
       ).then(() => {
-        this.tabIndex = 0
         this.selectedWidgetIndex = -1
         const { width, height } = this.node
         this.initCanvas({ width, height })
@@ -493,6 +466,7 @@ export default {
     },
     widgetOnDragStart (evt) {
       evt.dataTransfer.setData('type', evt.target.dataset.type)
+      console.log(evt.dataTransfer)
       this.dragging = true
     },
     widgetOnDragEnd () {
@@ -532,7 +506,7 @@ export default {
       this.selectedWidgetIndex = this.widgets.length - 1 - index
       this.onRightClick(evt)
     },
-    onScreenClick () {
+    onRootClick () {
       this.selectedWidgetIndex = -1
     },
     onRightClick (evt) {
@@ -646,16 +620,7 @@ export default {
       if (thumbnail) {
         asset.thumbnailUrl = getThumbnailUrl(thumbnail)
       } else {
-        switch (type) {
-          case AssetType.VIDEO:
-            asset.icon = 'video-bg'
-            break
-          case AssetType.AUDIO:
-            asset.icon = 'audio-bg'
-            break
-          default:
-            break
-        }
+        asset.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
       }
       return asset
     },
@@ -685,7 +650,7 @@ export default {
       }
       if (data.thumbnail) {
         source.thumbnailUrl = getThumbnailUrl(data.thumbnail)
-      } else if (data.type === AssetType.IMAGE) {
+      } else if (source.type === AssetType.IMAGE) {
         source.thumbnailUrl = getThumbnailUrl(data.keyName)
       } else {
         switch (source.type) {
@@ -728,8 +693,15 @@ export default {
       this.onViewAsset({ type: type || this.widgetAttr.options.type, url: keyName })
     },
     onViewAsset (asset) {
+      this.$muted = this.muted
+      if (!this.muted) {
+        this.muted = true
+      }
       this.$refs.previewDialog.show(asset)
     },
+    onClosePreview () {
+      this.muted = this.$muted
+    },
     onDelAsset (index) {
       this.$confirm(
         '移除该数据?',
@@ -744,12 +716,6 @@ export default {
     onSourceDragEnd () {
       this.dragging = false
     },
-    toScale () {
-      this.dragScale = true
-    },
-    hideScale () {
-      this.dragScale = false
-    },
     scaleDown () {
       this.scale = Math.max(this.scale - 10, this.minScale)
     },
@@ -840,19 +806,64 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-$drak: #242a30;
+$theme: #181b23;
+$dark: #121418;
+$light: #1f232e;
+$hover: lighten($theme, 10%);
+$active: darken($theme, 5%);
+$border: #242835;
 
 .c-designer {
-  display: flex;
   height: 100%;
+  min-width: 1080px;
+  min-height: 600px;
+  background-color: $theme;
+  overflow: hidden;
+
+  &__header {
+    height: 53px;
+    padding-left: 10px;
+    color: $info;
+    font-size: 12px;
+    line-height: 1;
+    border-bottom: 1px solid $border;
+  }
+
+  &__shortcut {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 36px;
+    height: 36px;
+    font-size: 18px;
+    border-radius: 50%;
+    transition: background-color 0.4s;
+
+    &:hover {
+      background-color: $hover;
+    }
+
+    &:active {
+      background-color: $active;
+    }
+  }
+
+  &__name {
+    max-width: 400px;
+    padding: 0 $spacing 0 6px;
+    font-size: 18px;
+  }
 
   &__side {
     flex: none;
-    width: 180px;
     min-height: 0;
 
-    &.large {
-      width: 240px;
+    &.left {
+      width: 144px;
+    }
+
+    &.right {
+      width: 196px;
     }
   }
 
@@ -867,48 +878,16 @@ $drak: #242a30;
   &__content {
     flex: 1 0 0;
     position: relative;
-    padding: 10px;
+    padding: 16px;
     overflow: auto;
-    background: url("~@/assets/dot.png") repeat;
+    background: $dark url("~@/assets/dot.png") 0 0 / 16px 16px repeat;
   }
 
   &__tool {
-    flex: 0 0 40px;
-    color: $gray--dark;
-    background-color: $drak;
-  }
-
-  &__name {
-    max-width: 400px;
-    font-size: 18px;
-    line-height: 1;
-  }
-
-  &__ratio {
-    font-size: 12px;
-    line-height: 1;
-  }
-
-  &__btn {
-    display: inline-flex;
-    justify-content: center;
-    align-items: center;
-    width: 55px;
-    height: 100%;
-    color: $gray;
-    cursor: pointer;
-
-    &:hover {
-      background-color: #191d22;
-    }
-
-    &:active {
-      background-color: darken(#191d22, 5%);
-    }
-
-    &.iconlajitong {
-      font-size: 20px;
-    }
+    flex: 0 0 60px;
+    color: $info;
+    border-left: 1px solid $border;
+    border-right: 1px solid $border;
   }
 
   &__wrapper {
@@ -920,17 +899,6 @@ $drak: #242a30;
     position: relative;
     background-color: #000;
     overflow: visible;
-
-    &::before {
-      content: "";
-      position: absolute;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      background-color: currentColor;
-      z-index: -1;
-    }
   }
 
   &__background {
@@ -939,6 +907,7 @@ $drak: #242a30;
     left: 0;
     right: 0;
     bottom: 0;
+    background-color: currentColor;
     z-index: -1;
   }
 
@@ -957,90 +926,95 @@ $drak: #242a30;
 .c-side {
   display: flex;
   flex-direction: column;
-  background-color: $drak;
 
   &__tool {
     flex: none;
     display: flex;
+    border-bottom: 1px solid $border;
+    background-color: $light;
   }
 
   &__item {
     flex: 1 0 0;
-    color: #909399;
-    font-size: 14px;
+    color: #5b6275;
+    font-size: 16px;
     text-align: center;
-    line-height: 40px;
-    background-color: #242f3b;
+    line-height: 60px;
     user-select: none;
-    cursor: pointer;
 
     &.active {
-      color: #409eff;
-      background-color: #31455d;
+      color: $blue;
+      background-color: $theme;
     }
   }
 
   &__content {
-    flex: 1 0 0;
-    min-height: 0;
-    padding: 15px;
-    overflow-y: auto;
-
-    &::-webkit-scrollbar {
-      width: 4px;
-      height: 4px;
-    }
+    padding: 10px 16px;
 
-    &::-webkit-scrollbar-track-piece {
-      background-color: #29405c;
-    }
-
-    &::-webkit-scrollbar-track {
-      box-shadow: 1px 1px 5px rgba(116, 148, 170, 0.5) inset;
+    &.mini {
+      padding: 10px;
     }
+  }
 
-    &::-webkit-scrollbar-thumb {
-      min-height: 20px;
-      background-clip: content-box;
-      box-shadow: 0 0 0 5px rgba(116, 148, 170, 0.5) inset;
-    }
+  &__scrollbar {
+    flex: 1 0 0;
+    min-height: 10px;
   }
 }
 
-.c-widget {
-  display: flex;
+.o-widget-cfg {
+  display: inline-flex;
+  flex-direction: column;
   align-items: center;
   position: relative;
-  width: 100%;
-  height: 48px;
-  margin-bottom: 1px;
-  padding: 0 6px;
-  color: #bfcbd9;
-  font-size: 12px;
   user-select: none;
   cursor: pointer;
 
+  & + & {
+    margin-left: 32px;
+  }
+
   &.active {
     background-color: #31455d;
   }
 
   &__icon {
     flex: none;
-    display: inline-block;
-    margin-right: 10px;
-    width: 52px;
-    height: 30px;
-    color: #409eff;
-    font-size: 16px;
-    line-height: 30px;
-    text-align: center;
-    border: 1px solid #3a4659;
-    background-color: #282a30;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 32px;
+    height: 32px;
+    color: #fff;
+    font-size: 22px;
+    border-radius: $radius;
+    background-color: $blue;
   }
 
   &__text {
-    flex: 1 1 auto;
-    min-width: 0;
+    margin-top: 6px;
+    color: #9fa5b8;
+    font-size: 12px;
+    line-height: 1;
+  }
+}
+
+.o-layer {
+  color: $gray--dark;
+  user-select: none;
+  border-radius: $radius;
+  background-color: #252d42;
+  overflow: hidden;
+  cursor: pointer;
+
+  & + & {
+    margin-top: 10px;
+  }
+
+  &.active {
+    color: #fff;
+    outline: 2px solid $blue;
+    background-color: $blue;
   }
 }
 
@@ -1095,17 +1069,6 @@ $drak: #242a30;
   }
 }
 
-.o-image {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background-position: center;
-  background-size: contain;
-  background-repeat: no-repeat;
-}
-
 .o-scale-slider {
   justify-content: flex-start;
   min-width: 48px;
@@ -1114,13 +1077,9 @@ $drak: #242a30;
   font-size: 20px;
 
   &__wrapper {
-    width: 0;
+    width: 184px;
     transition: width 0.1s;
     overflow: hidden;
-
-    &.expand {
-      width: 184px;
-    }
   }
 
   &__slider {

+ 116 - 149
src/views/bigscreen/ast/Viewer.vue

@@ -11,29 +11,25 @@
     />
     <div class="l-flex__none l-flex--row c-viewer__header">
       <i
-        class="c-viewer__back el-icon-arrow-left u-bold u-pointer"
+        class="c-viewer__shortcut el-icon-arrow-left u-bold u-pointer"
         @click="onBack"
       />
-      <span class="c-sibling-item c-viewer__name u-bold u-ellipsis">{{ program.name }}</span>
+      <span class="c-viewer__name u-bold u-ellipsis">{{ program.name }}</span>
       <span class="c-sibling-item">{{ program.resolutionRatio }}</span>
-      <el-tooltip
+      <div
         v-if="hasAudio"
-        class="o-tool-item"
-        content="音效"
+        class="c-sibling-item c-viewer__shortcut u-pointer"
+        @click="toggleMute"
       >
-        <div @click="toggleMute">
-          <volume :muted="muted" />
-        </div>
-      </el-tooltip>
-      <el-tooltip
+        <volume :muted="muted" />
+      </div>
+      <div
         v-if="hasNext"
-        class="o-tool-item"
-        content="切歌"
+        class="c-sibling-item c-viewer__shortcut u-pointer"
+        @click="switchBgm"
       >
-        <div @click="switchBgm">
-          <i class="o-next" />
-        </div>
-      </el-tooltip>
+        <i class="o-next" />
+      </div>
     </div>
     <div class="l-flex__fill l-flex">
       <div class="l-flex__none l-flex--col c-viewer__asserts">
@@ -41,36 +37,47 @@
           <span class="l-flex__auto u-bold">素材</span>
           <span class="l-flex__none c-viewer__count--info">{{ count }}</span>
         </div>
-        <div
-          v-if="sources"
-          class="l-flex__auto c-viewer__list u-overflow-y--auto"
+        <el-scrollbar
+          class="c-viewer__scrollbar"
+          native
         >
-          <div
-            v-for="(source, index) in sources"
-            :key="index"
-            class="c-viewer__item o-card u-pointer"
-            @click="onView(source)"
-          >
-            <div class="o-card__content">
-              <i
-                v-if="source.thumbnailUrl"
-                class="o-card__img"
-                :style="{ 'background-image': `url('${source.thumbnailUrl}')` }"
+          <div class="c-viewer__list">
+            <div
+              v-for="widget in layers"
+              ref="widgetElements"
+              :key="widget.id"
+              class="o-layer"
+              :class="{ active: widget.id === selectedWidgetId }"
+              @click="onWidgetClick(widget, $event)"
+            >
+              <widget-shortcut
+                :widget="widget"
+                @view="onView"
               />
-              <svg-icon
-                v-else-if="source.icon"
-                class="o-card__img"
-                :icon-class="source.icon"
+            </div>
+            <div
+              v-if="node"
+              ref="rootElement"
+              key="root"
+              class="o-layer"
+              :class="{ active: !selectedWidgetId }"
+              @click="onRootClick"
+            >
+              <widget-shortcut
+                :widget="node"
+                :custom-style="backgroundStyles"
+                source-key="bgm"
+                background
+                @view="onView"
               />
             </div>
-            <div class="o-card__name u-ellipsis">{{ source.name }}</div>
           </div>
-        </div>
+        </el-scrollbar>
       </div>
       <div
         ref="wrapper"
         class="c-viewer__content"
-        @click="onClickWidget()"
+        @click="onRootClick"
       >
         <div
           class="l-flex__fill c-viewer__canvas has-bg"
@@ -81,10 +88,10 @@
             :style="backgroundStyles"
           />
           <widget
-            v-for="(item, index) in widgets"
-            :key="`${item.type}${index}`"
+            v-for="item in widgets"
+            :key="item.id"
             :node="item"
-            @click.native.stop="onClickWidget(item)"
+            @click.native="onWidgetClick(item, $event)"
           />
         </div>
       </div>
@@ -97,8 +104,8 @@
 </template>
 
 <script>
-import { getThumbnailUrl } from '@/api/asset'
 import { AssetType } from '@/constant'
+import { WidgetType } from './core/constant'
 import mixin from './mixin'
 import Widget from './core/widget/WidgetViewer.vue'
 
@@ -110,73 +117,54 @@ export default {
   mixins: [mixin],
   data () {
     return {
-      sources: null
+      selectedWidget: null
     }
   },
   computed: {
+    layers () {
+      return this.widgets.filter(this.hasAssets).slice().reverse()
+    },
     count () {
-      return this.sources?.length
+      if (this.node) {
+        return this.node.bgm.length + this.node.backgroundImage.length + this.layers.reduce((total, { sources }) => total + sources.length, 0)
+      }
+      return 0
+    },
+    selectedWidgetId () {
+      return this.selectedWidget?.id
     }
   },
-  mounted () {
-    this.$item = null
-    this.onClickWidget()
+  watch: {
+    selectedWidget () {
+      this.node && setTimeout(() => {
+        if (this.selectedWidget) {
+          this.$refs.widgetElements?.find(element => element.classList.contains('active'))?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+        } else {
+          this.$refs.rootElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
+        }
+      })
+    }
   },
   methods: {
-    onClickWidget (item) {
-      if (this.$item === item) {
-        return
-      }
-      this.$item = item
-
-      if (!item) {
-        const arr = []
-        const bg = this.node.backgroundImage[0]
-        if (bg) {
-          arr.push({
-            type: AssetType.IMAGE,
-            url: bg.keyName,
-            name: bg.name,
-            thumbnailUrl: getThumbnailUrl(bg.keyName)
-          })
-        }
-        this.sources = arr.concat(this.node.bgm.map(source => {
-          return {
-            type: AssetType.AUDIO,
-            url: source.keyName,
-            name: source.name,
-            icon: 'audio-bg'
-          }
-        }))
-        return
+    hasAssets (widget) {
+      switch (widget.type) {
+        case WidgetType.IMAGE:
+        case WidgetType.VIDEO:
+          return true
+        default:
+          return false
       }
-
-      const sources = item.sources
-      if (sources) {
-        if (item.type === 'CImage') {
-          this.sources = sources.map(source => {
-            return {
-              type: AssetType.IMAGE,
-              url: source.keyName,
-              name: source.name,
-              thumbnailUrl: getThumbnailUrl(source.keyName)
-            }
-          })
-        } else {
-          this.sources = sources.map(source => {
-            return {
-              type: AssetType.VIDEO,
-              url: source.keyName,
-              name: source.name,
-              icon: 'video-bg',
-              thumbnailUrl: source.thumbnail && getThumbnailUrl(source.thumbnail)
-            }
-          })
+    },
+    onWidgetClick (widget, evt) {
+      if (!this.selectedWidget || this.selectedWidget.id !== widget.id) {
+        if (this.hasAssets(widget)) {
+          evt.stopPropagation()
+          this.selectedWidget = widget
         }
-        return
       }
-
-      this.sources = null
+    },
+    onRootClick () {
+      this.selectedWidget = null
     },
     onView (source) {
       this.$muted = this.muted
@@ -184,7 +172,7 @@ export default {
         case AssetType.VIDEO:
         case AssetType.AUDIO:
           if (!this.muted) {
-            this.muted = 1
+            this.muted = true
           }
           break
         default:
@@ -202,12 +190,13 @@ export default {
 <style lang="scss" scoped>
 .c-viewer {
   height: 100%;
+  min-width: 800px;
   min-height: 600px;
   overflow: hidden;
 
   &__header {
-    height: 69px;
-    padding: 0 10px;
+    height: 53px;
+    padding: 0 24px 0 10px;
     color: $black;
     font-size: 12px;
     line-height: 1;
@@ -215,10 +204,13 @@ export default {
     background-color: #fff;
   }
 
-  &__back {
-    margin-right: 4px;
-    padding: 10px;
-    font-size: 16px;
+  &__shortcut {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 36px;
+    height: 36px;
+    font-size: 18px;
     border-radius: 50%;
     transition: background-color 0.4s;
 
@@ -229,6 +221,7 @@ export default {
 
   &__name {
     max-width: 400px;
+    padding: 0 $spacing 0 6px;
     font-size: 18px;
   }
 
@@ -243,7 +236,7 @@ export default {
     color: $black;
     font-size: 16px;
     line-height: 1;
-    background-color: #f4f7fb;
+    background-color: #e8eaee;
 
     &--info {
       color: $info--dark;
@@ -251,14 +244,13 @@ export default {
     }
   }
 
-  &__list {
-    padding: 10px $spacing;
+  &__scrollbar {
+    flex: 1 0 0;
+    min-height: 10px;
   }
 
-  &__item {
-    & + & {
-      margin-top: 10px;
-    }
+  &__list {
+    padding: 10px;
   }
 
   &__content {
@@ -271,17 +263,6 @@ export default {
     position: relative;
     background-color: #000;
     overflow: hidden;
-
-    &::before {
-      content: "";
-      position: absolute;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      background-color: currentColor;
-      z-index: -1;
-    }
   }
 
   &__background {
@@ -290,41 +271,27 @@ export default {
     left: 0;
     right: 0;
     bottom: 0;
+    background-color: currentColor;
     z-index: -1;
   }
 }
 
-.o-tool-item {
-  display: inline-flex;
-  justify-content: center;
-  align-items: center;
-  padding: 10px 16px;
-  color: $gray--dark;
+.o-layer {
+  color: $black;
+  user-select: none;
+  border-radius: $radius;
+  background-color: #f4f7f8;
+  overflow: hidden;
   cursor: pointer;
-}
-
-.o-card {
-  &__content {
-    position: relative;
-    padding-top: 60%;
-  }
 
-  &__img {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background-position: center;
-    background-size: contain;
-    background-repeat: no-repeat;
+  & + & {
+    margin-top: 10px;
   }
 
-  &__name {
-    margin-top: 4px;
-    color: $black;
-    font-size: 12px;
-    line-height: 1;
+  &.active {
+    color: #fff;
+    outline: 2px solid $blue;
+    background-color: $blue;
   }
 }
 </style>

+ 1 - 1
src/views/bigscreen/ast/components/Card.vue

@@ -11,7 +11,7 @@
         :style="{ 'background-image': `url('${source.thumbnailUrl}')` }"
       />
       <svg-icon
-        v-else-if="source.icon"
+        v-else
         class="c-card__image"
         :icon-class="source.icon"
       />

+ 1 - 2
src/views/bigscreen/ast/components/Volume.vue

@@ -6,7 +6,7 @@
   />
   <div
     v-else
-    class="o-wave"
+    class="o-mute o-wave"
   >
     <span class="o-wave__line" />
     <span class="o-wave__line" />
@@ -49,7 +49,6 @@ export default {
   box-sizing: content-box;
   display: inline-flex;
   align-items: flex-end;
-  height: 20px;
 
   &.muted {
     .o-wave__line {

+ 211 - 0
src/views/bigscreen/ast/components/WidgetShortcut.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="o-widget-shortcut">
+    <div
+      class="o-widget-shortcut__content"
+      :class="{ thumbnail: background }"
+    >
+      <div
+        v-if="customStyle"
+        class="o-widget-shortcut__thumbnail"
+        :style="customStyle"
+      />
+      <i
+        v-if="icon"
+        class="o-widget-shortcut__logo"
+        :class="icon"
+      />
+      <span
+        v-else
+        class="o-widget-shortcut__logo text"
+      >
+        画布
+      </span>
+    </div>
+    <div
+      v-if="hasSources"
+      class="o-widget-shortcut__assets"
+    >
+      <div
+        v-for="(source, index) in sources"
+        :key="index"
+        class="o-widget-shortcut"
+      >
+        <div
+          class="o-widget-shortcut__content thumbnail radius"
+          @click="onView(source)"
+        >
+          <i
+            v-if="source.thumbnail"
+            class="o-widget-shortcut__thumbnail"
+            :style="{ 'background-image': `url('${source.thumbnail}')` }"
+          />
+          <svg-icon
+            v-else
+            class="o-widget-shortcut__thumbnail"
+            :icon-class="source.icon"
+          />
+        </div>
+        <div class="o-widget-shortcut__name u-ellipsis">{{ source.name }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getThumbnailUrl } from '@/api/asset'
+import { AssetType } from '@/constant'
+import { WidgetType } from '../core/constant'
+import { getIcon } from '../core/utils'
+
+export default {
+  name: 'WidgetShortcut',
+  props: {
+    widget: {
+      type: Object,
+      required: true
+    },
+    customStyle: {
+      type: Object,
+      default: null
+    },
+    sourceKey: {
+      type: String,
+      default: 'sources'
+    },
+    background: {
+      type: [Boolean, String],
+      default: false
+    }
+  },
+  computed: {
+    icon () {
+      return getIcon(this.widget.type)
+    },
+    sources () {
+      return this.widget[this.sourceKey]?.map(this.transform) || []
+    },
+    hasSources () {
+      return this.sources.length > 0
+    },
+    hasMore () {
+      return this.sources.length > 1
+    },
+    assetType () {
+      const type = this.widget.type
+      if (!type) {
+        return AssetType.AUDIO
+      }
+      switch (this.widget.type) {
+        case WidgetType.IMAGE:
+          return AssetType.IMAGE
+        case WidgetType.VIDEO:
+          return AssetType.VIDEO
+        default:
+          return null
+      }
+    }
+  },
+  methods: {
+    transform (data) {
+      const type = data.type || this.assetType
+      const source = {
+        type,
+        url: data.keyName,
+        name: data.name
+      }
+      switch (type) {
+        case AssetType.IMAGE:
+          source.thumbnail = getThumbnailUrl(data.keyName)
+          break
+        default:
+          if (data.thumbnail) {
+            source.thumbnail = getThumbnailUrl(data.thumbnail)
+          } else {
+            source.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
+          }
+          break
+      }
+      return source
+    },
+    onView (source) {
+      this.$emit('view', source)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.o-widget-shortcut {
+  &.dark {
+    .o-widget-shortcut__content {
+      background-color: rgba(#fff, 0.1);
+    }
+
+    .o-widget-shortcut__logo {
+      color: #fff;
+    }
+  }
+
+  & + & {
+    margin-top: 10px;
+  }
+
+  &__content {
+    position: relative;
+    padding-top: 40%;
+    background-color: rgba(#000, 0.05);
+    background-position: center;
+    background-size: contain;
+    background-repeat: no-repeat;
+
+    &.thumbnail {
+      padding-top: 60%;
+      background-color: #000 !important;
+
+      .o-widget-shortcut__logo {
+        color: #fff !important;
+      }
+    }
+
+    &.radius {
+      border-radius: $radius;
+      overflow: hidden;
+    }
+  }
+
+  &__logo {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    font-size: 24px;
+    white-space: nowrap;
+    transform: translate(-50%, -50%);
+    z-index: 9;
+
+    &.text {
+      font-size: 16px;
+    }
+  }
+
+  &__thumbnail {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-position: center;
+    background-size: contain;
+    background-repeat: no-repeat;
+  }
+
+  &__assets {
+    padding: 10px;
+  }
+
+  &__name {
+    margin-top: 6px;
+    font-size: 12px;
+    line-height: 1;
+  }
+}
+</style>

+ 2 - 24
src/views/bigscreen/ast/core/components/DynamicItem.vue

@@ -285,7 +285,8 @@ export default {
     align-self: center;
     min-width: 0;
 
-    ::v-deep .el-input__inner, ::v-deep .el-textarea__inner {
+    ::v-deep .el-input__inner,
+    ::v-deep .el-textarea__inner {
       color: #a8e3ff;
       border: 1px solid #3f5673;
       background-color: #263445;
@@ -316,29 +317,6 @@ export default {
         }
       }
     }
-
-    ::v-deep .el-textarea__inner {
-      min-height: 100px !important;
-
-      &::-webkit-scrollbar {
-        width: 4px;
-        height: 4px;
-      }
-
-      &::-webkit-scrollbar-track-piece {
-        background-color: #29405c;
-      }
-
-      &::-webkit-scrollbar-track {
-        box-shadow: 1px 1px 5px rgba(116, 148, 170, .5) inset;
-      }
-
-      &::-webkit-scrollbar-thumb {
-        min-height: 20px;
-        background-clip: content-box;
-        box-shadow: 0 0 0 5px rgba(116, 148, 170, .5) inset;
-      }
-    }
   }
 }
 

+ 0 - 6
src/views/bigscreen/ast/core/config-json/base.js

@@ -1,9 +1,3 @@
-export const layerNameOption = {
-  key: 'layerName',
-  type: 'text',
-  label: '图层名称'
-}
-
 export const positionConfig = {
   label: '坐标',
   list: [

+ 3 - 3
src/views/bigscreen/ast/core/config-json/image.js

@@ -1,8 +1,9 @@
 import { AssetType } from '@/constant'
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CImage',
+  type: WidgetType.IMAGE,
   label: '图片',
   icon: 'iconfont icontupian',
   defaults (scale) {
@@ -30,7 +31,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'radius',
           label: '圆角',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/live.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CLive',
+  type: WidgetType.LIVE,
   label: '直播',
   icon: 'el-icon-video-camera',
   defaults (scale) {
@@ -23,7 +24,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'radius',
           label: '圆角',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/marquee.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CMarquee',
+  type: WidgetType.MARQUEE,
   label: '滚动文本',
   icon: 'iconfont iconhengxiangwenzi',
   defaults (scale) {
@@ -26,7 +27,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'text',
           label: '文本',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/text.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CText',
+  type: WidgetType.TEXT,
   label: '文本',
   icon: 'iconfont iconziyuan',
   defaults (scale) {
@@ -25,7 +26,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'text',
           label: '文本',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/time.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CTime',
+  type: WidgetType.TIME,
   label: '时间',
   icon: 'iconfont iconshijian',
   defaults (scale) {
@@ -24,7 +25,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'format',
           label: '格式',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/video.js

@@ -1,8 +1,9 @@
 import { AssetType } from '@/constant'
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CVideo',
+  type: WidgetType.VIDEO,
   label: '视频',
   icon: 'iconfont iconshipin',
   defaults (scale) {
@@ -28,7 +29,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'radius',
           label: '圆角',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/weather.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CWeather',
+  type: WidgetType.WEATHER,
   label: '天气',
   icon: 'el-icon-cloudy-and-sunny',
   elementui: true,
@@ -24,7 +25,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'color',
           label: '字体颜色',

+ 3 - 3
src/views/bigscreen/ast/core/config-json/web.js

@@ -1,7 +1,8 @@
-import { layerNameOption, positionConfig } from './base'
+import { WidgetType } from '../constant'
+import { positionConfig } from './base'
 
 export default {
-  type: 'CWeb',
+  type: WidgetType.WEB,
   label: '网页',
   icon: 'iconfont iconchaolianjie',
   defaults (scale) {
@@ -17,7 +18,6 @@ export default {
     {
       label: '配置',
       list: [
-        layerNameOption,
         {
           key: 'href',
           label: '链接地址',

+ 10 - 0
src/views/bigscreen/ast/core/constant.js

@@ -0,0 +1,10 @@
+export const WidgetType = {
+  IMAGE: 'CImage',
+  VIDEO: 'CVideo',
+  WEATHER: 'CWeather',
+  WEB: 'CWeb',
+  TIME: 'CTime',
+  LIVE: 'CLive',
+  TEXT: 'CText',
+  MARQUEE: 'CMarquee'
+}

+ 34 - 20
src/views/bigscreen/ast/core/utils.js

@@ -1,3 +1,4 @@
+import { WidgetType } from './constant'
 import widgetCanvas from './config-json/canvas'
 import widgetText from './config-json/text'
 import widgetMarquee from './config-json/marquee'
@@ -41,11 +42,10 @@ export function toJSON (node) {
   return canvas
 }
 
-export function copy ({ id, layerName, ...node }) {
+export function copy ({ id, ...node }) {
   const widgetOptions = getWidget(node.type)
   const copyNode = widgetOptions.copy?.(node) || node
   copyNode.id = createID(copyNode.type)
-  copyNode.layerName = widgetOptions.label
   return copyNode
 }
 
@@ -70,7 +70,6 @@ export function normalize (data) {
   const widgetOptions = getWidget(type)
   return {
     id: createID(type),
-    layerName: widgetOptions.label,
     ...widgetOptions.defaults(scale),
     ...transform(data, widgetOptions.transform, { color: transformColorToWeb })
   }
@@ -131,8 +130,8 @@ export function fix (node) {
   const { bgm, widgets } = node
 
   if (bgm.length === 0) {
-    const videoType = widgetVideo.type
-    const liveType = widgetLive.type
+    const videoType = WidgetType.VIDEO
+    const liveType = WidgetType.LIVE
     const videos = widgets.filter(widget => {
       const type = widget.type
       switch (type) {
@@ -165,9 +164,9 @@ export function fix (node) {
 }
 
 export function getDuration (node) {
-  const imageType = widgetImage.type
-  const videoType = widgetVideo.type
-  const liveType = widgetLive.type
+  const imageType = WidgetType.IMAGE
+  const videoType = WidgetType.VIDEO
+  const liveType = WidgetType.LIVE
   return node.widgets.reduce((duration, widget) => {
     switch (widget.type) {
       case imageType:
@@ -187,20 +186,35 @@ export function validate (node) {
   if (!widgets.length) {
     return '未配置组件,请先进行配置'
   }
-  const sourceTypes = [widgetImage.type, widgetVideo.type]
-  const webType = widgetWeb.type
-  const liveType = widgetLive.type
+  const imageType = WidgetType.IMAGE
+  const videoType = WidgetType.VIDEO
+  const liveType = WidgetType.LIVE
+  const webType = WidgetType.WEB
   for (let i = 0; i < widgets.length; i++) {
     const widget = widgets[i]
-    const type = widget.type
-    if (sourceTypes.includes(type) && widget.sources.length === 0) {
-      return `${widget.layerName}未配置数据,请先进行配置`
-    }
-    if (type === webType && !widget.href) {
-      return `${widget.layerName}未配置链接地址,请先进行配置`
-    }
-    if (type === liveType && !widget.url) {
-      return `${widget.layerName}未配置播放地址,请先进行配置`
+    switch (widget.type) {
+      case imageType:
+        if (widget.sources.length === 0) {
+          return '有图片未配置数据,请先进行配置'
+        }
+        break
+      case videoType:
+        if (widget.sources.length === 0) {
+          return '有视频未配置数据,请先进行配置'
+        }
+        break
+      case webType:
+        if (!widget.href) {
+          return '有网页未配置链接地址,请先进行配置'
+        }
+        break
+      case liveType:
+        if (!widget.url) {
+          return '有直播未配置播放地址,请先进行配置'
+        }
+        break
+      default:
+        break
     }
   }
 }

+ 2 - 1
src/views/bigscreen/ast/core/widget/CImage.vue

@@ -59,10 +59,11 @@
 
 <script>
 import { getThumbnailUrl } from '@/api/asset'
+import { WidgetType } from '../constant'
 import { switchToNext } from '../utils'
 
 export default {
-  name: 'CImage',
+  name: WidgetType.IMAGE,
   inject: ['control'],
   props: {
     node: {

+ 4 - 3
src/views/bigscreen/ast/core/widget/CLive.vue

@@ -18,12 +18,13 @@
 
 <script>
 import HlsJs from 'hls.js'
+import { WidgetType } from '../constant'
 
 const isSupported = HlsJs.isSupported()
 const isHttps = location.protocol === 'https:'
 
 export default {
-  name: 'CLive',
+  name: WidgetType.LIVE,
   inject: ['control'],
   props: {
     node: {
@@ -118,11 +119,11 @@ export default {
   display: inline-flex;
   justify-content: center;
   align-items: center;
-  background-color: rgba(255, 255, 255, .8);
+  background-color: rgba(255, 255, 255, 0.8);
   overflow: hidden;
 
   &::before {
-    content: '\e772';
+    content: "\e772";
     font-family: element-icons;
     font-size: inherit;
   }

+ 3 - 1
src/views/bigscreen/ast/core/widget/CMarquee.vue

@@ -15,8 +15,10 @@
 </template>
 
 <script>
+import { WidgetType } from '../constant'
+
 export default {
-  name: 'CText',
+  name: WidgetType.MARQUEE,
   props: {
     node: {
       type: Object,

+ 9 - 2
src/views/bigscreen/ast/core/widget/CText.vue

@@ -1,10 +1,17 @@
 <template>
-  <div class="c-text" :style="styles">{{ node.text }}</div>
+  <div
+    class="c-text"
+    :style="styles"
+  >
+    {{ node.text }}
+  </div>
 </template>
 
 <script>
+import { WidgetType } from '../constant'
+
 export default {
-  name: 'CText',
+  name: WidgetType.TEXT,
   props: {
     node: {
       type: Object,

+ 7 - 2
src/views/bigscreen/ast/core/widget/CTime.vue

@@ -1,10 +1,15 @@
 <template>
-  <div class="c-time" :style="styles">{{ time }}</div>
+  <div
+    class="c-time"
+    :style="styles"
+  >{{ time }}</div>
 </template>
 
 <script>
+import { WidgetType } from '../constant'
+
 export default {
-  name: 'CText',
+  name: WidgetType.TIME,
   props: {
     node: {
       type: Object,

+ 2 - 1
src/views/bigscreen/ast/core/widget/CVideo.vue

@@ -24,10 +24,11 @@
 
 <script>
 import { getAssetUrl } from '@/api/asset'
+import { WidgetType } from '../constant'
 import { switchToNext } from '../utils'
 
 export default {
-  name: 'CVideo',
+  name: WidgetType.VIDEO,
   inject: ['control'],
   props: {
     node: {

+ 7 - 2
src/views/bigscreen/ast/core/widget/CWeather.vue

@@ -1,12 +1,17 @@
 <template>
-  <div class="c-weather" :style="styles">
+  <div
+    class="c-weather"
+    :style="styles"
+  >
     <i class="el-icon-cloudy-and-sunny" />&nbsp;19℃
   </div>
 </template>
 
 <script>
+import { WidgetType } from '../constant'
+
 export default {
-  name: 'CWeather',
+  name: WidgetType.WEATHER,
   props: {
     node: {
       type: Object,

+ 5 - 5
src/views/bigscreen/ast/core/widget/CWeb.vue

@@ -7,15 +7,15 @@
       class="c-web__iframe"
       :src="node.href"
     />
-    <i
-      class="c-web__icon iconfont iconchaolianjie"
-    />
+    <i class="c-web__icon iconfont iconchaolianjie" />
   </div>
 </template>
 
 <script>
+import { WidgetType } from '../constant'
+
 export default {
-  name: 'CWeb',
+  name: WidgetType.WEB,
   props: {
     node: {
       type: Object,
@@ -42,7 +42,7 @@ export default {
 .c-web {
   position: relative;
   display: inline-block;
-  background-color: rgba(255, 255, 255, .8);
+  background-color: rgba(255, 255, 255, 0.8);
 
   &__iframe {
     width: 100%;

+ 4 - 27
src/views/bigscreen/ast/core/widget/Widget.vue

@@ -17,49 +17,26 @@
 </template>
 
 <script>
+import widget from './widget'
 import Draggable from '../components/Draggable'
-import CText from './CText.vue'
-import CMarquee from './CMarquee.vue'
-import CImage from './CImage.vue'
-import CVideo from './CVideo.vue'
-import CTime from './CTime.vue'
-import CWeather from './CWeather.vue'
-import CWeb from './CWeb.vue'
-import CLive from './CLive.vue'
 
 export default {
   name: 'Widget',
   components: {
-    Draggable,
-    CText,
-    CMarquee,
-    CImage,
-    CVideo,
-    CTime,
-    CWeather,
-    CWeb,
-    CLive
+    Draggable
   },
+  mixins: [widget],
   props: {
     scale: {
       type: Number,
       default: -1
     },
-    node: {
-      type: Object,
-      default: null
-    },
     root: {
       type: Object,
-      default () {
-        return {}
-      }
+      required: true
     }
   },
   computed: {
-    widgetType () {
-      return this.node.type
-    },
     widgetsTop () {
       return this.node.top
     },

+ 3 - 40
src/views/bigscreen/ast/core/widget/WidgetViewer.vue

@@ -2,47 +2,18 @@
   <component
     :is="widgetType"
     class="c-widget"
-    :class="{ more }"
     :style="styles"
     :node="node"
   />
 </template>
 
 <script>
-import CText from './CText.vue'
-import CMarquee from './CMarquee.vue'
-import CImage from './CImage.vue'
-import CVideo from './CVideo.vue'
-import CTime from './CTime.vue'
-import CWeather from './CWeather.vue'
-import CWeb from './CWeb.vue'
-import CLive from './CLive.vue'
+import widget from './widget'
 
 export default {
-  name: 'Widget',
-  components: {
-    CText,
-    CMarquee,
-    CImage,
-    CVideo,
-    CTime,
-    CWeather,
-    CWeb,
-    CLive
-  },
-  props: {
-    node: {
-      type: Object,
-      default: null
-    }
-  },
+  name: 'WidgetViewer',
+  mixins: [widget],
   computed: {
-    more () {
-      return this.node.sources?.length > 0
-    },
-    widgetType () {
-      return this.node.type
-    },
     styles () {
       const { top, left } = this.node
       return {
@@ -58,13 +29,5 @@ export default {
 .c-widget {
   position: absolute !important;
   user-select: none;
-
-  &.more {
-    cursor: pointer;
-
-    &:hover {
-      outline: 2px dashed $gray;
-    }
-  }
 }
 </style>

+ 32 - 0
src/views/bigscreen/ast/core/widget/widget.js

@@ -0,0 +1,32 @@
+import CText from './CText.vue'
+import CMarquee from './CMarquee.vue'
+import CImage from './CImage.vue'
+import CVideo from './CVideo.vue'
+import CTime from './CTime.vue'
+import CWeather from './CWeather.vue'
+import CWeb from './CWeb.vue'
+import CLive from './CLive.vue'
+
+export default {
+  components: {
+    [CText.name]: CText,
+    [CMarquee.name]: CMarquee,
+    [CImage.name]: CImage,
+    [CVideo.name]: CVideo,
+    [CTime.name]: CTime,
+    [CWeather.name]: CWeather,
+    [CWeb.name]: CWeb,
+    [CLive.name]: CLive
+  },
+  props: {
+    node: {
+      type: Object,
+      required: true
+    }
+  },
+  computed: {
+    widgetType () {
+      return this.node.type
+    }
+  }
+}

+ 6 - 4
src/views/bigscreen/ast/mixin.js

@@ -6,6 +6,7 @@ import {
   create,
   switchToNext
 } from './core/utils'
+import WidgetShortcut from './components/WidgetShortcut.vue'
 import Volume from './components/Volume'
 import Card from './components/Card'
 
@@ -16,6 +17,7 @@ export default {
     }
   },
   components: {
+    WidgetShortcut,
     Volume,
     Card
   },
@@ -46,19 +48,19 @@ export default {
     },
     styles () {
       if (this.node) {
-        const { width, height, backgroundColor } = this.node
+        const { width, height } = this.node
         return {
           width: `${width}px`,
-          height: `${height}px`,
-          color: backgroundColor
+          height: `${height}px`
         }
       }
       return null
     },
     backgroundStyles () {
       if (this.node) {
-        const { backgroundImage } = this.node
+        const { backgroundImage, backgroundColor } = this.node
         return {
+          'background-color': backgroundColor,
           'background-image': backgroundImage[0] ? `url("${getThumbnailUrl(backgroundImage[0])}")` : 'none'
         }
       }

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

@@ -23,7 +23,7 @@ export default {
   render (h) {
     return h(
       'div',
-      { staticClass: 'c-info-grid more' },
+      { staticClass: 'c-info-grid small' },
       __STAGING__
         ? [
           h('ScreenNetwork', { props: this.$attrs }),

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="c-info-grid">
+  <div class="c-info-grid medium">
     <running v-bind="$attrs" />
     <template v-if="$attrs.online">
       <screen-shot v-bind="$attrs" />

+ 1 - 1
src/views/device/detail/components/external/Sensors/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="c-info-grid less">
+  <div class="c-info-grid">
     <sensor
       type="temperature"
       title="温度"