quanyawei
2023-10-20 d8b41fff43a2cee6a8f714ffa807623b15803786
uni_modules/cl-upload/components/cl-upload/cl-upload.vue
New file
@@ -0,0 +1,1031 @@
<template>
   <view class="cl-updata">
      <view class="file-list" :style="[listRowStyle]">
         <view v-for="(item, index) in previewList" @tap="clickSelectedFile(item, index)" class="file-list-row"
            :style="[rowStyle]" :key="index">
            <image
               class="_image"
               v-if="fileUrlType(item) === 'image'"
               :src="item.path"
               :style="[imgStyle]"
               mode="aspectFill">
            </image>
            <view v-else class="_video" :style="[imgStyle]">
               <!-- #ifdef MP-WEIXIN || MP-ALIPAY -->
               <video
                  v-if="!autoUpload || cloudType === 'other'"
                  class="_video"
                  :style="[imgStyle]"
                  :src="item.path"
                  :show-center-play-btn="false"
                  :show-fullscreen-btn="false"
                  :show-play-btn="false"
                  :show-loading="false"
                  :enable-progress-gesture="false"
                  :controls="false">
                  <view @tap="previewVideo(item, index)" class="play">
                     <image style="width: 100%;" :src="playImg" mode="widthFix"></image>
                  </view>
               </video>
               <!-- #endif -->
               <!-- #ifdef APP-PLUS -->
               <video
                  v-if="cloudType === 'other'"
                  class="_video"
                  :style="[imgStyle]"
                  :src="item.path"
                  :poster="item.path"
                  :controls="false"
                  :show-center-play-btn="false"
                  :show-fullscreen-btn="false"
                  :show-play-btn="false"
                  :show-loading="false"
                  :enable-progress-gesture="false">
                  <cover-image class="app_play" :src="playImg" @tap="previewVideo(item, index)"></cover-image>
                  <cover-view class="remove" v-if="remove" @tap="deleteSelectedFile(item, index)">
                     <cover-image class="image" :src="deleteImg" mode="widthFix" @tap="deleteSelectedFile(item, index)"></cover-image>
                  </cover-view>
               </video>
               <!-- #endif -->
               <!-- #ifndef MP-WEIXIN || MP-ALIPAY || APP-PLUS -->
               <video
                  v-if="cloudType === 'other'"
                  class="_video"
                  :autoplay="false"
                  :style="[imgStyle]"
                  :src="item.path"
                  :controls="false"
                  :show-center-play-btn="false"
                  :show-fullscreen-btn="false"
                  :show-play-btn="false"
                  :show-loading="false"
                  :enable-progress-gesture="false" >
                  <cover-view  @tap="previewVideo(item, index)" class="play">
                     <cover-image style="width: 100%;" :src="playImg" mode="widthFix"></cover-image>
                  </cover-view>
               </video>
               <!-- #endif -->
               <template v-else>
                  <cl-image class="pay" :style="[imgStyle]" :cloudType="cloudType"
                     :src="(item.poster || item.path)"></cl-image>
                  <view class="play" @tap="previewVideo(item, index)">
                     <image class="play-img" :src="playImg" mode="widthFix"></image>
                  </view>
               </template>
            </view>
            <view class="remove" v-if="remove" @tap.stop="deleteSelectedFile(item, index)">
               <image class="image" :src="deleteImg" mode="widthFix"></image>
            </view>
         </view>
         <view v-if="add && FileList.length < max" @tap="selectFileTypeOnAdd" :style="[rowStyle]" class="file-list-row">
            <slot name="addImg">
               <div class="add-image">
                  <image class="_image" :src="addImg" mode="widthFix"></image>
               </div>
            </slot>
         </view>
      </view>
      <view v-if="tempVideoUrl" class="mask">
         <image @tap="tempVideoUrl = ''" class="_root" :src="closeImg" mode="widthFix"></image>
         <view class="block" @tap.stop>
            <video class="block_video" autoplay :src="tempVideoUrl"></video>
         </view>
      </view>
   </view>
