vue实现预览图片及视频组件

2023-11-06

 

 组件代码内容 MediaViewer.vue

<template>
  <teleport to="body">
    <transition name="viewer-fade">
      <div ref="wrapper" :tabindex="-1" class="el-image-viewer__wrapper" :style="{ zIndex }">
        <div class="el-image-viewer__mask" @click.self="hideOnClickModal && hide()"></div>
        <!-- CLOSE -->
        <span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
          <el-icon>
            <Close />
          </el-icon>
        </span>
        <!-- ARROW -->
        <template v-if="!isSingle">
          <span class="el-image-viewer__btn el-image-viewer__prev" :class="{ 'is-disabled': !infinite && isFirst }"
            @click="prev">
            <el-icon>
              <ArrowLeft />
            </el-icon>
          </span>
          <span class="el-image-viewer__btn el-image-viewer__next" :class="{ 'is-disabled': !infinite && isLast }"
            @click="next">
            <el-icon>
              <ArrowRight />
            </el-icon>
          </span>
        </template>
        <!-- ACTIONS -->
        <div v-if="isImage" class="el-image-viewer__btn el-image-viewer__actions">
          <div class="el-image-viewer__actions__inner">
            <el-icon @click="handleActions('zoomOut')">
              <ZoomOut />
            </el-icon>
            <el-icon @click="handleActions('zoomIn')">
              <ZoomIn />
            </el-icon>
            <i class="el-image-viewer__actions__divider"></i>

            <el-icon @click="toggleMode">
              <component :is="mode.icon"></component>
            </el-icon>
            <i class="el-image-viewer__actions__divider"></i>

            <el-icon @click="handleActions('anticlocelise')">
              <RefreshLeft />
            </el-icon>

            <el-icon @click="handleActions('clocelise')">
              <RefreshRight />
            </el-icon>
          </div>
        </div>
        <!-- CANVAS -->
        <div class="el-image-viewer__canvas">
          <template v-for="(url, i) in urlList">
            <img v-if="i === index && isImage" ref="media" :key="url" :src="url" :style="mediaStyle"
              class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"
              @mousedown="handleMouseDown" />
            <video controls="controls" v-if="i === index && isVideo" ref="media" :key="url" :src="url" :style="mediaStyle"
              class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"
              @mousedown="handleMouseDown"></video>
          </template>
        </div>
      </div>
    </transition>
  </teleport>
</template>

<script setup>
import { computed, ref, onMounted, watch, nextTick } from 'vue'

const props = defineProps({
  urlList: {
    type: Array,
    default: () => [],
  },
  zIndex: {
    type: Number,
    default: 9999,
  },
  initialIndex: {
    type: Number,
    default: 0,
  },
  infinite: {
    type: Boolean,
    default: true,
  },
  hideOnClickModal: {
    type: Boolean,
    default: false,
  }, // 视频格式
  videoType: {
    type: Array,
  },
  //图片格式
  imgType: {
    type: Array,
  }
},)
const emits = defineEmits(['close', "switch"])
const EVENT_CODE = {
  tab: 'Tab',
  enter: 'Enter',
  space: 'Space',
  left: 'ArrowLeft', // 37
  up: 'ArrowUp', // 38
  right: 'ArrowRight', // 39
  down: 'ArrowDown', // 40
  esc: 'Escape',
  delete: 'Delete',
  backspace: 'Backspace',
}


const isFirefox = function () {
  return !!window.navigator.userAgent.match(/firefox/i)
}

const rafThrottle = function (fn) {
  let locked = false
  return function (...args) {
    if (locked) return
    locked = true
    window.requestAnimationFrame(() => {
      fn.apply(this, args)
      locked = false
    })
  }
}

const Mode = {
  CONTAIN: {
    name: 'contain',
    icon: 'FullScreen',
  },
  ORIGINAL: {
    name: 'original',
    icon: 'ScaleToOriginal',
  },
}

const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'

let _keyDownHandler = null
let _mouseWheelHandler = null
let _dragHandler = null
const loading = ref(true)
const index = ref(props.initialIndex)
const wrapper = ref(null)
const media = ref(null)
const mode = ref(Mode.CONTAIN)
const transform = ref({
  scale: 1,
  deg: 0,
  offsetX: 0,
  offsetY: 0,
  enableTransition: false,
})

