Vue实现图片预览,侧边栏懒加载,不用任何插件,简单好用

实现样式

需求

实现PDF上传预览,并且不能下载

第一次实现:用vue-pdf,将上传的文件用base64传给前端展示

问题:

  1. 水印第一次加载有后面又没有了。
  2. 当上传大的pdf文件后,前端获取和渲染又长又慢,甚至不能用

修改实现模式

  1. 前端上传PDF,后端将PDF转化成一页一页的图片
  2. 前端根据page去获取一页一页的PDF图片,类似于百度文库

实现思路

配合后端实现思路

  1. 获取全部页数,先把侧边栏的元素画出来占个位置
  2. 获取已经看到的页数,没有默认1
  3. 渲染上次看到的页数,同时侧边栏滚动到相同的index位置,通过监听元素是否进入视口去获取base64图片
  4. 已经获取回来的图片不再去请求

主要重点难点是侧边栏懒加载定位等比例展示图片

html 复制代码
 <div class="pdf-viewer">
   <div class="pdf-main">
     <canvas id="pdf-view"></canvas>
   </div>
   <div class="pdf-list" :class="{ collapse: collapse }">
     <div
       class="pdf-item"
       :class="{ active: currentPage === index }"
       v-for="index in pageTotalNum"
       :key="index"
       @click="changePage(index)"
       :data-index="index"
     >
       <img :src="imgList[index - 1]" alt="" />
     </div>
   </div>
 </div>