</template>
<script>
import ClImage from '../cl-image/cl-image.vue'
export default {
   name: "cl-upload",
   components: { ClImage },
   props: {
      //受控图片列表
      // #ifdef VUE2
      value: {
         type: Array,
         default: () => [],
      },
      // #endif
      // #ifdef VUE3
      modelValue: {
         type: Array,
         default: () => [],
      },
      // #endif
      // 存储云类型 oss阿里云  vframe七牛云   process腾讯云  other其他
      cloudType: {
         type: String,
         default: 'oss'
      },
      // 标识符,即后端接口参数名
      fileName: {
         type: String,
         default: 'file'
      },
      // 文件类型 'image', 'video', 'all'
      fileType: {
         type: String,
         default: 'all'
      },
      // 上传图片参数
      imageFormData: {
         type: Object | null,
         default: () => { }
      },
      // 上传视频参数
      videoFromData: {
         type: Object,
         default: () => { }
      },
      // 必选参数,上传的地址
      action: {
         type: String,
         default: ''
      },
      // 启用目录, 仅unicloud阿里云支持
      // https://uniapp.dcloud.net.cn/uniCloud/storage.html#storage-dir
      cloudPathAsRealPath: {
         type: Boolean,
         default: false
      },
      // 设置上传的请求头部
      headers: {
         type: Object,
         default: () => { }
      },
      // 上传时附带的额外参数
      data: {
         type: Object,
         default: () => { }
      },
      // 是否开启预览图片
      isPreviewImage: {
         type: Boolean,
         default: true
      },
      // 图片指示器样式,可取值:"default" - 底部圆点指示器; "number" - 顶部数字指示器; "none" - 不显示指示器。
      indicator: {
         type: String,
         default: 'none'
      },
      // 是否在选取文件后立即进行上传
      autoUpload: {
         type: Boolean,
         default: true
      },
      // 是否显示删除按钮
      remove: {
         type: Boolean,
         default: true
      },
      // 是否添加按钮
      add: {
         type: Boolean,
         default: true
      },
      // 最多显示数量
      max: {
         type: Number,
         default: 9
      },
      // 视频最大上传数量
      maxVideo: {
         type: Number,
         default: 0
      },
      // 列表样式
      listStyle: {
         type: Object,
         default: () => { }
      },
      // 删除提示弹窗标题
      deleteTitle: {
         type: String,
         default: '提示'
      },
      // 删除提示弹窗文案
      deleteText: {
         type: String,
         default: '您确认要删除吗?'
      },
      // 加载文案
      loadingText: {
         type: String,
         default: '正在上传中...'
      },
      // 是否开启删除前钩子
      useBeforeDelete: {
         type: Boolean,
         default: false
      },
      // 是否开启上传前钩子
      useBeforeUpload: {
         type: Boolean,
         default: false
      },
      // 添加按钮图片
      addImg: {
         type: String,
         default: 'https://mp-61599c79-d7ee-4a75-a24b-e5a288da6dd3.cdn.bspapp.com/cloudstorage/bb1550b3-e0a8-4a90-a86f-00f8c6afa9fb.png'
      },
      // 播放按钮图片
      playImg: {
         type: String,
         default: 'https://mp-61599c79-d7ee-4a75-a24b-e5a288da6dd3.cdn.bspapp.com/cloudstorage/ae40402f-aa53-4344-b553-2322799bebd6.png'
      },
      // 删除按钮图片
      deleteImg: {
         type: String,
         default: 'https://mp-61599c79-d7ee-4a75-a24b-e5a288da6dd3.cdn.bspapp.com/cloudstorage/d20177a5-417e-4c5d-a266-1988361c543d.png'
      },
      // 关闭视频按钮图片
      closeImg: {
         type: String,
         default: 'https://mp-61599c79-d7ee-4a75-a24b-e5a288da6dd3.cdn.bspapp.com/cloudstorage/cde4362d-7ec7-4cac-a692-12e1f576be1e.png'
      },
   },
   data() {
      return {
         // 渲染列表
         FileList: [],
         // 预览视频地址
         tempVideoUrl: '',
         // 临时文件列表
         tempFile_paths: [],
      };
   },
   watch: {
      // #ifdef VUE2
      'value': {
         handler: function (newVal, oldVal) {
            this.FileList = newVal;
         },
         deep: true,
         immediate: true
      },
      // #endif
      // #ifdef VUE3
      'modelValue': {
         handler: function (newVal, oldVal) {
            this.FileList = newVal;
         },
         deep: true,
         immediate: true
      },
      // #endif
   },
   computed: {
      previewList() {
         return this.FileList.map(item => {
            return {
               path: item.path || item,
               poster: item.poster || ''
            }
         })
      },
      listRowStyle() {
         const style = {
            'grid-template-columns': `repeat(${this.listStyle?.columns || 4}, 1fr)`, // 每行数量
            'grid-column-gap': this.listStyle?.columnGap || '40rpx', // 行间距
            'grid-row-gap': this.listStyle?.rowGap || '40rpx', // 列间距
            'padding': this.listStyle?.padding || '0rpx' // 列表内边距
         }
         return style;
      },
      rowStyle() {
         const { height = '140rpx', ratio } = this.listStyle || {};
         const style = {
            'aspect-ratio': height ? '' : ratio || '1/1', // 图片比例
            'height': height,
         };
         return style;
      },
      imgStyle() {
         const style = {
            'border-radius': this.listStyle?.radius || '6rpx', // 图片圆角
         }
         return style;
      }
   },
   methods: {
      /**
       * 删除已选择文件
       * @param {object} item 文件信息
       * @param {number} selectedFileIndex 文件索引
       * */
      deleteSelectedFile(item, selectedFileIndex) {
         const fileToDelete = this.FileList[selectedFileIndex];
         // 删除前钩子
         if (this.useBeforeDelete) {
            this.$emit('beforeDelete', fileToDelete, selectedFileIndex, () => {
               return deleteFileFromList()
            })
         }
         if (!this.useBeforeDelete) {
            uni.showModal({
               title: this.deleteTitle,
               content: this.deleteText,
               success: (res) => {
                  if (res.confirm) {
                     deleteFileFromList()
                  }
               }
            });
         }
         const deleteFileFromList = () => {
            const tempFileIndex = this.tempFile_paths.indexOf(item || item.path);
            if (tempFileIndex > -1) {
               this.tempFile_paths.splice(tempFileIndex, 1)
            }
            this.FileList.splice(selectedFileIndex, 1)
            // #ifdef VUE2
            this.$emit('input', this.FileList)
            // #endif
            // #ifdef VUE3
            this.$emit("update:modelValue", this.FileList);
            // #endif
         }
      },
      /**
       * 点击已选择文件
       * @param {object} item 文件信息
       * @param {number} index 文件索引
       * */
      clickSelectedFile(item, index) {
         this.previewImage(item?.path ?? item, index);
         this.$emit('onImage', {
            item,
            index
         })
      },
      /**
       * 点击选择图片按钮
       * */
      selectFileTypeOnAdd() {
         switch (this.fileType) {
            case 'image':
               this.handleFileSelection(1);
               break;
            case 'video':
               this.handleFileSelection(2);
               break;
            case 'all':
               uni.showActionSheet({
                  itemList: ['相册', '视频'],
                  success: (res) => {
                     const tapIndex = res.tapIndex;
                     if (tapIndex === 0) {
                        this.handleFileSelection(1);
                     } else {
                        this.handleFileSelection(2);
                     }
                  },
                  fail: (res) => {
                     console.error(res.errMsg);
                  }
               });
               break;
            default:
               this.handleFileSelection(1);
               break;
         }
      },
      /**
       * 从本地选择文件。
       * @param { number } updataType 选择类型 1:图片 2视频
       * */
      async handleFileSelection(updataType) {
         const that = this;
         if (updataType === 1) {
            const data = Object.assign({}, {
               // 最多可以选择的图片张数,默认9
               count: 9,
               // 仅对 mediaType 为 image 时有效,是否压缩所选文件
               // #ifndef MP-TOUTIAO
               sizeType: ['original', 'compressed'],
               // #endif
               // album 从相册选图,camera 使用相机,默认二者都有。
               sourceType: ['camera', 'album'],
               compress: false
            }, this.imageFormData)
            data['count'] = this.max - this.FileList.length
            uni.chooseImage({
               ...data,
               success: async (res) => {
                  let tempFiles = res.tempFiles
                  const compress = that.imageFormData?.compress || false;
                  // 限制图片上传尺寸
                  if (that.imageFormData?.size ?? false) {
                     const maxSize = that.imageFormData.size * 1024 * 1024
                     tempFiles.map((imgInfo, index) => {
                        if (imgInfo.size > maxSize) {
                           tempFiles.splice(index, 1)
                           that.$emit('onImageSize', imgInfo)
                           return uni.showToast({
                              title: `图片最大上传${that.imageFormData.size}MB`,
                              duration: 2000,
                              icon: 'none'
                           });
                        }
                     })
                  }
                  // 开启压缩图片
                  if (compress) {
                     const compressedImagePathList = tempFiles.map(imageItem => {
                        return that.compressImage(imageItem.path)
                     })
                     Promise.all(compressedImagePathList).then(result => {
                        upload(result);
                     })
                  } else {
                     upload(tempFiles);
                  }
                  function upload(tempImages) {
                     if (that.autoUpload) {
                        tempImages.map(item => {
                           that.onBeforeUploadFile(item, 'image')
                        })
                     } else {
                        that.FileList = [...that.FileList, ...tempImages]
                        tempImages.map(item => {
                           that.tempFile_paths.push(item)
                        })
                     }
                  }
               },
               fail(err) {
                  console.error('选择图片失败', err)
                  that.$emit('onError', err)
               }
            })
         }
         if (updataType === 2) {
            // 限制视频最大上传数量
            const VIDEO_REGEXP = /\.(mp4|flv|avi)/i
            const videoList = await that.FileList.filter(item => {
               const fileUrl = item?.url ?? item
               return VIDEO_REGEXP.test(fileUrl)
            })
            if (that.maxVideo > 0 && videoList.length >= that.maxVideo) {
               that.$emit('onVideoMax', that.maxVideo, videoList.length)
               return uni.showToast({
                  title: '视频数量已超出',
                  duration: 2000,
                  icon: 'none'
               });
            }
            const data = Object.assign({}, {
               //    拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。
               maxDuration: 60,
               // #ifndef MP-TOUTIAO
               // 'front'、'back',默认'back'
               camera: "back",
               // #endif
               // album 从相册选视频,camera 使用相机拍摄,默认二者都有。
               sourceType: ['camera', 'album'],
               // 是否压缩所选的视频源文件,默认值为 true,需要压缩。
               compressed: true,
               // 'front'、'back',默认'back'
            }, this.videoFromData)
            uni.chooseVideo({
               ...data,
               success: (res) => {
                  let tempFilePath = { ...res }
                  tempFilePath['path'] = res.tempFilePath
                  // 限制视频上传尺寸
                  if (that.videoFromData?.size ?? false) {
                     const maxSize = that.videoFromData.size * 1024 * 1024
                     if (tempFilePath.size > maxSize) {
                        uni.showToast({
                           title: `视频最大上传${that.videoFromData.size}MB`,
                           duration: 2000,
                           icon: 'none'
                        });
                        return false;
                     }
                  }
                  if (that.autoUpload) {
                     that.onBeforeUploadFile(tempFilePath, 'video')
                  } else {
                     that.FileList.push(tempFilePath)
                     that.tempFile_paths.push(tempFilePath)
                  }
               },
               fail(err) {
                  console.error('选择视频失败', err)
               }
            })
         }
      },
      /**
       * 上传前钩子
       * @param { tempFile } 临时文件
       * @return { Promise }
       * */
      onBeforeUploadFile(tempFile) {
         if (this.useBeforeUpload) {
            return this.$emit('beforeUpload', tempFile, () => {
               return this.updataFile(tempFile);
            })
         }
         return this.updataFile(tempFile);
      },
      /**
       * 上传文件到服务器
       * @param { tempFile } 临时文件
       * @return { Promise }
       * */
      updataFile(tempFile) {
         const that = this;
         const filePath = tempFile.path || tempFile;
         const fileType = this.fileUrlType(filePath) == 'image' ? '.png' : '.mp4';
         const fileName = tempFile.name || Date.now() + fileType;
         uni.showLoading({
            title: this.loadingText,
            icon: 'loading'
         })
         return new Promise((resolve, reject) => {
            // uniCloud上传
            if (that.action === 'uniCloud') {
               uniCloud.uploadFile({
                  cloudPath: String(fileName),
                  filePath: filePath,
                  // #ifdef MP-ALIPAY
                  fileType: fileType,
                  // #endif
                  cloudPathAsRealPath: this.cloudPathAsRealPath,
                  onUploadProgress: (progressEvent) => {
                     const percentCompleted = Math.round(
                        (progressEvent.loaded * 100) / progressEvent.total
                     );
                     that.$emit('onProgress', percentCompleted)
                  },
                  success(result) {
                     if (that.autoUpload) {
                        that.FileList.push(result.fileID)
                     } else {
                        that.FileList.map((item, index) => {
                           if (item === filePath || item.path === filePath) {
                              that.FileList.splice(index, 1, result.fileID)
                           }
                        })
                     }
                     // #ifdef VUE2
                     that.$emit('input', that.FileList)
                     // #endif
                     // #ifdef VUE3
                     that.$emit("update:modelValue", that.FileList);
                     // #endif
                     resolve(result.fileID)
                     uni.hideLoading();
                     that.$emit('onProgress', {
                        ...result
                     })
                  },
                  fail: (error) => {
                     uni.hideLoading();
                     console.error('error', error);
                     that.$emit('onError', error)
                     reject(error)
                  }
               })
               return false;
            }
            // 接口服务上传
            const uploadTask = uni.uploadFile({
               url: that.action,
               filePath: filePath,
               name: that.fileName,
               formData: that.data,
               header: that.headers,
               // #ifdef MP-ALIPAY
               fileType: filetype,
               // #endif
               success: (uploadFileRes) => {
                  const data = JSON.parse(uploadFileRes.data)
                  uni.hideLoading();
                  that.success(data)
                  if (!this.autoUpload) {
                     that.FileList.map((item, index) => {
                        if (item === filePath || item.path === filePath) {
                           that.FileList.splice(index, 1)
                        }
                     })
                  }
                  resolve(data)
               },
               fail: (error) => {
                  uni.hideLoading();
                  console.error('error', error);
                  that.$emit('onError', error)
                  reject(error)
               }
            });
            uploadTask.onProgressUpdate((res) => {
               that.$emit('onProgress', {
                  ...res,
                  ...tempFile
               })
            });
         })
      },
      /**
       * 手动上传
       * */
      submit() {
         return new Promise((resolve, reject) => {
            if (this.tempFile_paths.length <= 0) {
               resolve([])
            }
            const uploadedFilePaths = this.tempFile_paths.map(item => {
               return this.onBeforeUploadFile(item || item.path)
            })
            Promise.all(uploadedFilePaths).then(res => {
               this.tempFile_paths = []
               resolve(res)
            }).catch(err => {
               reject(err)
            })
         })
      },
      /**
       * 返回数据
       * @param {array} data 上传成功后的数据
       * @return {array} 返回数据
       * */
      success(data) {
         this.$emit('onSuccess', data);
         // 自定义数据结构-选择性开启
         // const list = data.map(item=> {
         //    return JSON.parse(item).data.link;
         // })
         // this.$emit('input', [...this.FileList, ...list]);
      },
      /**
       * 压缩图片
       * @param {array} tempFilePaths 临时路径数组
       * @return {array} 被压缩过的路径数组
       * */
      async compressImage(tempFilePaths) {
         const that = this;
         return new Promise((resolve, reject) => {
            if (typeof tempFilePaths !== 'string') {
               console.error('压缩路径错误')
               reject([])
            }
            uni.showLoading({
               title: '压缩中...',
               icon: 'loading',
            })
            // #ifdef H5
            this.canvasDataURL(tempFilePaths, {
               quality: that.imageFormData.quality / 100
            }, (base64Codes) => {
               resolve(base64Codes);
               uni.hideLoading();
            })
            // #endif
            // #ifndef H5
            uni.compressImage({
               src: tempFilePaths,
               quality: that.imageFormData.quality || 80,
               success: res => {
                  resolve(res.tempFilePath);
                  uni.hideLoading();
               },
               fail(err) {
                  reject(err);
                  uni.hideLoading();
               }
            })
            // #endif
         })
      },
      /**
       * H5压缩图片质量
       * @param {string} path 图片路径
       * @param {object} obj 压缩配置
       * @param {function} callback 回调函数
       * @return {string} base64
       * */
      canvasDataURL(path, obj, callback) {
         var img = new Image();
         img.src = path;
         img.onload = function () {
            var that = this;
            // 默认按比例压缩
            var w = that.width,
               h = that.height,
               scale = w / h;
            w = obj.width || w;
            h = obj.height || (w / scale);
            var quality = 0.8; // 默认图片质量为0.8
            //生成canvas
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            // 创建属性节点
            var anw = document.createAttribute("width");
            anw.nodeValue = w;
            var anh = document.createAttribute("height");
            anh.nodeValue = h;
            canvas.setAttributeNode(anw);
            canvas.setAttributeNode(anh);
            ctx.drawImage(that, 0, 0, w, h);
            // 图像质量
            if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
               quality = obj.quality;
            }
            // quality值越小,所绘制出的图像越模糊
            var base64 = canvas.toDataURL('image/jpeg', quality);
            // 回调函数返回base64的值
            callback(base64);
         }
      },
      /**
       * 预览图片
       * @param {string, object} item 文件信息
       * */
      previewImage(item) {
         if (this.fileUrlType(item) === 'video') return false;
         if (!this.isPreviewImage) return false;
         const imgs = this.FileList.filter(item => {
            return this.fileUrlType(item) !== 'video'
         }).map(item => item?.path ?? item)
         const current = imgs.indexOf(item || item.path);
         uni.previewImage({
            current: current,
            urls: imgs,
            success() {
            },
            fail(err) {
               console.log(err);
            }
         })
      },
      /**
       * 预览视频
       * @param {string, object} item 文件信息
       * @param {number} index 索引
       * */
      previewVideo(item, index) {
         this.$emit('onVideo', {
            item,
            index
         })
         this.tempVideoUrl = item.path;
      },
      /**
       * 是否img类型
       * @param {string, object} item 文件信息
       * @return {boolean} 是否img类型
       * */
      fileUrlType(file) {
         const filePath = file.path || file;
         if (this.isBase64(filePath)) return 'image'
         const fileType = filePath.split('.').pop();
         const IMAGE_REGEXP = /(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg|image)/i
         if (IMAGE_REGEXP.test(fileType)) {
            return 'image';
         } else {
            return 'video';
         }
      },
      // 判断是否是base64
      isBase64(str) {
         if (str === '' || typeof str !== 'string') return console.error('文件路径错误, base64', str);
         return str.includes('blob:') || str.includes('data:image');
      }
   }
}
</script>
<style lang="scss" scoped>
.cl-updata {
   .file-list {
      display: grid;
      &-row {
         display: inline-flex;
         align-items: center;
         position: relative;
         .play-img {
            width: 100%;
         }
         ._image {
            height: 100%;
            width: 100%;
         }
         ._video {
            position: relative;
            width: 100%;
            height: 100%;
            overflow: hidden;
         }
         .video-fixed {
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            width: 100%;
            height: 100%;
            border-radius: 10rpx;
            z-index: 96;
         }
         .play {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 30%;
            z-index: 95;
         }
         .app_play {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 50rpx;
            height: 50rpx;
         }
         .remove {
            position: absolute;
            top: 0;
            right: 0;
            background-color: #373737;
            height: 50rpx;
            width: 50rpx;
            border-bottom-left-radius: 200rpx;
            z-index: 97;
            .image {
               width: 20rpx;
               height: 20rpx;
               position: absolute;
               right: 12rpx;
               top: 12rpx;
            }
         }
      }
      .add-image {
         display: flex;
         align-items: center;
         justify-content: center;
         border: 2rpx dashed #ccc;
         width: 100%;
         height: 100%;
         border-radius: 5rpx;
         &:active {
            opacity: 0.8;
         }
         ._image {
            width: 40%;
         }
      }
   }
   .mask {
      background-color: #000;
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 99;
      .block {
         padding: 0 30rpx;
         position: absolute;
         top: 50%;
         left: 50%;
         transform: translate(-50%, -50%);
         width: 100%;
         .block_video {
            width: 100%;
            height: 78vh;
         }
      }
      ._root {
         width: 60rpx;
         height: 60rpx;
         position: absolute;
         left: 40rpx;
         top: 5vh
      }
   }
}
</style>