const isSingle = computed(() => {
  const { urlList } = props
  return urlList.length <= 1
})

const isFirst = computed(() => {
  return index.value === 0
})

const isLast = computed(() => {
  return index.value === props.urlList.length - 1
})

const currentMedia = computed(() => {
  return props.urlList[index.value]
})

const isVideo = computed(() => {
  const currentUrl = props.urlList[index.value]
  const name = currentUrl.split('.').slice(-1)[0].toLocaleLowerCase()
  const isVideo = props.videoType.find(itemVideo => itemVideo == name);
  if (isVideo) {
    return true
  } else {
    return false
  }
})

const isImage = computed(() => {
  const currentUrl = props.urlList[index.value]
  const name = currentUrl.split('.').slice(-1)[0].toLocaleLowerCase()
  const isImg = props.imgType.find(itemVideo => itemVideo == name);
  if (isImg) {
    return true
  } else {
    return false
  }
})

const mediaStyle = computed(() => {
  const { scale, deg, offsetX, offsetY, enableTransition } =
    transform.value
  const style = {
    transform: `scale(${scale}) rotate(${deg}deg)`,
    transition: enableTransition ? 'transform .3s' : '',
    marginLeft: `${offsetX}px`,
    marginTop: `${offsetY}px`,
  }
  if (mode.value.name === Mode.CONTAIN.name) {
    style.maxWidth = style.maxHeight = '100%'
  }
  return style
})
function hide() {
  deviceSupportUninstall()
  emits('close')
}

function deviceSupportInstall() {
  _keyDownHandler = rafThrottle((e) => {
    switch (e.code) {
      // ESC
      case EVENT_CODE.esc:
        hide()
        break
      // SPACE
      case EVENT_CODE.space:
        toggleMode()
        break
      // LEFT_ARROW
      case EVENT_CODE.left:
        prev()
        break
      // UP_ARROW
      case EVENT_CODE.up:
        handleActions('zoomIn')
        break
      // RIGHT_ARROW
      case EVENT_CODE.right:
        next()
        break
      // DOWN_ARROW
      case EVENT_CODE.down:
        handleActions('zoomOut')
        break
    }
  })

  _mouseWheelHandler = rafThrottle((e) => {
    const delta = e.wheelDelta ? e.wheelDelta : -e.detail
    if (delta > 0) {
      handleActions('zoomIn', {
        zoomRate: 0.015,
        enableTransition: false,
      })
    } else {
      handleActions('zoomOut', {
        zoomRate: 0.015,
        enableTransition: false,
      })
    }
  })

  document.addEventListener('keydown', _keyDownHandler, false)
  document.addEventListener(
    mousewheelEventName,
    _mouseWheelHandler,
    false
  )
}

function deviceSupportUninstall() {
  document.removeEventListener('keydown', _keyDownHandler, false)
  document.removeEventListener(
    mousewheelEventName,
    _mouseWheelHandler,
    false
  )
  _keyDownHandler = null
  _mouseWheelHandler = null
}

function handleMediaLoad() {
  loading.value = false
}

function handleMediaError(e) {
  loading.value = false
}

function handleMouseDown(e) {
  if (loading.value || e.button !== 0) return

  const { offsetX, offsetY } = transform.value
  const startX = e.pageX
  const startY = e.pageY

  const divLeft = wrapper.value.clientLeft
  const divRight =
    wrapper.value.clientLeft + wrapper.value.clientWidth
  const divTop = wrapper.value.clientTop
  const divBottom =
    wrapper.value.clientTop + wrapper.value.clientHeight

  _dragHandler = rafThrottle((ev) => {
    transform.value = {
      ...transform.value,
      offsetX: offsetX + ev.pageX - startX,
      offsetY: offsetY + ev.pageY - startY,
    }
  })
  document.addEventListener('mousemove', _dragHandler, false)
  document.addEventListener(
    'mouseup',
    (e) => {
      const mouseX = e.pageX
      const mouseY = e.pageY
      if (
        mouseX < divLeft ||
        mouseX > divRight ||
        mouseY < divTop ||
        mouseY > divBottom
      ) {
        reset()
      }
      document.removeEventListener(
        'mousemove',
        _dragHandler,
        false
      )
    },
    false
  )

  e.preventDefault()
}

