Vue3图片预览(Image)

news2024/12/23 11:19:08

本图片预览组件主要包括以下功能:

  • 展示图片时,可设置鼠标悬浮时的预览文本;图像无法加载时要显示的描述;自定义图像高度和宽度;设置图像如何适应容器高度和宽度( fill(填充) | contain(等比缩放包含) | cover(等比缩放覆盖));传入单张图像或图像数组;设置图像缩放比率;设置最大最小缩放比例;

  • 单张图片预览时,可顺时针旋转或逆时针旋转;放大缩小;鼠标任意拖动;鼠标双击图片还原;使用触摸板或鼠标滚轮控制图片缩放;

  • 多张图片预览时,可点击左右切换按钮预览多张图片;使用键盘上下左右按键进行图片切换;设置是否可以循环切换图片;

可自定义设置以下属性:

  • 图像无法加载时显示的描述(alt),类型:string,默认 'image'

  • 图像宽度(width),类型:string | number,单位px,默认 300

  • 图像高度(height),类型:string | number,默认 '100%'

  • 图形如何适应容器高度和宽度(fit),类型:string,默认'contain',可选 fill(填充) | contain(等比缩放包含) | cover(等比缩放覆盖)

  • 图像地址 | 图像地址数组(src),类型:string | Array<{src: string, alt?: string}>,默认 ''

  • 预览文本(preview),类型:string | slot,默认 '预览'

  • 每次缩放比率(zoomRatio),类型:number,默认 0.1

  • 最小缩放比例(minZoomScale),类型:number,默认 0.1

  • 最大缩放比例(maxZoomScale),类型:number,默认 10

  • 缩放移动旋转图片后,是否可以双击还原(resetOnDbclick),类型:boolean,默认 true

  • 是否可以循环切换图片(loop),类型:boolean,默认 false

效果如下图:

正常展示时样式:

图片加载时样式:

 鼠标悬浮时样式:

预览时样式:

  ①创建图片预览组件Image.vue:

