手把手实现vue图片预览组件(二)

465 阅读1分钟

之前写过一篇文章讲图片预览。(juejin.cn/post/702209… 图片预览的实现我们已经通过canvas画图+计算长宽比实现预览,但是总体而言稍显复杂,而且里面有一些小坑。如果我们不通过canvas,是否有更好的决绝解决方式? 答案是有的。

在这个图片预览的组件中,我们花了很多的精力去得到预览图片的真实宽高。保持它不变形。这也是图片预览这个功能里最核心的要点。而这次我们还有另外一个方法可以不费吹灰之力来完成这件事。那就是借助css的maxHeight属性。

max-height 这个属性会阻止 height 属性的设置值变得比 max-height 更大。
max-height 属性用来设置给定元素的最大高度. 如果height 属性设置的高度比该属性设置的高度还大,则height 属性会失效. 借助这个特征,我们先来实现一个最简单的预览效果:

<template>
  <div class="">
   <View 
      :url="url2"
      v-if="show"
      @close="show=false">
    </View>
    <el-button @click="show=true">预览</el-button>
  </div>
 </template>
 import View from './view.vue'
 export default {
     components: {  View},
     data() {
        return {
          show: false,     
          url:"http://121.5.169.60:8081/imageOnlineManager/images/4d41ffd0a1984e818574d8c79ed24282.jpg",//宽图片
          }
      }
 }
 //接下来是我们的预览组件的核心代码
 view.vue
 -------------------------
 <template>
  <div class="viewer">
    <div
      class="mask"
      @click="$emit('close')"
    >
    </div>
    <div class="img-wrap">
      <i
        class="el-icon-loading loading"
        v-if="loading"
      ></i>
      <img
        ref="img"
        class="viewer__img"
        :key="url"
        :src="currentImg"
        @load="handleImgLoad"
        @error="handleImgError"
      />
    </div>
  </div>
</template>
export default {
  name: 'view5',
  props: {
    url: {
      type: String,
      default: ""
    }
  },
  computed: {
    currentImg() {
      return this.url
    },
  },
  watch: {
    currentImg(val) {
      this.$nextTick(_ => {
        const $img = this.$refs.img;
        if (!$img.complete) {
          this.loading = true;
        }
      });
    }
  },
  mounted() {

  },
  data() {
    return {
      loading: false,
      isShow: false,
    }
  },
  methods: {
    handleImgLoad(e) {
      this.loading = false;
    },
    handleImgError(e) {
      this.loading = false;
      e.target.alt = '加载失败';
    },
    loadImg() {
      let imgObj = new Image()
      imgObj.onload = function (e) {
      }
      imgObj.src = this.url
    },
    },
    

    
  }
}
</script>
<style scoped >
.mask {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  opacity: 0.5;
  background: #000;
}
.viewer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.img-wrap {
  width: 100%;
  height: 100%;

  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
}
.loading {
  color: #fff;
}
.actions {
  position: fixed;
  top: 16px;
  right: 30px;
  width: 220px;
  z-index: 9999;
  color: #fff;
  display: flex;
  justify-content: flex-end;
}
.actions i {
  display: inline-block;
  margin-left: 10px;
}
.viewer__img{
    max-height: 100%;
}
//特别注意:这里我们给viewer__img设置了一个maxHeight:100%

就是这个简单的属性,此时我们点击预览按钮。图片将会以正常的宽高比显示出来

1.png 不论你是长图还是宽图都正常显示。 到此,我们的工作已经完成了一半。 完整代码如下:

<template>
  <div class="viewer">
    <div
      class="mask"
      @click="$emit('close')"
    ></div>
    <div class="actions">
        <i
          class="el-icon-zoom-in fs14"
          @click="operate('zoomIn')"
        ></i>
        <i
          class="el-icon-zoom-out fs14"
          @click="operate('zoomOut')"
        ></i>
        <i class="el-icon-c-scale-to-original fs14"
          @click="toggleMode"></i>
        <i class="el-icon-refresh-left fs14"
          @click="operate('rotateDY')"
        ></i>
        <i
          class="el-icon-refresh-right fs14"
          @click="operate('rotateY')"
        ></i>
        <i
          class="el-icon-circle-close fs14"
          @click="operate('close')"
        ></i>
      </div>
    <div class="img-wrap">
      <i
        class="el-icon-loading loading"
        v-if="loading"
      ></i>
      <img
        ref="img"
        class="viewer__img"
        :key="url"
        :src="currentImg"
        :style="imgStyle"
        @load="handleImgLoad"
        @error="handleImgError"
      />
    </div>
  </div>