<script>
let observer = null;
export default {
  name: "PDFView",
  data() {
    return {
      currentPage: 1, //当前页数
      pageTotalNum: 1, //总页数
      imgList: [], //base64图片列表
      updateTimer: null
    };
  },
  watch: {
    /**
     * @description 监听当前页变化 滚动列表到顶部
     */
    currentPage() {
      this.$nextTick(() => {
        const activeEl = document.querySelector(".pdf-list .active");
        if (activeEl) {
          document.querySelector(".pdf-list").scrollTo({
            top: activeEl.offsetTop - 20,
            behavior: "smooth",
          });
          // 解决进来会请求当前页数 前面所有图片
          setTimeout(() => {
            if (observer) {
              observer.disconnect();
            }
            this.isEnter();
          }, 500);
        }
        // 切换页面 将查看区域滚动到最上面
        const mainEl = document.querySelector(".pdf-main");
        mainEl.scrollTo({
          top: 0,
        });
      });
    },
  },
  mounted() {
    this.getPageTotal();
  },
  beforeDestroy() {
    if (observer) {
      observer.disconnect();
    }
  },
  methods: {
    /**
     * @description 获取pdf总页数
     */
    getPageTotal() {
      const params = {
        id: this.$route.query.id,
      };
      apiGetViewPdfPageTotal(params).then((response) => {
        this.pageTotalNum = response.data;
        this.updateStudy(true);
      });
    },
    /**
     * @description 切换当前页
     */
    changePage(index) {
      this.currentPage = index;
      this.updateStudy();
      if (this.imgList[index - 1]) {
        this.drawImage(this.imgList[index - 1]);
      } else {
        this.getPdf();
      }
    },
    /**
     * @description 上一页
     */
    prePage() {
      let page = this.currentPage;
      if (page !== 1) {
        page = page > 1 ? page - 1 : this.pageTotalNum;
        this.currentPage = page;
        this.updateStudy();
        if (this.imgList[page - 1]) {
          this.drawImage(this.imgList[page - 1]);
        } else {
          this.getPdf();
        }
      }
    },
    /**
     * @description 下一页
     */
    nextPage() {
      let page = this.currentPage;
      if (page !== this.pageTotalNum) {
        page = page < this.pageTotalNum ? page + 1 : 1;
        this.currentPage = page;
        this.updateStudy();
        if (this.imgList[page - 1]) {
          this.drawImage(this.imgList[page - 1]);
        } else {
          this.getPdf();
        }
      }
    },
    /**
     * @description 更新学习 flag=true第一次进入
     */
    updateStudy(flag = false) {
      const params = {
        courseId: this.$route.query.id,
        pageRate: this.currentPage,
        flag,
        totalPageRate: this.pageTotalNum,
      };
      apiUpdateStudy(params)
        .then((response) => {
          this.currentPage = response.data.pageRate;
          if (flag) {
            this.updateTimer = setInterval(() => {
              this.updateStudy();
            }, 1000 * 10);
          }
          if (flag) {
            this.getPdf();
            // 解决第一页进来不请求的问题,一页大概能展示4-5张
            if (this.currentPage < 5) {
              this.isEnter();
            }
          }
        })
    },
    /**
     * @description 查看资料
     */
    getPdf() {
      const params = {
        id: this.$route.query.id,
        page: this.currentPage,
      };
      apiGetPdf(params).then((response) => {
        let base64 = "data:image/png;base64," + response.data;
        this.drawImage(base64);
      });
    },
    /**
     * @description 将base64图片 画到canvas上
     */
    drawImage(base64) {
      const canvas = document.getElementById("pdf-view");
      const context = canvas.getContext("2d");
      const image = new Image();
      image.src = base64;
      image.onload = () => {
        const proportion = image.width / image.height;
        // 获取style设置width:100% 的canvas宽度
        const canvasWidth = canvas.offsetWidth;
        // 图片宽度与canvas宽度比例
        const canvasWidthProportion = image.width / canvasWidth;
        // canvas宽度设置为宽度
        canvas.width = image.width;
        // 根据图片比例和宽度比例计算出canvas高度
        canvas.height = (canvasWidth / proportion) * canvasWidthProportion;
        context.drawImage(image, 0, 0);
      };
    },
    /**
     * @description 监听元素进入视口
     */
    isEnter() {
      observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          const target = entry.target;
          const index = target.dataset.index;
          if (entry.isIntersecting) {
            if (!this.imgList[index - 1]) {
              this.getImgList(index);
            }
          } else {
            // console.log("元素离开视口", index);
          }
        });
      });
      this.$nextTick(() => {
      //将所有侧边栏的元素进行监听
        const els = document.querySelectorAll(".pdf-item");
        Array.from(els).forEach((el) => {
          observer.observe(el);
        });
      });
    },
    /**
     * @description 滚动获取图片
     */
    getImgList(index) {
      const params = {
        id: this.$route.query.id,
        page: index,
      };
      apiGetPdf(params).then((response) => {
        let base64 = "data:image/png;base64," + response.data;
        this.imgList[index - 1] = base64;
        // 解决请求回来页面没更新的问题
        this.$forceUpdate();
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.pdf-container {
  width: 100%;
  height: 100%;
  color: #999;
}
.pdf-viewer {
  width: 100%;
  height: calc(100vh - 50px - 30px - 60px - 6px);
  position: relative;
  display: flex;
}
.pdf-list {
  width: 240px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: 20px;
  background: #000;
  box-sizing: border-box;
  // transition: all 0.3s ease-in-out;
  border-left: 1px solid #999;
  &::-webkit-scrollbar {
    width: 0px;
  }
  .pdf-item {
    height: 183px;
    min-height: 183px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    overflow: hidden;
    &:hover {
      ::v-deep img {
        transition: all 0.5s ease-in-out;
        transform: scale(1.1);
      }
    }
    &.active {
      box-shadow: 0px 0px 0px 4px #e6a23c;
    }
    &:not(:last-child) {
      margin-bottom: 10px;
    }
    img {
      pointer-events: none;
      width: 100%;
      // height: 100%;
    }
  }
  &.collapse {
    width: 0;
    padding: 0;
  }
}
.pdf-main {
  flex: 1;
  // width: 100%;
  // height: 100%;
  overflow-y: auto;
  background: #000;
  position: relative;
  padding: 10px 0;
  &::-webkit-scrollbar {
    width: 0px;
  }
}
.handle-btn {
  background: #000;
  display: flex;
  font-size: 12px;
  position: relative;
  height: 60px;
  padding: 0 6px;
  border-bottom: 1px solid #999;
  .right {
    width: 240px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    font-size: 32px;
  }
  .main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32px;
    margin-left: 250px;
    .pagination {
      display: flex;
      align-items: center;
      margin: 0 10px;
      .pagination-info {
        font-size: 14px;
        margin: 0 8px;
      }
    }
    .zoom {
      display: flex;
      align-items: center;
      margin: 0 10px;
      .scale {
        font-size: 14px;
        margin: 0 8px;
      }
    }
  }
  .tips {
    color: #e6a23c;
    font-size: 12px;
  }
  .start-test {
    display: flex;
    align-items: center;
  }
  .time {
    position: absolute;
    left: 6px;
    top: 50%;
    transform: translateY(-50%);
    > span {
      display: inline-block;
      margin-left: 10px;
    }
  }
}
i {
  cursor: pointer;
  &:hover {
    color: #fff;
  }
}
#pdf-view {
  width: 100%;
  // height: 100%;
  padding: 10px;
}
</style>
相关推荐
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税6 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore