仿照elementui写图片放大的案例,但多加了下载按钮,vue3

javascript 复制代码
//组件
<template>
    <div class="custom-image-preview">
        <!-- 自定义预览层(替代默认预览) -->
        <div v-if="previewVisibleShow" class="custom-image-viewer" @click.self="closePreview">
            <div class="viewer-container">
                <!-- 图片展示区域 -->
                <div class="image-container" @wheel="handleWheel" @mousedown="startDrag" @mousemove="drag"
                    @mouseup="endDrag" @mouseleave="endDrag">
                    <img :src="currentImage" :style="imageStyle" class="preview-image" alt="预览图片" />
                </div>

                <!-- 顶部工具栏 -->
                <div class="viewer-toolbar top">
                    <span class="image-index">{{ currentIndex + 1 }} / {{ previewSrcList.length }}</span>
                    <div class="toolbar-actions">
                        <el-button circle @click="closePreview" :icon="Close" />
                    </div>
                </div>

                <!-- 底部工具栏 -->
                <div class="viewer-toolbar bottom">
                    <div class="zoom-controls">
                        <!-- 缩小 -->
                        <el-button circle @click="zoomOut" :icon="ZoomOut" :disabled="scale <= 0.5" />
                        <!-- 放大 -->
                        <el-button circle @click="zoomIn" :icon="ZoomIn" :disabled="scale >= 3" />
                        <!-- 旋转 -->
                        <el-button circle @click="rotateImage" :icon="RefreshRight"  />
                        <!-- 重置 -->
                        <el-button circle @click="resetImage" :icon="Refresh" />
                        <!-- 下载 -->
                        <el-button circle @click="downloadImage" :icon="Download" />
                    </div>
                </div>

                <!-- 左右切换按钮 -->
                <button v-if="previewSrcList.length > 1" class="nav-btn prev" @click="prevImage">
                    <el-icon>
                        <ArrowLeft />
                    </el-icon>
                </button>
                <button v-if="previewSrcList.length > 1" class="nav-btn next" @click="nextImage">
                    <el-icon>
                        <ArrowRight />
                    </el-icon>
                </button>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
    Download,
    Close,
    ZoomIn,
    ZoomOut,
    RefreshRight,
    Refresh,
    ArrowLeft,
    ArrowRight,
    PictureFilled
} from '@element-plus/icons-vue'

interface Props {
    src: string
    previewSrcList?: string[]
    initialIndex?: number
    imageName?: string
    previewVisibleShow: boolean
}

const props = withDefaults(defineProps<Props>(), {
    src: '',
    previewSrcList: () => [],
    initialIndex: 0,
    imageName: '图片',
    previewVisibleShow: false
})

// 定义 emits
const emit = defineEmits<{
  (e: 'updatePreviewVisible', value: boolean): void
}>()

// 预览状态
const previewVisible = ref(props.previewVisibleShow)
const currentIndex = ref(props.initialIndex)
const scale = ref(1)
const rotate = ref(0)
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const position = ref({ x: 0, y: 0 })

// 当前显示的图片
const currentImage = computed(() => {
    return props.previewSrcList[currentIndex.value] || props.src
})

// 图片样式
const imageStyle = computed(() => ({
    transform: `scale(${scale.value}) rotate(${rotate.value}deg) translate(${position.value.x}px, ${position.value.y}px)`,
    transition: isDragging.value ? 'none' : 'transform 0.3s'
}))

// 关闭预览
const closePreview = () => {
    // previewVisible.value = false
    emit('updatePreviewVisible', false)
    resetImage()
}

// 上一张
const prevImage = () => {
    if (currentIndex.value > 0) {
        currentIndex.value--
        resetImage()
    }else{
        // 循环到最后一张
        currentIndex.value = props.previewSrcList.length - 1
    }
}

// 下一张
const nextImage = () => {
    if (currentIndex.value < props.previewSrcList.length - 1) {
        currentIndex.value++
        resetImage()
    }else{
        // 循环到第一张
        currentIndex.value = 0
    }
}

// 放大
const zoomIn = () => {
    if (scale.value < 3) {
        scale.value = Math.min(scale.value + 0.2, 3)
    }
}

// 缩小
const zoomOut = () => {
    if (scale.value > 0.5) {
        scale.value = Math.max(scale.value - 0.2, 0.5)
    }
}

// 旋转
const rotateImage = () => {
    rotate.value = (rotate.value + 90) % 360
}

// 重置
const resetImage = () => {
    scale.value = 1
    rotate.value = 0
    position.value = { x: 0, y: 0 }
}

// 鼠标滚轮缩放
const handleWheel = (e: WheelEvent) => {
    e.preventDefault()
    const delta = e.deltaY > 0 ? -0.1 : 0.1
    scale.value = Math.min(Math.max(scale.value + delta, 0.5), 3)
}

// 拖拽开始
const startDrag = (e: MouseEvent) => {
    if (scale.value > 1) {
        isDragging.value = true
        dragStart.value = { x: e.clientX - position.value.x, y: e.clientY - position.value.y }
    }
}

// 拖拽中
const drag = (e: MouseEvent) => {
    if (isDragging.value && scale.value > 1) {
        position.value = {
            x: e.clientX - dragStart.value.x,
            y: e.clientY - dragStart.value.y
        }
    }
}

// 拖拽结束
const endDrag = () => {
    isDragging.value = false
}