function reset() {
  transform.value = {
    scale: 1,
    deg: 0,
    offsetX: 0,
    offsetY: 0,
    enableTransition: false,
  }
}

function toggleMode() {
  if (loading.value) return

  const modeNames = Object.keys(Mode)
  const modeValues = Object.values(Mode)
  const currentMode = mode.value.name
  const index = modeValues.findIndex((i) => i.name === currentMode)
  const nextIndex = (index + 1) % modeNames.length
  mode.value = Mode[modeNames[nextIndex]]
  reset()
}

function prev() {
  if (isFirst.value && !props.infinite) return
  const len = props.urlList.length
  index.value = (index.value - 1 + len) % len
}

function next() {
  if (isLast.value && !props.infinite) return
  const len = props.urlList.length
  index.value = (index.value + 1) % len
}

function handleActions(action, options = {}) {
  if (loading.value) return
  const { zoomRate, rotateDeg, enableTransition } = {
    zoomRate: 0.2,
    rotateDeg: 90,
    enableTransition: true,
    ...options,
  }
  switch (action) {
    case 'zoomOut':
      if (transform.value.scale > 0.2) {
        transform.value.scale = parseFloat(
          (transform.value.scale - zoomRate).toFixed(3)
        )
      }
      break
    case 'zoomIn':
      transform.value.scale = parseFloat(
        (transform.value.scale + zoomRate).toFixed(3)
      )
      break
    case 'clocelise':
      transform.value.deg += rotateDeg
      break
    case 'anticlocelise':
      transform.value.deg -= rotateDeg
      break
  }
  transform.value.enableTransition = enableTransition
}

watch(currentMedia, () => {
  nextTick(() => {
    const $media = media.value
    if (!$media.complete) {
      loading.value = true
    }
  })
})

watch(index, (val) => {
  reset()
  emits('switch', val)
})

onMounted(() => {
  deviceSupportInstall()
  // add tabindex then wrapper can be focusable via Javascript
  // focus wrapper so arrow key can't cause inner scroll behavior underneath
  wrapper.value?.focus?.()
})


</script>

引用  VideoOrImagePreview.vue

<template>
  <!-- 参数已element-plus图片预览组件相似 -->
  <MediaViewer v-if="isShow" :url-list="realSrcList" :videoType="videoType" :imgType="imgType" @close="closeViewer" />
  <div @click="openViewer" :style="`width:${realWidth};height:${realHeight};`" class="viewer">
    <el-image v-if="isImage" class="viewer-img" fit="cover" :src="realSrc" />
    <div v-if="isVideo" style="z-index: 0;position: relative;">
      <video :src="realSrc" :width="width" style="z-index: -1;position: relative;" :height="height" controls
        disablePictureInPicture="true" controlslist="nodownload noremoteplayback"></video>
    </div>
  </div>
</template>
<script setup>
import { isExternal } from "@/utils/validate";
import MediaViewer from "../MediaViewer"
const props = defineProps({
  src: {
    type: String,
    default: ""
  },
  width: {
    type: [Number, String],
    default: ""
  },
  height: {
    type: [Number, String],
    default: ""
  },
  // 视频格式
  videoType: {
    type: Array,
    default: ["avi", "wmv", "mpg", "mpeg", "mov", "rm", "ram", "swf", "flv", "mp4", "mp3", "wma", "avi", "rm", "rmvb", "flv", "mpg", "mkv"]
  },
  //图片格式
  imgType: {
    type: Array,
    default: ['svgz', 'pjp', 'png', 'ico', 'avif', 'tiff', 'tif', 'jfif', 'svg', 'xbm', 'pjpeg', 'webp', 'jpg', 'jpeg', 'bmp', 'gif']
  }
});
const isShow = ref(false)
// 默认显示第一张处理
const realSrc = computed(() => {
  if (!props.src) {
    return;
  }
  let real_src = props.src.split(",")[0];
  if (isExternal(real_src)) {
    return real_src;
  }
  return import.meta.env.VITE_APP_BASE_API + real_src;
});
// 判断第一张是否为图片
const isImage = computed(() => {
  if (!realSrc.value) {
    return;
  }
  const name = realSrc.value.split('.').slice(-1)[0].toLocaleLowerCase()
  const isImg = props.imgType.find(itemVideo => itemVideo == name);
  if (isImg) {
    return true
  } else {
    return false
  }
})
// 判断第一张是否为视频
const isVideo = computed(() => {
  if (!realSrc.value) {
    return;
  }
  const name = realSrc.value.split('.').slice(-1)[0].toLocaleLowerCase()
  const isVideo = props.videoType.find(itemVideo => itemVideo == name);
  if (isVideo) {
    return true
  } else {
    return false
  }
})
const realSrcList = computed(() => {
  if (!props.src) {
    return;
  }
  let real_src_list = props.src.split(",");
  let srcList = [];
  real_src_list.forEach(item => {
    if (isExternal(item)) {
      return srcList.push(item);
    }
    return srcList.push(import.meta.env.VITE_APP_BASE_API + item);
  });
  return srcList;
});
const realWidth = computed(() =>
  typeof props.width == "string" ? props.width : `${props.width}px`
);

const realHeight = computed(() =>
  typeof props.height == "string" ? props.height : `${props.height}px`
);
// 关闭预览
function closeViewer() {
  isShow.value = false
}
//打开预览
function openViewer() {
  isShow.value = true
}
</script>
<style lang="scss" scoped>
.viewer {
  cursor: pointer;
  border-radius: 4px;
  overflow: hidden;

  .viewer-img {
    width: 100%;
    height: 100%;
    transition: all 0.3s;
  }

  .viewer-img:hover {
    transform: scale(1.2);
  }
}
</style>
/**
 * 判断path是否为外链
 * @param {string} path
 * @returns {Boolean}
 */
 export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

使用

<template>
    <videoorimage-preview v-if="arr" :src="arr"                                                 :width="50" :height="50" />
</template>

注:element-plus,vue3

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue实现预览图片及视频组件 的相关文章

  • Pandas知识点-逻辑运算

    Pandas知识点 逻辑运算 逻辑运算在代码中基本是必不可少的 Pandas的逻辑运算与Python基础语法中的逻辑运算存在一些差异 所以本文介绍Pandas中的逻辑运算符和逻辑运算 本文使用的数据来源于网易财经 具体下载方法可以参考 ht

