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

 

 组件代码内容 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

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]中提到了一个场景,即在一个项目中需要实现一个图片预览组件,支持放大预览、切换音视频和文件、缩放、旋转、移动等功能。该组件已经封装好,注释详细,可以直接拿来使用。引用\[2\]中提到了一个适用于Vue 3.0的视频播放插件vue3-video-play,该插件的UI和功能都很好,可以作为实现视频播放功能的参考。引用\[3\]中提到了定义一个transform样式对象,包含缩放、旋转、移动等属性,可以在computed计算属性中返回一个由该对象组成的CSS样式对象,然后在模板中将该样式对象绑定到图片上,当触发特效相关的事件时,改变对象中的某个值,就会重新计算computed属性,从而实时更新图片的样式。 综合以上引用内容,可以根据项目需求使用已封装好的图片预览组件,并结合vue3-video-play插件实现视频播放功能。同时,可以定义一个transform样式对象,通过computed属性实时更新图片的样式,以实现缩放、旋转、移动等效果。 #### 引用[.reference_title] - *1* *2* *3* [Vue3.0实现图片预览组件(媒体查看器)](https://blog.csdn.net/dabaooooq/article/details/128841487)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值