<think>我们将使用v-viewer库在Vue3中实现一个功能丰富的图片查看器。v-viewer是一个基于viewer.js的Vue图片查看器组件,支持旋转、缩放、翻转、播放等操作。
步骤:
1. 安装v-viewer和viewer.js
2. 在Vue3中全局注册v-viewer组件
3. 创建图片列表并使用v-viewer组件
4. 实现单击图片查看当前图片,并支持从第一张开始查看
代码实现:</think># Vue3中使用v-viewer实现高级图片预览功能
下面我将展示如何在Vue3中使用v-viewer库实现一个功能丰富的图片预览组件,支持单击预览、多图导航、缩放旋转等高级功能。
## 解决方案思路
1. 安装并配置v-viewer库
2. 创建图片列表组件
3. 实现单图预览和全图集预览功能
4. 添加自定义操作按钮(旋转、缩放、下载等)
5. 实现响应式设计和加载状态处理
## 完整代码实现
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3 + v-viewer 高级图片预览</title>
<!-- 引入v-viewer样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.36/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-viewer@1.6.4/dist/index.min.js"></script>
<style>
:root {
--primary: #4361ee;
--secondary: #3f37c9;
--accent: #4895ef;
--light: #f8f9fa;
--dark: #212529;
--success: #4cc9f0;
--warning: #f72585;
--gray: #6c757d;
--border: #dee2e6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
color: var(--dark);
}
.container {
max-width: 1400px;
margin: 40px auto;
background: white;
border-radius: 20px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
header {
background: linear-gradient(to right, var(--primary), var(--secondary));
color: white;
padding: 30px 40px;
text-align: center;
}
h1 {
font-size: 2.8rem;
margin-bottom: 15px;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
max-width: 700px;
margin: 0 auto;
}
.content {
padding: 40px;
}
.features {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 25px;
margin: 40px 0;
}
.feature-card {
width: 220px;
padding: 25px;
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
text-align: center;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: 0 12px 20px rgba(67, 97, 238, 0.15);
border-color: var(--accent);
}
.feature-icon {
font-size: 48px;
margin-bottom: 20px;
color: var(--primary);
}
.feature-title {
font-size: 1.3rem;
margin-bottom: 10px;
color: var(--dark);
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
flex-wrap: wrap;
}
.btn {
padding: 14px 32px;
font-size: 1.1rem;
border-radius: 50px;
border: none;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.btn-primary {
background: linear-gradient(to right, var(--primary), var(--accent));
color: white;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.3);
}
.btn-secondary {
background: white;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-secondary:hover {
background: var(--light);
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
margin: 40px 0;
}
.gallery-item {
border-radius: 15px;
overflow: hidden;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
height: 240px;
}
.gallery-item:hover {
transform: translateY(-8px);
box-shadow: 0 15px 30px rgba(67, 97, 238, 0.2);
}
.gallery-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.gallery-item:hover .image-info {
transform: translateY(0);
}
.image-title {
font-size: 1.2rem;
margin-bottom: 5px;
}
.image-desc {
font-size: 0.9rem;
opacity: 0.8;
}
.status-bar {
display: flex;
justify-content: space-between;
background: var(--light);
padding: 15px 25px;
border-radius: 12px;
margin-top: 30px;
font-size: 1.1rem;
}
footer {
text-align: center;
padding: 30px;
background: var(--light);
color: var(--gray);
font-size: 1.1rem;
}
.custom-viewer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
background: rgba(0, 0, 0, 0.9);
}
.tech-stack {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
flex-wrap: wrap;
}
.tech-item {
background: rgba(67, 97, 238, 0.1);
color: var(--primary);
padding: 8px 20px;
border-radius: 30px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 768px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<h1>Vue3 + v-viewer 高级图片预览</h1>
<p class="subtitle">实现单图预览、多图导航、缩放旋转等高级功能</p>
<div class="tech-stack">
<div class="tech-item">Vue 3</div>
<div class="tech-item">v-viewer</div>
<div class="tech-item">Viewer.js</div>
<div class="tech-item">CSS3</div>
</div>
</header>
<div class="content">
<div class="features">
<div class="feature-card">
<div class="feature-icon">🔍</div>
<h3 class="feature-title">缩放查看</h3>
<p>支持鼠标滚轮缩放图片</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔄</div>
<h3 class="feature-title">旋转翻转</h3>
<p>支持任意角度旋转和水平翻转</p>
</div>
<div class="feature-card">
<div class="feature-icon">📷</div>
<h3 class="feature-title">全屏模式</h3>
<p>支持全屏查看图片细节</p>
</div>
<div class="feature-card">
<div class="feature-icon">⬇️</div>
<h3 class="feature-title">下载图片</h3>
<p>一键下载原始图片</p>
</div>
</div>
<div class="controls">
<button class="btn btn-primary" @click="viewAll">
<span>查看全部图片</span>
</button>
<button class="btn btn-secondary" @click="shuffleImages">
<span>随机排序图片</span>
</button>
</div>
<div class="gallery">
<div
v-for="(image, index) in images"
:key="image.id"
class="gallery-item"
@click="viewImage(index)"
>
<img :src="image.thumbnail" :alt="image.title" class="gallery-img">
<div class="image-info">
<div class="image-title">{{ image.title }}</div>
<div class="image-desc">{{ image.desc }}</div>
</div>
</div>
</div>
<div class="status-bar">
<div>图片数量: {{ images.length }} 张</div>
<div>当前预览模式: {{ previewMode }}</div>
<div>最后操作: {{ lastAction || '无' }}</div>
</div>
</div>
<footer>
Vue3 + v-viewer 图片预览组件 | 支持高级图片查看功能
</footer>
</div>
<!-- v-viewer 组件 -->
<viewer :images="previewList" :options="viewerOptions" @inited="viewerInited" ref="viewerRef">
<template #default="scope">
<div v-for="(src, index) in scope.images" :key="index" style="display: none;">
<img :src="src" :data-source="src">
</div>
</template>
</viewer>
</div>
<script>
const { createApp, ref, reactive, computed, onMounted } = Vue;
const app = createApp({
setup() {
// 图片数据
const images = ref([
{
id: 1,
thumbnail: 'https://images.unsplash.com/photo-1682687220067-dced9a881b56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220067-dced9a881b56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '山间日出',
desc: '清晨的第一缕阳光穿过山谷'
},
{
id: 2,
thumbnail: 'https://images.unsplash.com/photo-1682687220208-22d7a2543e88?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220208-22d7a2543e88?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '沙漠风光',
desc: '无垠沙丘与蓝天相接'
},
{
id: 3,
thumbnail: 'https://images.unsplash.com/photo-1682687220923-471809f7e9c0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220923-471809f7e9c0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '海滩日落',
desc: '夕阳下的金色海岸线'
},
{
id: 4,
thumbnail: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '森林小径',
desc: '绿意盎然的林间小路'
},
{
id: 5,
thumbnail: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '雪山湖泊',
desc: '雪山倒映在清澈湖水中'
},
{
id: 6,
thumbnail: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=400',
full: 'https://images.unsplash.com/photo-1682687220509-61b8a906ca19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTY0NjQ1MQ&ixlib=rb-4.0.3&q=80&w=1200',
title: '城市夜景',
desc: '灯火辉煌的都市夜晚'
}
]);
// 预览状态
const previewMode = ref(null);
const lastAction = ref(null);
const viewerRef = ref(null);
// 计算预览列表
const previewList = computed(() => {
return images.value.map(img => img.full);
});
// Viewer.js 配置选项
const viewerOptions = reactive({
inline: false,
button: true,
navbar: true,
title: true,
toolbar: {
zoomIn: 1,
zoomOut: 1,
oneToOne: 1,
reset: 1,
prev: 1,
play: {
show: 1,
size: 'large',
},
next: 1,
rotateLeft: 1,
rotateRight: 1,
flipHorizontal: 1,
flipVertical: 1,
},
viewed(ev) {
lastAction.value = `查看图片: ${images.value[ev.detail.index].title}`;
},
rotate(ev) {
lastAction.value = `旋转图片: ${ev.detail.degree}°`;
},
flip(ev) {
lastAction.value = `翻转图片: ${ev.detail.direction === 'horizontal' ? '水平' : '垂直'}`;
}
});
// 查看单张图片
const viewImage = (index) => {
previewMode.value = '单图预览';
if (viewerRef.value && viewerRef.value.$viewer) {
viewerRef.value.$viewer.view(index);
}
};
// 查看所有图片
const viewAll = () => {
previewMode.value = '全图集预览';
if (viewerRef.value && viewerRef.value.$viewer) {
viewerRef.value.$viewer.show();
}
};
// 随机排序图片
const shuffleImages = () => {
images.value = [...images.value].sort(() => Math.random() - 0.5);
lastAction.value = '随机排序图片';
};
// Viewer初始化回调
const viewerInited = (viewer) => {
console.log('v-viewer 已初始化');
// 添加自定义按钮
viewer.toolbar.appendChild(createDownloadButton(viewer));
};
// 创建下载按钮
const createDownloadButton = (viewer) => {
const button = document.createElement('button');
button.className = 'viewer-download';
button.innerHTML = '⬇️';
button.title = '下载图片';
button.style.marginLeft = '10px';
button.style.fontSize = '24px';
button.style.background = 'none';
button.style.border = 'none';
button.style.cursor = 'pointer';
button.style.color = '#fff';
button.addEventListener('click', () => {
const image = viewer.image;
if (image) {
const link = document.createElement('a');
link.href = image.src;
link.download = `image-${new Date().getTime()}.jpg`;
link.click();
lastAction.value = '下载图片';
}
});
return button;
};
onMounted(() => {
console.log('图片预览组件已加载');
});
return {
images,
previewMode,
lastAction,
previewList,
viewerOptions,
viewerRef,
viewImage,
viewAll,
shuffleImages,
viewerInited
};
}
});
// 使用v-viewer插件
app.use(VViewer);
app.mount('#app');
</script>
</body>
</html>
```
## 代码解释
### 1. 安装和配置v-viewer
首先需要安装v-viewer库:
```bash
npm install v-viewer viewerjs
```
在Vue3中全局注册:
```javascript
import { createApp } from 'vue'
import App from './App.vue'
import VViewer from 'v-viewer'
import 'viewerjs/dist/viewer.css'
const app = createApp(App)
app.use(VViewer)
app.mount('#app')
```
### 2. 使用v-viewer组件
```html
<viewer
:images="previewList"
:options="viewerOptions"
@inited="viewerInited"
ref="viewerRef"
>
<template #default="scope">
<div v-for="(src, index) in scope.images" :key="index" style="display: none;">
<img :src="src" :data-source="src">
</div>
</template>
</viewer>
```
- `:images`:绑定要预览的图片列表
- `:options`:配置Viewer.js的选项
- `@inited`:Viewer初始化完成的回调
- `ref`:获取Viewer实例引用
### 3. Viewer.js配置选项
```javascript
const viewerOptions = reactive({
inline: false,
button: true,
navbar: true,
title: true,
toolbar: {
zoomIn: 1,
zoomOut: 1,
oneToOne: 1,
reset: 1,
prev: 1,
play: { show: 1, size: 'large' },
next: 1,
rotateLeft: 1,
rotateRight: 1,
flipHorizontal: 1,
flipVertical: 1,
},
viewed(ev) {
// 图片查看事件
},
rotate(ev) {
// 图片旋转事件
},
flip(ev) {
// 图片翻转事件
}
});
```
### 4. 图片预览功能实现
**单图预览**:
```javascript
const viewImage = (index) => {
previewMode.value = '单图预览';
if (viewerRef.value && viewerRef.value.$viewer) {
viewerRef.value.$viewer.view(index);
}
};
```
**全图集预览**:
```javascript
const viewAll = () => {
previewMode.value = '全图集预览';
if (viewerRef.value && viewerRef.value.$viewer) {
viewerRef.value.$viewer.show();
}
};
```
### 5. 自定义下载按钮
```javascript
const createDownloadButton = (viewer) => {
const button = document.createElement('button');
// ...按钮样式设置
button.addEventListener('click', () => {
const image = viewer.image;
if (image) {
const link = document.createElement('a');
link.href = image.src;
link.download = `image-${new Date().getTime()}.jpg`;
link.click();
}
});
return button;
};
// 在初始化回调中添加按钮
const viewerInited = (viewer) => {
viewer.toolbar.appendChild(createDownloadButton(viewer));
};
```
## 功能特点
1. **多种预览模式**:
- 单图预览:单击图片查看当前图片
- 全图集预览:从第一张开始顺序查看所有图片
2. **丰富的图片操作**:
- 缩放(放大、缩小、1:1比例)
- 旋转(左右旋转90度)
- 翻转(水平和垂直)
- 幻灯片播放
- 图片下载
3. **响应式设计**:
- 适应不同屏幕尺寸
- 移动设备友好
4. **用户体验优化**:
- 图片悬停效果
- 操作记录显示
- 平滑的动画过渡
## 技术优势
1. **v-viewer特性**:
- 基于强大的Viewer.js库
- 支持所有Viewer.js的功能和配置
- 无缝集成到Vue3应用中
2. **Vue3优势**:
- 使用Composition API组织代码
- 响应式数据管理
- 组件化开发