// 下载图片
const downloadImage = async () => {
    try {
        // 显示加载提示
        const loading = ElMessage({
            message: '正在下载图片...',
            type: 'info',
            duration: 0
        })

        // 获取图片数据
        const response = await fetch(currentImage.value)
        const blob = await response.blob()

        // 创建下载链接
        const url = window.URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.style.display = 'none'
        link.href = url

        // 设置下载文件名
        const extension = blob.type.split('/')[1] || 'jpg'
        link.download = `${props.imageName}_${new Date().getTime()}.${extension}`

        // 触发下载
        document.body.appendChild(link)
        link.click()

        // 清理
        setTimeout(() => {
            document.body.removeChild(link)
            window.URL.revokeObjectURL(url)
            loading.close()
            ElMessage.success('图片下载成功')
        }, 100)

    } catch (error) {
        console.error('下载图片失败:', error)
        ElMessage.error('下载图片失败,请稍后重试')
    }
}

// 键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
    if (!previewVisible.value) return

    switch (e.key) {
        case 'Escape':
            closePreview()
            break
        case 'ArrowLeft':
            prevImage()
            break
        case 'ArrowRight':
            nextImage()
            break
        case '+':
            zoomIn()
            break
        case '-':
            zoomOut()
            break
        case 'r':
            rotateImage()
            break
    }
}

// 监听键盘事件
watch(previewVisible, (visible) => {
    if (visible) {
        window.addEventListener('keydown', handleKeyDown)
    } else {
        window.removeEventListener('keydown', handleKeyDown)
    }
})
watch(() => props.initialIndex, (newVal) => {
  currentIndex.value = newVal
})
</script>

<style scoped lang="scss">
.custom-image-preview {
    display: inline-block;
}

.custom-image-viewer {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.9);
    z-index: 10000;
    display: flex;
    justify-content: center;
    align-items: center;
}

.viewer-container {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.image-container {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    cursor: grab;

    &:active {
        cursor: grabbing;
    }
}

.preview-image {
    max-width: 90vw;
    max-height: 90vh;
    object-fit: contain;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    transition: transform 0.3s;
    user-select: none;
    pointer-events: none;
}

.viewer-toolbar {
    position: absolute;
    left: 0;
    right: 0;
    padding: 16px 24px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);
    color: white;
    z-index: 10001;

    &.top {
        top: 0;
    }

    &.bottom {
        bottom: 0;
        background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
        justify-content: center;
    }
}

.toolbar-actions {
    display: flex;
    gap: 12px;
}

.image-index {
    font-size: 14px;
    background: rgba(0, 0, 0, 0.3);
    padding: 4px 12px;
    border-radius: 20px;
}

.zoom-controls {
    display: flex;
    gap: 12px;
    align-items: center;
    background: #606266;
    padding: 8px 16px;
    border-radius: 40px;
    height: 44px;

    .zoom-percent {
        color: white;
        font-size: 14px;
        min-width: 60px;
        text-align: center;
    }
}

.nav-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.3);
    border: none;
    color: white;
    font-size: 24px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    transition: all 0.3s;
    z-index: 10001;

    &:hover {
        background: rgba(0, 0, 0, 0.6);
        transform: translateY(-50%) scale(1.1);
    }

    &.prev {
        left: 20px;
    }

    &.next {
        right: 20px;
    }
}

:deep(.el-button) {
    &.el-button--small {
        padding: 8px;
    }

    .el-icon {
        font-size: 16px;
    }
}
</style>
javascript 复制代码
//使用
 <CustomImagePreview :preview-visible-show="previewShow" :src="previewSrcUrl" :initial-index="showIndex"
      :preview-src-list="getImageList(srcList, 'near')" :image-name="`_近景`" @updatePreviewVisible="previewShow = $event" />
    
const srcList = [
  'https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg',
  'https://fuss10.elemecdn.com/1/8e/aeffeb4de74e2fde4bd74fc7b4486jpeg.jpeg'
]
// 获取图片列表(根据实际数据结构调整)
const getImageList = (row: any, type: 'near' | 'far') => {
  if (type === 'near') {
    return row.nearImageUrl ? [row.nearImageUrl] : srcList
  } else {
    return row.farImageUrl ? [row.farImageUrl] : srcList
  }
}
// 预览图片弹窗是否显示
const previewShow = ref(false)
// 预览图片弹窗当前显示的图片
const previewSrcUrl = ref('')
// 预览图片弹窗当前显示的图片索引
const showIndex = ref(0)
// 预览图片
const handlePreview = (src: string, index: number) => {
  previewShow.value = true
  previewSrcUrl.value = src;
  showIndex.value = index
}

写一个按钮调用此函数即可handlePreview
相关推荐
yanlele2 小时前
AI Coding 时代下, 关于你会写代码这件事儿, 还重要吗?
前端·javascript·ai编程
打瞌睡的朱尤3 小时前
Vue day9 购物车,项目,vant组件库,vw,路由
前端·javascript·vue.js
cc.ChenLy7 小时前
【CSS进阶】毛玻璃效果与代码解析
前端·javascript·css
何中应7 小时前
使用Jenkins部署前端项目(Vue)
前端·vue.js·jenkins
西门吹-禅7 小时前
node js 性能处理
开发语言·javascript·ecmascript
一只大侠的侠7 小时前
React Native for OpenHarmony:日期范围选择器实现
javascript·react native·react.js
一只大侠的侠9 小时前
React Native for OpenHarmony:DatePicker 日期选择器组件详解
javascript·react native·react.js
JosieBook9 小时前
【Vue】15 Vue技术——Vue计算属性简写:提升代码简洁性的高效实践
前端·javascript·vue.js
rfidunion9 小时前
springboot+VUE+部署(11。Nginx)
linux·vue.js·nginx