vue.js: viewerjs+vue3+js
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
<!-- 图片容器,用于 Viewer.js -->
<div ref="viewerContainer" style="margin: 20px;">
<img
v-for="src in images"
:key="src"
:src="getThumbnail(src)"
:data-src="src"
alt="image"
class="viewer-image"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted} from "vue";
import Viewer from "viewerjs";
import "viewerjs/dist/viewer.css";
import "@fortawesome/fontawesome-free/css/all.min.css";
// 假设这些是高清图地址
const images = [
"1.png", // 高清图
"2.jpg",
"3.jpg",
];
const viewerContainer = ref(null);
let viewer = null;
// 生成缩略图(用于页面显示)
const getThumbnail = (url) => {
return url.replace(/(\d+)\/(\d+)/, '200/150');
};
// 定义下载函数(核心!)
const handleDownload = function () {
if (viewer && viewer.image) {
const currentImage = viewer.image;
console.log("准备下载:", currentImage.src);
const tempImage = new Image();
tempImage.crossOrigin = "Anonymous"; // ✅ 启用跨域请求
tempImage.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = tempImage.width;
canvas.height = tempImage.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(tempImage, 0, 0);
// 转为 data URL(同源)
const dataUrl = canvas.toDataURL("image/jpeg", 0.95); // 可调整质量
const a = document.createElement("a");
a.href = dataUrl;
a.download = `download-${Date.now()}.jpg`; // 强制下载
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
tempImage.onerror = function () {
console.warn("Canvas 加载失败(可能是 CORS 拒绝),尝试直接下载");
// 回退:直接使用原链接(可能在新标签打开)
const a = document.createElement("a");
a.href = currentImage.src;
a.download = `image-${Date.now()}.jpg`;
a.target = "_blank";
a.rel = "noopener noreferrer";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
tempImage.src = currentImage.src;
}
};
onMounted(() => {
if (viewerContainer.value) {
viewer = new Viewer(viewerContainer.value, {
url: "data-src", // 使用 data-src 作为高清图源
toolbar: {
download: {
show: true,
size: "large",
click() {
// 调用我们定义的可靠下载函数
handleDownload();
},
},
zoomIn: true,
zoomOut: true,
oneToOne: true,
reset: true,
prev: true,
next: true,
rotateLeft: true,
rotateRight: true,
flipHorizontal: true,
flipVertical: true,
},
navbar: true,
title: true,
keyboard: true,
viewed() {
console.log("当前预览图片:", viewer.image.src);
},
});
}
});
onUnmounted(() => {
if (viewer) {
viewer.destroy();
}
});
</script>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
.viewer-image {
margin: 10px;
cursor: pointer;
max-width: 200px;
border: 1px solid #eee;
border-radius: 4px;
}
/* 下载按钮样式:使用 Font Awesome 图标 */
.viewer-toolbar .viewer-download {
display: flex !important;
align-items: center;
justify-content: center;
}
.viewer-toolbar .viewer-download::before {
font-family: "Font Awesome 5 Free" !important;
content: "\f019" !important; /* 下载图标 */
font-weight: 900;
color: #fff;
font-size: 16px;
}
</style>
没有考虑原图片类型。还有需要有名称相应下载。
以下解决:
<script>
export default {
}
</script>
<style>
</style>
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
<!-- 图片容器,用于 Viewer.js -->
<div ref="viewerContainer" style="margin: 20px;">
<img
v-for="src in images"
:key="src"
:src="getThumbnail(src)"
:data-src="src"
alt="image"
class="viewer-image"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted} from "vue";
import Viewer from "viewerjs";
import "viewerjs/dist/viewer.css";
import "@fortawesome/fontawesome-free/css/all.min.css";
// 假设这些是高清图地址
const images = [
"1.png", // 高清图
"2.jpg",
"3.jpg",
];
const viewerContainer = ref(null);
let viewer = null;
// 生成缩略图(用于页面显示)
const getThumbnail = (url) => {
return url.replace(/(\d+)\/(\d+)/, '200/150');
};
// 根据图片 URL 推断文件扩展名
function getExtensionFromUrl(url){
const match = url.match(/\.([a-zA-Z0-9]{1,5})($|\?|#)/);
if (match) {
const ext = match[1].toLowerCase();
if (ext === 'jpeg') return 'jpg';
if (['jpg', 'png', 'gif', 'webp', 'bmp'].includes(ext)) {
return ext;
}
}
return 'jpg'; // 默认
}
const handleDownload = function () {
if (viewer && viewer.image) {
const currentImage = viewer.image;
const src = currentImage.src;
console.log("准备下载:", src);
const tempImage = new Image();
tempImage.crossOrigin = "Anonymous";
tempImage.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = tempImage.width;
canvas.height = tempImage.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(tempImage, 0, 0);
// 👇 推断原始扩展名
const ext = getExtensionFromUrl(src);
const mimeType = `image/${ext === 'jpg' ? 'jpeg' : ext}`;
// 生成 data URL
const dataUrl = canvas.toDataURL(mimeType, 0.95);
// 👇 强制使用 .jpg 扩展名
const filename = `image-${Date.now()}.${ext}`;
const a = document.createElement("a");
a.href = dataUrl;
a.download = filename; // ✅ 强制 .jpg
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
tempImage.onerror = function () {
console.warn("Canvas 失败,回退到直接下载");
const ext = getExtensionFromUrl(src);
const a = document.createElement("a");
a.href = src;
a.download = `image-${Date.now()}.${ext}`;
a.target = "_blank";
a.rel = "noopener noreferrer";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
tempImage.src = src;
}
};
onMounted(() => {
if (viewerContainer.value) {
viewer = new Viewer(viewerContainer.value, {
url: "data-src", // 使用 data-src 作为高清图源
toolbar: {
download: {
show: true,
size: "large",
click() {
// 调用我们定义的可靠下载函数
handleDownload();
},
},
zoomIn: true,
zoomOut: true,
oneToOne: true,
reset: true,
prev: true,
next: true,
rotateLeft: true,
rotateRight: true,
flipHorizontal: true,
flipVertical: true,
},
navbar: true,
title: true,
keyboard: true,
viewed() {
console.log("当前预览图片:", viewer.image.src);
},
});
}
});
onUnmounted(() => {
if (viewer) {
viewer.destroy();
}
});
</script>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
.viewer-image {
margin: 10px;
cursor: pointer;
max-width: 200px;
border: 1px solid #eee;
border-radius: 4px;
}
/* 下载按钮样式:使用 Font Awesome 图标 */
.viewer-toolbar .viewer-download {
display: flex !important;
align-items: center;
justify-content: center;
}
.viewer-toolbar .viewer-download::before {
font-family: "Font Awesome 5 Free" !important;
content: "\f019" !important; /* 下载图标 */
font-weight: 900;
color: #fff;
font-size: 16px;
}
</style>
npm install v-viewer viewerjs npm install @fortawesome/fontawesome-svg-core npm install @fortawesome/free-solid-svg-icons npm install @fortawesome/vue-fontawesome@prerelease npm install @fortawesome/free-regular-svg-icons npm install @fortawesome/free-brands-svg-icons npm install @fortawesome/fontawesome-free
/*!
* Viewer.js v1.11.7
* https://fengyuanchen.github.io/viewerjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2024-11-24T04:32:14.526Z
*/
.viewer-zoom-in::before, .viewer-zoom-out::before, .viewer-one-to-one::before, .viewer-reset::before, .viewer-prev::before, .viewer-play::before, .viewer-next::before, .viewer-rotate-left::before, .viewer-rotate-right::before, .viewer-flip-horizontal::before, .viewer-flip-vertical::before, .viewer-fullscreen::before, .viewer-fullscreen-exit::before, .viewer-close::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 viewBox%3D%220 0 560 40%22%3E%3Cpath fill%3D%22%23fff%22 d%3D%22M49.6 17.9h20.2v3.9H49.6zm123.1 2 10.9-11 2.7 2.8-8.2 8.2 8.2 8.2-2.7 2.7-10.9-10.9zm94 0-10.8-11-2.7 2.8 8.1 8.2-8.1 8.2 2.7 2.7 10.8-10.9zM212 9.3l20.1 10.6L212 30.5V9.3zm161.5 4.6-7.2 6 7.2 5.9v-4h12.4v4l7.3-5.9-7.3-6v4h-12.4v-4zm40.2 12.3 5.9 7.2 5.9-7.2h-4V13.6h4l-5.9-7.3-5.9 7.3h4v12.6h-4zm35.9-16.5h6.3v2h-4.3V16h-2V9.7Zm14 0h6.2V16h-2v-4.3h-4.2v-2Zm6.2 14V30h-6.2v-2h4.2v-4.3h2Zm-14 6.3h-6.2v-6.3h2v4.4h4.3v2Zm-438 .1v-8.3H9.6v-3.9h8.2V9.7h3.9v8.2h8.1v3.9h-8.1v8.3h-3.9zM93.6 9.7h-5.8v3.9h2V30h3.8V9.7zm16.1 0h-5.8v3.9h1.9V30h3.9V9.7zm-11.9 4.1h3.9v3.9h-3.9zm0 8.2h3.9v3.9h-3.9zm244.6-11.7 7.2 5.9-7.2 6v-3.6c-5.4-.4-7.8.8-8.7 2.8-.8 1.7-1.8 4.9 2.8 8.2-6.3-2-7.5-6.9-6-11.3 1.6-4.4 8-5 11.9-4.9v-3.1Zm147.2 13.4h6.3V30h-2v-4.3h-4.3v-2zm14 6.3v-6.3h6.2v2h-4.3V30h-1.9zm6.2-14h-6.2V9.7h1.9V14h4.3v2zm-13.9 0h-6.3v-2h4.3V9.7h2V16zm33.3 12.5 8.6-8.6-8.6-8.7 1.9-1.9 8.6 8.7 8.6-8.7 1.9 1.9-8.6 8.7 8.6 8.6-1.9 2-8.6-8.7-8.6 8.7-1.9-2zM297 10.3l-7.1 5.9 7.2 6v-3.6c5.3-.4 7.7.8 8.7 2.8.8 1.7 1.7 4.9-2.9 8.2 6.3-2 7.5-6.9 6-11.3-1.6-4.4-7.9-5-11.8-4.9v-3.1Zm-157.3-.6c2.3 0 4.4.7 6 2l2.5-3 1.9 9.2h-9.3l2.6-3.1a6.2 6.2 0 0 0-9.9 5.1c0 3.4 2.8 6.3 6.2 6.3 2.8 0 5.1-1.9 6-4.4h4c-1 4.7-5 8.3-10 8.3a10 10 0 0 1-10-10.2 10 10 0 0 1 10-10.2Z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-size: 280px;
color: transparent;
display: block;
font-size: 0;
height: 20px;
line-height: 0;
width: 20px;
}
.viewer-zoom-in::before {
background-position: 0 0;
content: 'Zoom In';
}
.viewer-zoom-out::before {
background-position: -20px 0;
content: 'Zoom Out';
}
.viewer-one-to-one::before {
background-position: -40px 0;
content: 'One to One';
}
.viewer-reset::before {
background-position: -60px 0;
content: 'Reset';
}
.viewer-prev::before {
background-position: -80px 0;
content: 'Previous';
}
.viewer-play::before {
background-position: -100px 0;
content: 'Play';
}
.viewer-next::before {
background-position: -120px 0;
content: 'Next';
}
.viewer-rotate-left::before {
background-position: -140px 0;
content: 'Rotate Left';
}
.viewer-rotate-right::before {
background-position: -160px 0;
content: 'Rotate Right';
}
.viewer-flip-horizontal::before {
background-position: -180px 0;
content: 'Flip Horizontal';
}
.viewer-flip-vertical::before {
background-position: -200px 0;
content: 'Flip Vertical';
}
.viewer-fullscreen::before {
background-position: -220px 0;
content: 'Enter Full Screen';
}
.viewer-fullscreen-exit::before {
background-position: -240px 0;
content: 'Exit Full Screen';
}
.viewer-close::before {
background-position: -260px 0;
content: 'Close';
}
.viewer-container {
bottom: 0;
direction: ltr;
font-size: 0;
left: 0;
line-height: 0;
overflow: hidden;
position: absolute;
right: 0;
-webkit-tap-highlight-color: transparent;
top: 0;
-ms-touch-action: none;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.viewer-container::-moz-selection, .viewer-container *::-moz-selection {
background-color: transparent;
}
.viewer-container::selection,
.viewer-container *::selection {
background-color: transparent;
}
.viewer-container:focus {
outline: 0;
}
.viewer-container img {
display: block;
height: auto;
max-height: none !important;
max-width: none !important;
min-height: 0 !important;
min-width: 0 !important;
width: 100%;
}
.viewer-canvas {
bottom: 0;
left: 0;
overflow: hidden;
position: absolute;
right: 0;
top: 0;
}
.viewer-canvas > img {
height: auto;
margin: 15px auto;
max-width: 90% !important;
width: auto;
}
.viewer-footer {
bottom: 0;
left: 0;
overflow: hidden;
position: absolute;
right: 0;
text-align: center;
}
.viewer-navbar {
background-color: rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.viewer-list {
box-sizing: content-box;
height: 50px;
margin: 0;
overflow: hidden;
padding: 1px 0;
}
.viewer-list > li {
color: transparent;
cursor: pointer;
float: left;
font-size: 0;
height: 50px;
line-height: 0;
opacity: 0.5;
overflow: hidden;
transition: opacity 0.15s;
width: 30px;
}
.viewer-list > li:focus,
.viewer-list > li:hover {
opacity: 0.75;
}
.viewer-list > li:focus {
outline: 0;
}
.viewer-list > li + li {
margin-left: 1px;
}
.viewer-list > .viewer-loading {
position: relative;
}
.viewer-list > .viewer-loading::after {
border-width: 2px;
height: 20px;
margin-left: -10px;
margin-top: -10px;
width: 20px;
}
.viewer-list > .viewer-active,
.viewer-list > .viewer-active:focus,
.viewer-list > .viewer-active:hover {
opacity: 1;
}
.viewer-player {
background-color: #000;
bottom: 0;
cursor: none;
display: none;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 1;
}
.viewer-player > img {
left: 0;
position: absolute;
top: 0;
}
.viewer-toolbar > ul {
display: inline-block;
margin: 0 auto 5px;
overflow: hidden;
padding: 6px 3px;
}
.viewer-toolbar > ul > li {
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
float: left;
height: 24px;
overflow: hidden;
transition: background-color 0.15s;
width: 24px;
}
.viewer-toolbar > ul > li:focus,
.viewer-toolbar > ul > li:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.viewer-toolbar > ul > li:focus {
box-shadow: 0 0 3px #fff;
outline: 0;
position: relative;
z-index: 1;
}
.viewer-toolbar > ul > li::before {
margin: 2px;
}
.viewer-toolbar > ul > li + li {
margin-left: 1px;
}
.viewer-toolbar > ul > .viewer-small {
height: 18px;
margin-bottom: 3px;
margin-top: 3px;
width: 18px;
}
.viewer-toolbar > ul > .viewer-small::before {
margin: -1px;
}
.viewer-toolbar > ul > .viewer-large {
height: 30px;
margin-bottom: -3px;
margin-top: -3px;
width: 30px;
}
.viewer-toolbar > ul > .viewer-large::before {
margin: 5px;
}
.viewer-tooltip {
background-color: rgba(0, 0, 0, 0.8);
border-radius: 10px;
color: #fff;
display: none;
font-size: 12px;
height: 20px;
left: 50%;
line-height: 20px;
margin-left: -25px;
margin-top: -10px;
position: absolute;
text-align: center;
top: 50%;
width: 50px;
}
.viewer-title {
color: #ccc;
display: inline-block;
font-size: 12px;
line-height: 1.2;
margin: 5px 5%;
max-width: 90%;
min-height: 14px;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.15s;
white-space: nowrap;
}
.viewer-title:hover {
opacity: 1;
}
.viewer-button {
-webkit-app-region: no-drag;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
height: 80px;
overflow: hidden;
position: absolute;
right: -40px;
top: -40px;
transition: background-color 0.15s;
width: 80px;
}
.viewer-button:focus,
.viewer-button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.viewer-button:focus {
box-shadow: 0 0 3px #fff;
outline: 0;
}
.viewer-button::before {
bottom: 15px;
left: 15px;
position: absolute;
}
.viewer-fixed {
position: fixed;
}
.viewer-open {
overflow: hidden;
}
.viewer-show {
display: block;
}
.viewer-hide {
display: none;
}
.viewer-backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.viewer-invisible {
visibility: hidden;
}
.viewer-move {
cursor: move;
cursor: grab;
}
.viewer-fade {
opacity: 0;
}
.viewer-in {
opacity: 1;
}
.viewer-transition {
transition: all 0.3s;
}
@keyframes viewer-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.viewer-loading::after {
animation: viewer-spinner 1s linear infinite;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
content: '';
display: inline-block;
height: 40px;
left: 50%;
margin-left: -20px;
margin-top: -20px;
position: absolute;
top: 50%;
width: 40px;
z-index: 1;
}
/* 确保下载按钮显示 */
.viewer-toolbar .viewer-download {
display: flex !important;
align-items: center;
justify-content: center;
color: white !important;
font-family: 'FontAwesome' !important;
}
/* 下载按钮图标样式 */
.viewer-download .fa-download {
font-size: 16px;
color: white !important;
}
.viewer-toolbar .viewer-download::before {
content: "\f019"; /* 下载图标 */
font-family: "FontAwesome", sans-serif !important;
font-size: 14px;
color: white;
display: inline-block;
width: 100%;
height: 100%;
text-align: center;
line-height: 30px; /* 匹配 height: 30px */
speak: never; /* 防止屏幕阅读器读出 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (max-width: 767px) {
.viewer-hide-xs-down {
display: none;
}
}
@media (max-width: 991px) {
.viewer-hide-sm-down {
display: none;
}
}
@media (max-width: 1199px) {
.viewer-hide-md-down {
display: none;
}
}

const mimeMap = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
};
function getMimeTypeAndExt(url) {
const match = url.match(/\.([a-zA-Z0-9]{1,5})($|\?|#)/);
let ext = match ? match[1].toLowerCase() : 'jpg';
if (ext === 'jpeg') ext = 'jpg'; // 统一为 jpg
const mimeType = mimeMap[ext] || 'image/jpeg';
return { ext, mimeType };
}
const { ext, mimeType } = getMimeTypeAndExt(currentImage.src);
const dataUrl = canvas.toDataURL(mimeType, 0.95);
a.download = `photo.${ext}`; // 始终是 .jpg / .png / .gif
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)
浙公网安备 33010602011771号