</template>

<script>
const Mode = {
  CONTAIN: {
    name: 'contain',
    icon: 'el-icon-full-screen'
  },
  ORIGINAL: {
    name: 'original',
    icon: 'el-icon-c-scale-to-original'
  }
};
export default {
  name: 'HelloWorld',
  props: {
    url: {
      type: String,
      default: ""
    }
  },
  computed: {
    currentImg() {
      return this.url
    },
    imgStyle() {
      const { scale, deg, offsetX, offsetY, enableTransition } = this.transform;
      const style = {
        transform: `scale(${scale}) rotate(${deg}deg)`,
        transition: enableTransition ? 'transform .3s' : '',
        'margin-left': `${offsetX}px`,
        'margin-top': `${offsetY}px`,
      };
      if (this.mode === Mode.CONTAIN) {
        // style.maxWidth = style.maxHeight = '100%';
        style.maxWidth = style.maxHeight = '100%';
      }
      return style;
    },
  },
  watch: {
    currentImg(val) {
      this.$nextTick(_ => {
        const $img = this.$refs.img;
        if (!$img.complete) {
          this.loading = true;
        }
      });
    }
  },
  mounted() {
  },
  data() {
    return {
      loading: false,
      isShow: false,
      mode: Mode.CONTAIN,
      transform: {
        scale: 1,
        deg: 0,
        offsetX: 0,
        offsetY: 0,
        enableTransition: false
      },
    }
  },
  methods: {
    handleImgLoad(e) {
      this.loading = false;
    },
    handleImgError(e) {
      this.loading = false;
      e.target.alt = '加载失败';
    },
    loadImg() {
      let imgObj = new Image()
      imgObj.onload = function (e) {
      }
      imgObj.src = this.url
    },
    toggleMode() {
       if (this.loading) return;
      const modeNames = Object.keys(Mode);
      const modeValues = Object.values(Mode);
      const index = modeValues.indexOf(this.mode);
      const nextIndex = (index + 1) % modeNames.length;
      this.mode = Mode[modeNames[nextIndex]];
      console.log("this.mode:'",this.mode)

      this.reset();
    },
    operate(action,options={}){ 
       if (this.loading) return;
      const { zoomRate, rotateDeg, enableTransition } = {
        zoomRate: 0.2,
        rotateDeg: 90,
        enableTransition: true,
        ...options
      };
      const { transform } = this;
      this.transform.enableTransition=true;
      switch (action) {
        case 'zoomOut':
          if (transform.scale > 0.2) {
            transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3));
          }
          break;
        case 'zoomIn':
          transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3));
          break;
        case 'rotateY':
          transform.deg += rotateDeg;
          break;
        case 'rotateDY':
          transform.deg -= rotateDeg;
          break;
        case "close":
            this.$emit("close");
          break;
      }
      transform.enableTransition = enableTransition;  
    },
    reset() {
      this.transform = {
        scale: 1,
        deg: 0,
        offsetX: 0,
        offsetY: 0,
        enableTransition: false
      };
    }
  }
}
</script>
<style scoped >
.mask {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  opacity: 0.5;
  background: #000;
}
.viewer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.img-wrap {
  width: 100%;
  height: 100%;

  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
}
.loading {
  color: #fff;
}
.actions {
  position: fixed;
  top: 16px;
  right: 30px;
  width: 220px;
  z-index: 9999;
  color: #fff;
  display: flex;
  justify-content: flex-end;
}
.actions i {
  display: inline-block;
  margin-left: 10px;
}
</style>
如果看过element-ui的源码。没错。这就是它的实现思路。不得不说,实在高明。巧用css,可以事半功倍,在这个例子的基础上,我们可以在预览的时候给图片添加水印效果进行进一步拓展