<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
interface Image {
  src: string // 图像地址
  alt?: string // 图像无法加载时显示的描述
}
interface Props {
  alt?: string // 图像无法加载时显示的描述
  width?: string|number // 图像宽度
  height?: string|number // 图像高度
  fit?: string // 图像如何适应容器高度和宽度
  src?: string|Image[] // 图像地址 | 图像地址数组
  preview?: string // 预览文本 string | slot
  zoomRatio?: number // 每次缩放比率
  minZoomScale?: number // 最小缩放比例
  maxZoomScale?: number // 最大缩放比例
  resetOnDbclick?: boolean // 缩放移动旋转图片后,是否可以双击还原
  loop?: boolean // 是否可以循环切换图片
}
const props = withDefaults(defineProps<Props>(), {
  alt: 'image',
  width: 300,
  height: '100%',
  fit: 'contain', // 可选 fill(填充) | contain(等比缩放包含) | cover(等比缩放覆盖)
  src: '',
  preview: '预览',
  zoomRatio: 0.1,
  minZoomScale: 0.1,
  maxZoomScale: 10,
  resetOnDbclick: true,
  loop: false
})
const imageWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return props.width
  }
})
const imageHeight = computed(() => {
  if (typeof props.height === 'number') {
    return props.height + 'px'
  } else {
    return props.height
  }
})
const images = computed(() => {
  if (Array.isArray(props.src)) {
    return props.src
  } else {
    return [{
      src: props.src,
      alt: props.alt
    }]
  }
})
const imageCount = computed(() => {
  return images.value.length
})
onMounted(() => {
  // 监听键盘切换事件
  document.addEventListener('keydown', keyboardSwitch)
})
onUnmounted(() => {
  // 移除键盘切换事件
  document.removeEventListener('keydown', keyboardSwitch)
})
const complete = ref(false) // 图片是否加载完成
const loaded = ref(false) // 预览图片是否加载完成
const previewIndex = ref(0) // 当前预览的图片索引
const showPreview = ref(false) // 是否显示预览
const rotate = ref(0) // 预览图片旋转角度
const scale = ref(1) // 缩放比例
const sourceX = ref(0) // 拖动开始时位置
const sourceY = ref(0) // 拖动开始时位置
const dragX = ref(0) // 拖动横向距离
const dragY = ref(0) // 拖动纵向距离
const image = ref() // 预览图片模板引用
function keyboardSwitch (e: KeyboardEvent) {
  e.preventDefault()
  if (showPreview.value && imageCount.value > 1) {
    if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
      if (props.loop) {
        onSwitchLeft()
      } else {
        if (previewIndex.value > 0) {
          onSwitchLeft()
        }
      }
    }
    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
      if (props.loop) {
        onSwitchRight()
      } else {
        if (previewIndex.value < imageCount.value - 1) {
          onSwitchRight()
        }
      }
    }
  }
}
function onComplete () { // 图片加载完成
  complete.value = true
}
function onLoaded () { // 预览图片加载完成
  loaded.value = true
}
function onPreview () {
  scale.value = 1
  rotate.value = 0
  dragX.value = 0
  dragY.value = 0
  showPreview.value = true
}
function onClose () {
  showPreview.value = false
}
// 消除js加减精度问题的加法函数
function add (num1: number, num2: number) {
  const num1DeciStr = String(num1).split('.')[1]
  const num2DeciStr = String(num2).split('.')[1]
  let maxLen = Math.max(num1DeciStr?.length || 0, num2DeciStr?.length || 0) // 两数中最长的小数位长度
  let num1Str = num1.toFixed(maxLen) // 补零,返回字符串
  let num2Str = num2.toFixed(maxLen)
  const result = +(num1Str.replace('.', '')) + +(num2Str.replace('.', '')) // 转换为整数相加
  return result / Math.pow(10, maxLen)
}
function onZoomin () { // 放大
  if (scale.value + props.zoomRatio > props.maxZoomScale) {
    scale.value = props.maxZoomScale
  } else {
    scale.value = add(scale.value, props.zoomRatio)
  }
}
function onZoomout () { // 缩小
  if (scale.value - props.zoomRatio < props.minZoomScale) {
    scale.value = props.minZoomScale
  } else {
    scale.value = add(scale.value, -props.zoomRatio)
  }
}
function onWheel (e: WheelEvent) { // 鼠标滚轮缩放
  // e.preventDefault() // 禁止浏览器捕获滑动事件
  const scrollZoom = e.deltaY * props.zoomRatio // 滚轮的纵向滚动量
  if (scale.value === props.minZoomScale && scrollZoom > 0) {
    return
  }
  if (scale.value === props.maxZoomScale && scrollZoom < 0) {
    return
  }
  if (scale.value - scrollZoom < props.minZoomScale) {
    scale.value = props.minZoomScale
  } else if (scale.value - scrollZoom > props.maxZoomScale) {
    scale.value = props.maxZoomScale
  } else {
    scale.value = add(scale.value, -scrollZoom)
  }
}
function onResetZoom () { // 双击图片重置为初始状态
  scale.value = 1
  rotate.value = 0
  dragX.value = 0
  dragY.value = 0
}
function onAnticlockwiseRotate () { // 逆时针旋转
  rotate.value -= 90
}
function onClockwiseRotate () { // 顺时针旋转
  rotate.value += 90
}
function onMouseDown (event: MouseEvent) {
  // event.preventDefault() // 消除拖动元素时的阴影
  const imageRect = image.value[0].getBoundingClientRect()
  const top = imageRect.top // 图片上边缘距浏览器窗口上边界的距离
  const bottom = imageRect.bottom // 图片下边缘距浏览器窗口上边界的距离
  const right = imageRect.right // 图片右边缘距浏览器窗口左边界的距离
  const left = imageRect.left // 图片左边缘距浏览器窗口左边界的距离
  const viewportWidth = document.body.clientWidth
  const viewportHeight = document.body.clientHeight
  sourceX.value = event.clientX // 鼠标按下时相对于视口左边缘的X坐标
  sourceY.value = event.clientY // 鼠标按下时相对于视口上边缘的Y坐标
  const sourceDragX = dragX.value // 鼠标按下时图片的X轴偏移量
  const sourceDragY = dragY.value // 鼠标按下时图片的Y轴偏移量
  document.onmousemove = (e: MouseEvent) => {
    // e.clientX返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
    dragX.value = sourceDragX + e.clientX - sourceX.value
    dragY.value = sourceDragY + e.clientY - sourceY.value
  }
  document.onmouseup = () => {
    if (dragX.value > sourceDragX + viewportWidth - right) { // 溢出视口右边缘
      dragX.value = sourceDragX + viewportWidth - right
    }
    if (dragX.value < sourceDragX - left) { // 溢出视口左边缘
      dragX.value = sourceDragX - left
    }
    if (dragY.value > sourceDragY + viewportHeight - bottom) { // 溢出视口下边缘
      dragY.value = sourceDragY + viewportHeight - bottom
    }
    if (dragY.value < sourceDragY - top) { // 溢出视口上边缘
      dragY.value = sourceDragY - top
    }
    document.onmousemove = null
  }
}
function onSwitchLeft () {
  previewIndex.value = (previewIndex.value - 1 + imageCount.value) % imageCount.value
  loaded.value = false
}
function onSwitchRight () {
  previewIndex.value = (previewIndex.value + 1) % imageCount.value
  loaded.value = false
}
</script>
<template>
  <div class="m-image-wrap">
    <div class="m-image" :class="{'image-hover-mask': complete}" :style="`width: ${imageWidth}; height: ${imageHeight};`">
      <div class="u-spin-circle" v-show="!complete"></div>
      <img class="u-image" :style="`object-fit: ${fit};`" @load="onComplete" :src="images[0].src" :alt="images[0].alt" />
      <div class="m-image-mask" @click="onPreview">
        <div class="m-image-mask-info">
          <svg class="u-eye" focusable="false" data-icon="eye" aria-hidden="true" viewBox="64 64 896 896"><path d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"></path></svg>
          <slot name="preview">{{ preview }}</slot>
        </div>
      </div>
    </div>
    <Transition name="mask">
      <div class="m-preview-mask" v-show="showPreview"></div>
    </Transition>
    <Transition name="preview">
      <div class="m-preview-wrap" v-show="showPreview" @click.self="onClose" @wheel.prevent="onWheel">
        <div class="m-preview-body">
          <div class="m-preview-operations">
            <div class="u-preview-operation" @click="onClose">
              <svg class="u-icon" focusable="false" data-icon="close" aria-hidden="true" viewBox="64 64 896 896"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"></path></svg>
            </div>
            <div class="u-preview-operation" :class="{'u-operation-disabled': scale===maxZoomScale}" @click="onZoomin">
              <svg class="u-icon" focusable="false" data-icon="zoom-in" aria-hidden="true" viewBox="64 64 896 896"><path d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"></path></svg>
            </div>
            <div class="u-preview-operation" :class="{'u-operation-disabled': scale===minZoomScale}" @click="onZoomout">
              <svg class="u-icon" focusable="false" data-icon="zoom-out" aria-hidden="true" viewBox="64 64 896 896"><path d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"></path></svg>
            </div>
            <div class="u-preview-operation" @click="onClockwiseRotate">
              <svg class="u-icon" focusable="false" data-icon="rotate-right" aria-hidden="true" viewBox="64 64 896 896"><path d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2A399.75 399.75 0 00123.5 631h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z"></path><path d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z"></path></svg>
            </div>
            <div class="u-preview-operation" @click="onAnticlockwiseRotate">
              <svg class="u-icon" focusable="false" data-icon="rotate-left" aria-hidden="true" viewBox="64 64 896 896"><path d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z"></path><path d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8a325.95 325.95 0 016.5 140.9h74.9c14.8-103.6-11.3-213-81-302.3z"></path></svg>
            </div>
          </div>
          <div
            class="m-preview-image"
            :style="`transform: translate3d(${dragX}px, ${dragY}px, 0px);`"
            @mousedown.prevent="onMouseDown">
            <div class="u-spin-circle" v-show="!loaded"></div>
            <img
              v-for="(image, index) in images" :key="index"
              v-show="previewIndex === index"
              ref="image"
              class="u-preview-image"
              :style="`transform: scale3d(${scale}, ${scale}, 1) rotate(${rotate}deg);`"
              :src="image.src"
              :alt="image.alt"
              @load="onLoaded"
              @dblclick="resetOnDbclick ? onResetZoom():(e: Event) => e.preventDefault()"/>
          </div>
          <template v-if="imageCount > 1">
            <div
              class="m-switch-left"
              :class="{'u-switch-disabled': previewIndex === 0 && !loop}"
              @click="previewIndex === 0 ? (e: Event) => e.preventDefault():onSwitchLeft()">
              <svg focusable="false" class="u-switch" data-icon="left" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg>
            </div>
            <div
              class="m-switch-right"
              :class="{'u-switch-disabled': previewIndex === imageCount - 1 && !loop}"
              @click="previewIndex === imageCount - 1 ? (e: Event) => e.preventDefault():onSwitchRight()">
              <svg focusable="false" class="u-switch" data-icon="right" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg>
            </div>
          </template>
        </div>
      </div>
    </Transition>
  </div>
</template>
<style lang="less" scoped>
.mask-enter-active, .mask-leave-active {
  transition: all 0.15s ease-out;
}
.mask-enter-from, .mask-leave-to {
  opacity: 0;
  transform: scale(0.5);
}
.preview-enter-active, .preview-leave-active {
  transition: all 0.15s ease-out;
}
.preview-enter-from, .preview-leave-to {
  opacity: 0;
  transform: scale(0.1);
}
.u-spin-circle {
  position: absolute;
  inset: 0;
  margin: auto;
  pointer-events: none;
  display: inline-block;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border-width: 4px;
  border-style: solid;
  border-color: @themeColor;
  border-top-color: transparent; // 隐藏1/4圆
  animation: loadingCircle 1s infinite linear;
  -webkit-animation: loadingCircle 1s infinite linear;
}
@keyframes loadingCircle {
  100% {
    transform: rotate(360deg);
  }
}
.m-image-wrap {
  .image-hover-mask {
    &:hover {
      .m-image-mask {
        opacity: 1;
        pointer-events: auto;
      }
    }
  }
  .m-image {
    position: relative;
    display: inline-block;
    .u-image {
      width: 100%;
      height: 100%;
      object-fit: contain;
      vertical-align: middle;
    }
    .m-image-mask {
      // top right bottom left 简写为 inset: 0
      // insert 无论元素的书写模式、行内方向和文本朝向如何,其所定义的都不是逻辑偏移而是实体偏移
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      background: rgba(0, 0, 0, 0.5);
      cursor: pointer;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s;
      .m-image-mask-info {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        padding: 0 4px;
        .u-eye {
          display: inline-flex;
          align-items: center;
          margin-right: 4px;
          vertical-align:  -0.125em;
          width: 14px;
          height: 14px;
          fill: #FFF;
        }
      }
    }
  }
  .m-preview-mask {
    position: fixed;
    top: 0;
    inset-inline-end: 0;
    bottom: 0;
    inset-inline-start: 0;
    z-index: 1000;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.45);
  }
  .m-preview-wrap {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    overflow: auto;
    outline: 0;
    z-index: 1080;
    height: 100%;
    text-align: center;
    .m-preview-body {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      .m-preview-operations {
        display: flex;
        flex-direction: row-reverse;
        align-items: center;
        background: rgba(0, 0, 0, 0.1);
        height: 42px;
        pointer-events: auto;
        .u-preview-operation {
          line-height: 1;
          padding: 12px;
          cursor: pointer;
          transition: all 0.3s;
          &:not(:last-child) {
            margin-left: 12px;
          }
          &:hover {
            background: rgba(0,0,0,0.25);
          }
          .u-icon {
            width: 18px;
            height: 18px;
            vertical-align: bottom;
            fill: #FFF;
          }
        }
        .u-operation-disabled {
          color: rgba(255, 255, 255, 0.25);
          pointer-events: none;
          .u-icon {
            fill: rgba(255, 255, 255, 0.25);
          }
        }
      }
      .m-preview-image {
        position: absolute;
        inset: 0;
        transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
        display: flex;
        justify-content: center;
        align-items: center;
        .u-preview-image {
          max-width: 100%;
          max-height: 100%;
          vertical-align: middle;
          transform: scale3d(1, 1, 1);
          cursor: grab;
          transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
          user-select: none;
          pointer-events: auto;
        }
      }
      .m-switch-left {
        inset-inline-start: 12px;
        position: fixed;
        inset-block-start: 50%;
        z-index: 1081;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 40px;
        height: 40px;
        margin-top: -20px;
        color: rgb(255, 255, 255);
        background: rgba(0, 0, 0, 0.1);
        border-radius: 50%;
        transform: translateY(-50%);
        cursor: pointer;
        transition: all 0.3s;
        pointer-events: auto;
        .u-switch {
          width: 18px;
          height: 18px;
          fill: #FFF;
        }
      }
      .m-switch-right {
        inset-inline-end: 12px;
        position: fixed;
        inset-block-start: 50%;
        z-index: 1081;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 40px;
        height: 40px;
        margin-top: -20px;
        color: rgb(255, 255, 255);
        background: rgba(0, 0, 0, 0.1);
        border-radius: 50%;
        transform: translateY(-50%);
        cursor: pointer;
        transition: all 0.3s;
        pointer-events: auto;
        .u-switch {
          width: 18px;
          height: 18px;
          fill: #FFF;
        }
      }
      .u-switch-disabled {
        color: rgba(255, 255, 255, 0.25);
        background: transparent;
        cursor: not-allowed;
        .u-switch {
          fill: rgba(255, 255, 255, 0.25);
        }
      }
    }
  }
}
</style>

 ②在要使用的页面引入:

<script setup lang="ts">
import Image from './Image.vue'
import { ref } from 'vue'
const images = ref([
  {
    src: 'https://***.jpg',
    alt: 'image-1'
  },
  {
    src: 'https://***.jpg',
    alt: 'image-2'
  },
  {
    src: new URL(`../assets/images/1.jpg`, import.meta.url).href,
    alt: 'image-3'
  },
  {
    src: new URL(`../assets/images/2.jpg`, import.meta.url).href,
    alt: 'image-4'
  },
  {
    src: new URL(`../assets/images/3.jpg`, import.meta.url).href,
    alt: 'image-5'
  }
])
</script>
<template>
  <div>
    <h2 class="mb10">Image 图片基本使用</h2>
    <Image :width="400" :height="300" src="https://***.jpg" />
    <h2 class="mt30 mb10">多张图片预览,同时支持键盘 (left / right / up / down) 按键切换 (src: images)</h2>
    <Image :width="400" :height="300" :src="images" />
    <h2 class="mt30 mb10">多张图片预览,支持循环切换图片 (loop: true)</h2>
    <Image :width="400" :height="300" loop :src="images" />
    <h2 class="mt30 mb10">预览文本设为 preview 同时图片覆盖容器 (preview: preview & fit: cover)</h2>
    <Image :width="400" :height="300" fit="cover" src="https://***.jpg">
      <template #preview>
        <p class="u-pre">preview</p>
      </template>
    </Image>
    <h2 class="mt30 mb10">更改缩放比率和最大最小缩放比例 (zoomRatio: 0.2 & minZoomScale: 0.5 & maxZoomScale: 2)</h2>
    <Image
      :width="400"
      :height="300"
      :zoomRatio="0.2"
      :minZoomScale="0.5"
      :maxZoomScale="2"
      src="https://***.jpg" />
  </div>
