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;
  }
}

  

 

 

09d1597e-82db-430a-a982-73b08cfd70cc

 

 

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

  

 

posted @ 2025-10-30 23:13  ®Geovin Du Dream Park™  阅读(5)  评论(0)    收藏  举报