|
|
@@ -0,0 +1,350 @@
|
|
|
+<template>
|
|
|
+ <div class="l-flex--col c-viewer">
|
|
|
+ <audio
|
|
|
+ v-if="bgmUrl"
|
|
|
+ ref="audio"
|
|
|
+ :src="bgmUrl"
|
|
|
+ :muted.prop="muted"
|
|
|
+ autoplay
|
|
|
+ @ended="onAudioEnded"
|
|
|
+ @error="onAudioError"
|
|
|
+ />
|
|
|
+ <div class="l-flex__none l-flex--row c-viewer__header">
|
|
|
+ <span class="c-viewer__name u-bold u-ellipsis">{{ program.name }}</span>
|
|
|
+ <span class="c-sibling-item">{{ program.resolutionRatio }}</span>
|
|
|
+ <div
|
|
|
+ v-if="hasAudio"
|
|
|
+ class="c-sibling-item c-viewer__shortcut u-pointer"
|
|
|
+ @click="toggleMute"
|
|
|
+ >
|
|
|
+ <volume :muted="muted" />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="hasNext"
|
|
|
+ class="c-sibling-item c-viewer__shortcut u-pointer"
|
|
|
+ @click="switchBgm"
|
|
|
+ >
|
|
|
+ <i class="o-next" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="l-flex__none l-flex">
|
|
|
+ <div class="l-flex__none l-flex--col c-viewer__asserts">
|
|
|
+ <div class="l-flex__none l-flex--row c-viewer__count has-padding--h">
|
|
|
+ <span class="l-flex__auto u-bold">素材</span>
|
|
|
+ <span class="l-flex__none c-viewer__count--info">{{ count }}</span>
|
|
|
+ </div>
|
|
|
+ <el-scrollbar
|
|
|
+ class="c-viewer__scrollbar"
|
|
|
+ native
|
|
|
+ >
|
|
|
+ <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"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="hasRootAssets"
|
|
|
+ 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>
|
|
|
+ </el-scrollbar>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="l-flex__none"
|
|
|
+ :style="canvasStyles"
|
|
|
+ @click="onRootClick"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="c-viewer__canvas has-bg"
|
|
|
+ :style="[transformStyles, styles]"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="c-viewer__background has-bg"
|
|
|
+ :style="backgroundStyles"
|
|
|
+ />
|
|
|
+ <widget
|
|
|
+ v-for="item in widgets"
|
|
|
+ :key="item.id"
|
|
|
+ :class="{ 'breathe-inset': item.id === selectedWidgetId }"
|
|
|
+ :node="item"
|
|
|
+ @click.native="onWidgetClick(item, $event)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <preview-dialog
|
|
|
+ ref="previewDialog"
|
|
|
+ append-to-body
|
|
|
+ @close="onClose"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { AssetType } from '@/constant'
|
|
|
+import { WidgetType } from './core/constant'
|
|
|
+import mixin from './mixin'
|
|
|
+import Widget from './core/widget/WidgetViewer.vue'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'BigScreenPreviewer',
|
|
|
+ components: {
|
|
|
+ Widget
|
|
|
+ },
|
|
|
+ mixins: [mixin],
|
|
|
+ data () {
|
|
|
+ return {
|
|
|
+ selectedWidget: null,
|
|
|
+ canvasStyles: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ layers () {
|
|
|
+ return this.widgets.filter(this.hasAssets).slice().reverse()
|
|
|
+ },
|
|
|
+ count () {
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ hasRootAssets () {
|
|
|
+ if (this.node) {
|
|
|
+ const { backgroundImage, bgm } = this.node
|
|
|
+ return backgroundImage.length > 0 || bgm.length > 0
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ selectedWidget () {
|
|
|
+ this.node && setTimeout(() => {
|
|
|
+ if (this.selectedWidget) {
|
|
|
+ this.$refs.widgetElements?.find(element => element.classList.contains('active'))?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
+ } else if (this.hasRootAssets) {
|
|
|
+ this.$refs.rootElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ hasAssets (widget) {
|
|
|
+ switch (widget.type) {
|
|
|
+ case WidgetType.MEDIA:
|
|
|
+ case WidgetType.IMAGE:
|
|
|
+ case WidgetType.VIDEO:
|
|
|
+ return true
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onWidgetClick (widget, evt) {
|
|
|
+ if (this.hasAssets(widget)) {
|
|
|
+ evt.stopPropagation()
|
|
|
+ if (!this.selectedWidget || this.selectedWidget.id !== widget.id) {
|
|
|
+ this.selectedWidget = widget
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onRootClick () {
|
|
|
+ this.selectedWidget = null
|
|
|
+ },
|
|
|
+ onView (source) {
|
|
|
+ this.$muted = this.muted
|
|
|
+ switch (source.type) {
|
|
|
+ case AssetType.VIDEO:
|
|
|
+ case AssetType.AUDIO:
|
|
|
+ if (!this.muted) {
|
|
|
+ this.muted = true
|
|
|
+ }
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+ this.$refs.previewDialog.show(source)
|
|
|
+ },
|
|
|
+ onClose () {
|
|
|
+ this.muted = this.$muted
|
|
|
+ },
|
|
|
+ checkRatio () {
|
|
|
+ if (this.canvasStyles) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const { width, height } = this.node
|
|
|
+ if (width <= 960 && height <= 540) {
|
|
|
+ this.canvasStyles = {}
|
|
|
+ } else {
|
|
|
+ const wScale = 960 / width
|
|
|
+ const hScale = 540 / height
|
|
|
+ this.scale = Math.min(wScale, hScale) * 100 | 0
|
|
|
+ if (wScale <= hScale) {
|
|
|
+ this.canvasStyles = {
|
|
|
+ width: '960px',
|
|
|
+ height: `${height * wScale}px`
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.canvasStyles = {
|
|
|
+ width: `${width * hScale}px`,
|
|
|
+ height: '540px'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.c-viewer {
|
|
|
+ &__header {
|
|
|
+ height: 54px;
|
|
|
+ padding: 0 $spacing;
|
|
|
+ color: $black;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1;
|
|
|
+ border-bottom: 1px solid $border;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__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: $gray--light;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ background-color: $gray;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &__name {
|
|
|
+ max-width: 400px;
|
|
|
+ padding: 0 $spacing 0 6px;
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__asserts {
|
|
|
+ width: 160px;
|
|
|
+ border-right: 1px solid $border;
|
|
|
+ background-color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__count {
|
|
|
+ height: 48px;
|
|
|
+ color: $black;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1;
|
|
|
+ background-color: #e8eaee;
|
|
|
+
|
|
|
+ &--info {
|
|
|
+ color: $info--dark;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &__scrollbar {
|
|
|
+ flex: 1 1 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 0;
|
|
|
+
|
|
|
+ ::v-deep {
|
|
|
+ .el-scrollbar__wrap {
|
|
|
+ flex: 1 1 0;
|
|
|
+ min-height: 0;
|
|
|
+ margin-bottom: 0 !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &__list {
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__canvas {
|
|
|
+ position: relative;
|
|
|
+ background-color: #000;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__background {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-color: currentColor;
|
|
|
+ z-index: -1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.o-layer {
|
|
|
+ color: $black;
|
|
|
+ user-select: none;
|
|
|
+ border-radius: $radius;
|
|
|
+ background-color: #f4f7f8;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ & + & {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ color: #fff;
|
|
|
+ outline: 2px solid $blue;
|
|
|
+ background-color: $blue;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.breathe-inset::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ animation: breathe-inset 1s ease-in-out infinite alternate;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes breathe-inset {
|
|
|
+ 0% {
|
|
|
+ box-shadow: inset 0 0 20px 30px rgba($blue, 0.5);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ box-shadow: inset 0 0 60px 30px $blue;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|