</template>
<style lang="less" scoped>
.u-pre {
  display: inline-block;
  font-size: 16px;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/440182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node 06-包管理器

包管理工具 概念介绍 包是什么 『包』英文单词是 package &#xff0c;代表了一组特定功能的源码集合 包管理工具 管理『包』的应用软件&#xff0c;可以对「包」进行 下载安装 &#xff0c; 更新 &#xff0c; 删除&#xff0c; 上传等操作 借助包管理工具&#xff0c;可以…

ASEMI代理ADAU1701JSTZ-RL原装ADI车规级ADAU1701JSTZ-RL

编辑&#xff1a;ll ASEMI代理ADAU1701JSTZ-RL原装ADI车规级ADAU1701JSTZ-RL 型号&#xff1a;ADAU1701JSTZ-RL 品牌&#xff1a;ADI /亚德诺 封装&#xff1a;LQFP-48 批号&#xff1a;2023 安装类型&#xff1a;表面贴装型 引脚数量&#xff1a;48 类型&#xff1a;车…

向凯文·凯利提问:未来 5000 天我们将走向何处?

ChatGPT 的问世不禁让人遐想&#xff0c;接下来的 5000 天&#xff0c;将会发生什么事&#xff1f; 硅谷精神之父、世界互联网教父、《失控》《必然》的作者凯文凯利&#xff08;Kevin Kelly&#xff0c;以下简称 K.K.&#xff09;是这样预测的&#xff1a; 未来将会是一切都与…

WIFI-OmniPeek抓包

一、简介 有时候&#xff0c;我们需要抓取空口数据包来分析数据&#xff0c;此时就需要了解抓包软件如何使用。此时需要准备如下东西&#xff1a; SNIFFER&#xff1a;AC-12000 软件&#xff1a;OmniPeek 二&#xff1a;抓包 打开OmniPeek&#xff0c;新建捕获。 确认设备是否…

android studio ImageView和ImageButton和Button

1.ImageView 1.1代码显示 ImageView img findViewById(R.id.img); img.setImageResource(R.drawable.apple); 1.2XML <ImageViewandroid:layout_width"match_parent"android:layout_height"match_parent"android:id"id/img"android:src&qu…

如何用python写 翻译腔?时时刻刻优雅在线~

人生苦短&#xff0c;我用python 本文仅供参考 没有代码 看一乐呵就行 在家看完中配版国外经典电影之后&#xff0c;我的语气就变成了这样&#xff1a; " 我的老伙计&#xff0c;说真的&#xff0c;真是活见鬼。天气这么热&#xff0c;我们为什么不坐下喝杯咖啡呢&…

util-linix 实用程序包中包含了许多系统管理员常用的其它命令

util-linix 实用程序包中包含了许多系统管理员常用的其它命令。这些实用程序是由 Linux 内核组织发布的&#xff0c;这 107 条命令中几乎每一个都来自原本是三个单独的集合 —— fileutils、shellutils 和 textutils&#xff0c;2003 年它们被合并成一个包&#xff1a;util-lin…

家用洗地机有什么优缺点?平价洗地机推荐

随着社会经济的发展和人们生活水平的提高&#xff0c;对于清洁卫生的要求也越来越高。洗地机作为一种集高效、节能、环保、卫生等多重优点于一身的清洁设备&#xff0c;可以有效提高清洁效率和清洁质量&#xff0c;并且可以减少对环境的污染。不仅如此&#xff0c;洗地机的还有…

Linux线程:互斥锁、条件变量和信号量

本节重点&#xff1a; 1.学会线程同步。 2. 学会使用互斥量&#xff0c;条件变量&#xff0c;posix信号量&#xff0c;以及读写锁。 3. 理解基于读写锁的读者写者问题。 Linux线程互斥 相关概念&#xff1a; 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源 临界…

小红书达人种草怎么收费?

随着小红书平台的快速发展&#xff0c;用户数量在不断的上升&#xff0c;市场上也涌现出了很多的小红书营销公司&#xff0c;小红书营销主要是以小红书代写代发、品牌植入广告等方式来做品牌种草品宣。 小红书达人种草怎么收费这个问题&#xff0c;相信很多商家朋友都非常关心…

dockerfile 微服务 私有仓库

镜像编排 Dockerfile语法 CMD中的执行指令 放入前台运行 需要通过rpm -ql 包 | grep bao.service 看里面的启动命令 语法指令语法说明FROM基础镜像RUN制作镜像时执行的命令&#xff0c;可以有多个ADD复制文件到镜像&#xff0c;自动解压COPY复制文件到镜像&#xff0c;不解压…

Android Crash 前的最后抢救

众所周知,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出。而所谓安全气囊,是指在 Crash 发生时捕获异常,然后触发兜底逻辑,在程序退出前做最后的抢救。 一,Java捕获异常 在实现安全气囊之前,我们先思考一个问题,像 bugly、sentry 这种监控框架是如何…

深入认识VirtualPrivateNetwork

目录 一、认识什么是认证&#xff1f; 1.什么是数据认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段? 2.什么是身份认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段? 二、认识什么是VPN 1.什么VPN技术? 2.VPN技术有哪些分类? 3.IPSEC技术能…

c++常见算法

目录 1、常见遍历算法 1.1、for_each遍历算法 1.2、、transform算法 2、常见的查找算法 2.1、find 算法查找元素 2.1.1、find 算法查找基本类型 2.1.2、find 算法查找自定义类型 2.2、find_if 按照条件查找 2.3、adjacent_find算法 查找相邻重复元素 2.4、binary_sear…

JavaSE-Map

Map集合 5.0 重点掌握 能够说出Map集合的特点能够使Map集合添加方法保存数据使用“键找值””键值对“的方式遍历Map集合使用HashMap存储自定义键值对的数据完成HashMap案例 5.1 Map集合概述 现实生活中&#xff0c;经常会出现这么一种集合&#xff1a;如 身份证与个人&…

web自动化测试入门篇06 —— 元素定位进阶技巧

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

camunda的manual task节点用途

Camunda的Manual Task用于在流程中暂停执行&#xff0c;直到人工干预完成某个任务。与User Task不同&#xff0c;Manual Task没有分配给特定用户或用户组&#xff0c;而是需要手动启动并指定下一步流程。 Manual Task可以用于以下场景&#xff1a; 1、流程执行需要等待人工干…

13-nginx

一 初始Nginx 1 Nginx概述 Nginx是一款轻量级的Web服务器、反向代理服务器&#xff0c;由于它的内存占用少&#xff0c;启动极快&#xff0c;高并发能力强&#xff0c;在互联网项目中广泛应用。Nginx 专为性能优化而开发&#xff0c;使用异步非阻塞事件驱动模型。 常见服务器 …

MySQL基础——约束

前言 MySQL在我们工作中都会用到&#xff0c;那么我们最常接触的就是增删改查&#xff0c;而对于增删改查来说&#xff0c;我们更多的是查询。但是面试中&#xff0c;面试官又不会问你什么查询是怎么写的&#xff0c;都是问一些索引啊&#xff0c;事务啊&#xff0c; 底层结构…

搞懂 API:XML 和 Json的差异到底有多大

XML和JSON是两种常见的数据格式&#xff0c;它们在现代网络应用中起着重要的作用。本文将介绍XML和JSON的基础知识&#xff0c;并比较它们之间的差异。 XML XML&#xff0c;即可扩展标记语言&#xff08;eXtensible Markup Language&#xff09;&#xff0c;是一种用于描述数…