随机推荐

  • RuntimeError: CUDA error: initialization error when calling `cusparseCreate(handle)

    原代码 tf swingbase arm torch matmul torch inverse tf base upper torch inverse tf uppernew base 改成 tf swingbase arm torch m
  • vscode中终端字体设置

    整理了一些vscode中终端字体及各种样式的设定 废话不多说 看下面 把下面的粘贴到 setting json 中 根据个人需要可以更改样式 瞎搞的 大部分应该准确 自己调试出来的 workbench colorCustomizations
  • 沈阳师范大学C++ pta第三章 指针引用

    沈阳师范大学C pta第三章 指针引用 判断题 1 字符串常量实质上是一个指向该字符串首字符的指针常量 T 2 定义一个指针数组并初始化赋值若干个字符串常量 则指针数组并不存放这些字符串 而仅仅指向各个字符串 T 3 指向整数指针的指针与指
  • idea 配置详解 (二) 之editor 详解

    3 File Settings Editor 3 1 File Settings Editor General 3 1 1 File Settings Editor General Auto Import 3 1 2 File Settin
  • Qt应用开发(基础篇)——输入对话框 QInputDialog

    一 前言 QInputDialog类继承于QDialog 是一个简单方便的对话框 用于从用户获取单个值 对话框窗口 QDialog QInputDialog输入对话框带有一个文本标签 一个输入框和标准按钮 输入内容可以字符 数字和选项 文本
  • html 浏览器存储方式

    浏览器有三种本地存储方式 1 localstorage 2 sessionStorage 3 cookie 浏览器 F12 打开调试模式 可以看到 点击对应域名 可以看到当前域名下存储的数据 是以key value形式存储的 三种方式的共同
  • 自己动手开发编译器之参考博客

    http www cnblogs com Ninputer archive 2011 06 10 2077991 html 2588239 可以参考这个博客进行开发 加油哈
  • 短视频平台-小说推文(番茄小说)推广任务详情

    字节旗下平台 番茄小说 今日头条 抖音故事 抖音漫画官方每周只出一次数据 预计每周二出上周四之前的数据 有时官方回传数据较晚 会延迟到周三出 请达人知悉 注意 再次强调 番茄拉新规则 是以设备第一次下载番茄小说后搜的第一个别名为一个有效拉新
  • vue3实现 多个input框输入 自动聚焦下一个

    我最近要实现 车牌号输入框的功能 vant里有密码输入框 功能一样 但是vant只有数字键盘 不符合我的需求 所
  • java在注解中绑定方法参数的解决方案

    我们有这样子的需求 需要记录用户操作某个方法的信息并记录到日志里面 例如 用户在保存和更新任务的时候 我们需要记录下用户的ip 具体是保存还是更新 调用的是哪个方法 保存和更新的任务名称以及操作是否成功 这里最好的技术就是spring ao
  • scrapyd,scrapy部署

    Library Frameworks Python framework Versions 3 7 bin scrapyd deploy 23 ScrapyDeprecationWarning Module scrapy utils http
  • 腾讯AI战略详解:技术社会与创新图景

    来源 腾讯研究院 概要 11月8日上午 以 开放 创想 为主题的2017腾讯全球合作伙伴大会在成都开幕 11月8日上午 以 开放 创想 为主题的2017腾讯全球合作伙伴大会在成都开幕 来自全球的350位国内外顶级大咖 500家主流媒体 数万
  • jdbc oracle 需要jar,什么jdbc jar用于oracle 11g&jdk 1.6以及如何连接到db本身

    gt Oracle将Jar与Oracle客户端或服务器安装捆绑在一起 可以在 ORACLE HOME jdbc lib ojdbc6 jar中找到 我总是用那个 gt Driver类名是oracle jdbc OracleDriver UR
  • [JavaEE系列] Thread类的基本用法

    文章目录 线程创建 第一类 继承 Thread 类 继承 Thread 类 重写 run 方法 使用匿名内部类 继承 Thread 类 重写 run 方法 第二类 实现 Runnable 接口 实现 Runnable 接口 重写 run 方
  • 趣头条基于 Flink+ClickHouse 构建实时数据分析平台

    摘要 本文由趣头条数据平台负责人王金海分享 主要介绍趣头条 Flink to Hive 小时级场景和 Flink to ClickHouse 秒级场景 内容分为以下四部分 一 业务场景与现状分析 二 Flink to Hive 小时级场景
  • 小米红米利用安装徕卡相机(附安装包)

    在帖子里说用adb安装的过程 安装狮的教程在分享的包里 测试设备 小米12pro 准备 手机和电脑在一个局域网或者用数据线连接 准备好安装包 1 手机打开开发者选项 打开无线usb调试 老安卓设备可以用数据线连接 进入设置 我的设备 连续点
  • maven的命令-deploy

    maven deploy命令的含义 maven deploy命令是将你负责的模块发布到你配置的仓库位置 多模块开发中使用deploy命令 配置仓库位置 一般公司都会搭建自己的公司私服 用于加速获取jar以及管理公司的一些技术沉淀工具包之类的
  • go语言开发工具sublime text3 + gosublime配置

    开始go语言开发时 网上google了下go的开发工具 大都推荐 sublime text3 gosublime 但是实际操作中gosublime不能直接安装 需要自己手动安装 将自己的安装过程整理一下 1 sublime text 3安装
  • Oss上传

    package com yazq hszm utils import android content Context import com alibaba sdk android oss ClientConfiguration import
  • vue实现预览图片及视频组件

    组件代码内容 MediaViewer vue