motivation

Need to add new functionality in recent project background, in a nutshell is a take picture comment response system, look for a similar system, decided to imitate the nuggets boiling point design, concise and elegant, the basic module interface and the boiling point is same, only a few functions (link and subject function doesn’t do)

The whole system is relatively complex, including picture text upload component, emoji component, level 1 comment, level 2 reply, and level 2 reply to display pictures, like component, picture display component and so on. In fact, the main background is more complex, how to effectively design the database table structure and all kinds of add and delete comments. This paper mainly introduces the idea and code writing logic of the picture display module, as shown in the picture below. The picture display interface (thumbnail) in news is shown in the picture below. When users upload pictures when Posting new news, all pictures will be displayed in the news component below.

I feel that this picture shows a lot of logic and complexity of components, and I can’t find other people’s wheels, so it is worth taking them out for analysis. The following analysis will only give key codes, not all codes, mainly too much and too complex, so you can make one by yourself if you understand the principle

Component function analysis

First received the task in the boiling point of the first step is to take a closer look at the Denver nuggets the manifestation of pictures show components and logic (try) to the boiling point module, this step requires a lot of testing to dig all of logic, the following list out some of the component performance (1) if there is only a figure, is displayed as a larger version, the following figure

(2) If there are more than one picture (9 at most), the picture will be displayed as thumbnail, and the picture will be arranged differently with different number of pictures

(3) When the aspect ratio of a picture exceeds a certain threshold, the long picture label will be displayed in the lower right corner of the picture

(4) Click any thumbnail to switch to the display interface of detail picture

(54) Click the view big picture button in the picture above to enter the full-screen view big picture component

Then click the screen to switch to the display interface of the original ruler of the long picture (scroll bar appears on the right), as shown below

Component structure abstraction

To name the entire component (the new image display component), all you need is a prop array containing the URL of each image

After the above functional analysis, it is found that the full screen view module can be abstracted into a single component, which is a child of the above component

<! --> <full-screen-viewer :imageList="imageList"
  :currentImageIndex="currentImageIndex"
  @close="handleFullScreenViewerClose"
  v-if="isShowFullScreenViewer">
</full-screen-viewer>
Copy the code

Two props are required, first the array of images to display and then the index of the currently displayed image, passed in by its parent

Here is the overall structure of

<template>
  <div class="wrapper" ref="wrapper"> <! Thumbnail div--> <div class="brief-view-wrapper" v-show=! "" isShowDetail" ref="outerWrapper"> <! --> <template v-if="isSingleImage">
        <div class="single-img-container"
             @click="showDetailImage(imageList[0])"
             :style="{backgroundImage:'url('+imageList[0]+')'}"> <! Div class= <div class="ratio-holder" :style="{paddingTop: calcSingleImgHeight}">
          </div>
          <span class="long-image" v-if="isLongImage"< span> </div> </template> <! <template v-else> <! The image is placed in the backgroundImage property --> <div class="multiple-img-wrapper" :class="[colsOfMultipleImages]">
          <div class="multiple-img-container"
               v-for="(item,index) in imageList"
               @click="showDetailImage(item)"
               :style="{backgroundImage:'url('+item+')'}"> <! Padding-top controls the height and width of the image as well as the width of the parent element --> <div class="ratio-holder" style="padding-top: 100%">
            </div>
            <span class="long-image" v-show="isLongImageList[index]"</span> </div> </div> </template> </div> <! > <div class="detail-view-wrapper" v-show="isShowDetail" ref="detailViewWrapper"> <! Top action bar --> <div class="top-panel">
        <div class="panel-item" @click="hideDetailImage">
          <i class="iconfont icon-zoomout icon-pos"</span> </div> <div class= </span"panel-item" @click="showFullScreenViewer">
          <i class="iconfont icon-zoomin icon-pos"></ span> <div >< div class="panel-item" @click="handleImageRotate(-1)"> <! --inline-block rotation, inline-block rotation --> < I class="iconfont icon-reload icon-pos"
             style="transform: rotateY(-180deg); display:inline-block;"> </ I > <span> Rotate left </span> </div> <div class="panel-item" @click="handleImageRotate(1)">
          <i class="iconfont icon-reload icon-pos"></ I >< span> Rotate to the right </span> </div> </div> <! -- middle image display bar, note need to set height, because inside img is absolute position --> <div class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}"> <! -- loaded logo--> <div class="detail-img-loading">
          <circle-loading fillColor="#9C9C9C" v-if=! "" isDetailImageLoaded">
          </circle-loading>
        </div>
        <img src=""
             ref="detailImage"
             v-show="isDetailImageLoaded"
             :style="detailImageStyle"
             class="detail-img"> <! -- Click on the hidden detail diagram div--> <div class="toggle-zoomout" @click="hideDetailImage"> </div> <! Div --> <div class="prev-img" @click="switchImage(-1)" v-if="currentImageIndex > 0">
        </div>
        <div class="next-img" @click="switchImage(1)" v-if="currentImageIndex<imageList.length-1"> </div> <! --> <full-screen-viewer :imageList="imageList"
          :currentImageIndex="currentImageIndex"
          @close="handleFullScreenViewerClose"
          v-if="isShowFullScreenViewer"> </full-screen-viewer> </div> <! Thumbnail display bar --> <div class="small-img-wrapper">
        <div class="small-img-container"
             @click="switchSmallImage(index)"
             :class="{'small-img-active':currentImageIndex === index}"
             :style="{backgroundImage:'url('+item+')'}"
             v-for="(item,index) in imageList">
        </div>
      </div>
    </div>
  </div>
</template>
Copy the code

Doesn’t it feel very complicated? In fact, the structure was very simple at the beginning, but gradually with the code, it became like this. The main structure diagram is as follows

isShowDetail

Thumbnail part analysis

Next, analyze the thumbnail part first. Thumbnail is the appearance of the thumbnail of the picture mentioned above. From the previous analysis, we know that a single picture will be displayed as a large picture, and multiple pictures will be displayed as a small picture, with a maximum of 9 pictures. The overall structure is as follows

isSingleImage

IsSingleImage:function() {return! (this.imageList.length>1) },Copy the code

The logic seems simple, but in fact the rules are more complex. Here are the rules for displaying a single graph that I have tested: (1) First judge whether the picture should be displayed as vertical or horizontal, according to the ratio of the length to width of the original picture to decide (2) All pictures have a certain width, only the height changes (3) The height/width exceeds a certain value (1.8) Display the long picture label, the height of the picture displayed is 1.45 times of the width. (4) If the height/width is less than a certain value (0.68), the height of the outer frame of the thumbnail is 0.68 times of the width and is displayed in the center; otherwise, the thumbnail is displayed in proportion

These rules are necessary. For example, if you give a graph with a long vertical direction, it will appear as follows

Below is the display logic code for a single graph

<! --> <template v-if="isSingleImage">
    <div class="single-img-container"
         @click="showDetailImage(imageList[0])"
         :style="{backgroundImage:'url('+imageList[0]+')'}"> <! Div class= <div class="ratio-holder" :style="{paddingTop: calcSingleImgHeight}">
      </div>
      <span class="long-image" v-if="isLongImage"</span> </div> </template>Copy the code

The image display is using the backgroundImage attribute instead of the image tag. It feels simpler, and it needs to set background-size:cover and background-position:50% to ensure that the image is centered and the div is full of images without white space. Note that the single-image-container class only has a fixed width of 200px, and its height is split by the div inside. The padding-top is dynamically set for the ratio holder class. The padding-top percentage value is the percentage based on the width of the parent element, so the height can be calculated if the width is certain. The function to calculate the height of div is given below

// calcSingleImgHeight:function() {let self = this;
    letimage = new Image(); // If the image is larger, the size image.onload = cannot be obtained until the image is loadedfunction(){
      self.singleImageNaturalWidth = image.naturalWidth;
      self.singleImageNaturalHeight = image.naturalHeight;
    };
    image.src = this.imageList[0];
    letratio = this.singleImageNaturalWidth? this.singleImageNaturalHeight / this.singleImageNaturalWidth : 1;if(ratio < this.imageMinHeightRatio){
      ratio = this.imageMinHeightRatio
    }
    if(ratio > this.imagemaxheightratio){// The image is a long image this.islongImage =true;
      ratio = this.imageMaxHeightRatio
    }
      return ratio*100+The '%';
},
Copy the code

NaturalWidth and naturalHeight are the original width and height of the image, but cannot be retrieved until the image is loaded. Otherwise, the value is 0. Since this is a calculated property, the onLoad method will recalculate the image scale. Below, analyze the thumbnail display in the case of multiple images

<! <template v-else> <! The image is placed in the backgroundImage property --> <div class="multiple-img-wrapper" :class="[colsOfMultipleImages]">
      <div class="multiple-img-container"
           v-for="(item,index) in imageList"
           @click="showDetailImage(item)"
           :style="{backgroundImage:'url('+item+')'}"> <! Padding-top controls the height and width of the image as well as the width of the parent element --> <div class="ratio-holder" style="padding-top: 100%">
        </div>
        <span class="long-image" v-show="isLongImageList[index]"</span> </div> </div> </template>Copy the code

Col-4, 5,6, 7,8, and 9 are col-3. Col-n represents a v-for for traversing the array of images under column n. The width and height of the images are the same, so the padding-top is 100%. Note that the logic of how many images are arranged at the same time is to calculate the class name based on the number of images by colsOfMultipleImages

ColsOfMultipleImages = colsOfMultipleImagesfunction() {let len = this.imageList.length;
  let map = {
      	1:'col-4', 2:'col-4', 3:'col-4', 4:'col-4',
        5:'col-3', 6:'col-3', 7:'col-4', 8:'col-4',
        9:'col-3'};
  return len===0?' ':map[len]
},
Copy the code

In fact, there are only two classes that control their width to ensure that images are timed and wrapped

.col-3{
  width:75%
}
.col-4{
  width:100%;
}
Copy the code

Detailed graph analysis

The detail diagram mainly involves image rotation, which is a bit more troublesome

(2) If the width of the picture does not exceed the width of the outer div, the picture will be displayed in its original size and horizontally centered. (3) By default, the width of the loading diagram is fixed, and the width is the width of the outer div. Slightly smaller height Note that if you load a very small image, it will still display at the same scale, which is what the nuggets did, as shown below

The image rotation function is rotated by the transform rotate of CSS. The image is rotated visually but not in essence. If the image is rotated in essence, it needs to be redrawn on canvas. Rotate (90deg) :rotate(90deg) Transform :rotate(90deg) In fact, this rotation will only cause the image to be rotated in the original position (transform-Origin defaults to center, the center of the image), and the width and height of the image will not change, you can imagine the situation after the next long image is rotated horizontally, obviously there is a problem. So the logic here needs to dynamically calculate the width and height of the image and the width and height of the outer div. The result is shown in the following dynamic diagram

As can be seen from the above figure, this rotation is actually a change in the width and height of the image, not a simple CSS rotation, slowly analyzed below, the HTML structure is as follows

<! -- middle image display bar, note need to set height, because inside img is absolute position --> <div class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}"> <! -- loaded logo--> <div class="detail-img-loading">
      <circle-loading fillColor="#9C9C9C" v-if=! "" isDetailImageLoaded"> </circle-loading> </div> <! --> <img SRC =""
         ref="detailImage"
         v-show="isDetailImageLoaded"
         :style="detailImageStyle"
         class="detail-img"> <! -- Click on the hidden detail diagram div--> <div class="toggle-zoomout" @click="hideDetailImage"> </div> <! Div --> <div class="prev-img" @click="switchImage(-1)" v-if="currentImageIndex > 0">
    </div>
    <div class="next-img" @click="switchImage(1)" v-if="currentImageIndex<imageList.length-1"> </div> <! --> <full-screen-viewer :imageList="imageList"
      :currentImageIndex="currentImageIndex"
      @close="handleFullScreenViewerClose"
      v-if="isShowFullScreenViewer">
    </full-screen-viewer>
</div>
Copy the code

The tag in the code above is the image to display. Since the image needs to be loaded, set a circe-loading component to display the loading effect. Execute the following function when clicking the thumbnail. The rest of the divs are absolutely positioned. Notice that the outermost div is dynamically bound to a height. This is to change the height of the outer div according to the height of the image, otherwise the image will overflow the div

ShowDetailImage:function(imgUrl){
  	letself = this; CurrentImageIndex = this.imagelist.indexof (imgUrl); // Set index this.currentimageIndex = this.imagelist.indexof (imgUrl); // Change the state to this.isDetailImageLoaded =false; // Calculate the original size of the large imagelet image = this.$refs.detailImage;
        image.onload = function(){
          self.isDetailImageLoaded = true;
          self.detailImageNaturalWidth = image.naturalWidth;
          self.detailImageNaturalHeight = image.naturalHeight;
         };
        image.src = imgUrl;
        this.isShowDetail = true;
  },
Copy the code

The main thing to do here is to record the original width and height of the current image after the image is loaded for subsequent rotation. To analyze the image rotation process, first notice that the IMG tag adds a detail-img class. This class has the following contents

.detail-img{
    position: absolute;
    left:50%;
    top:0;
    transform-origin: top left;
}
Copy the code

The CSS above shows that the image is absolutely positioned, takes up no space in the document, and moves 50% to the right, with the base point of the transformation set in the upper left corner of the image. The resulting image should look like the following image

At this point, the image is already rotated in the right direction, but a portion of the image is still outside the div, so a rotation is not enough. Each rotation must be accompanied by a translation operation that brings the image back inside the div. Need to translate after rotation (0, 50%), which does not handle in the x direction, y direction movement – 50% range, easy to get here, the pictures looked at is need horizontal displacement, the results of that in fact this is rotated so the horizontal displacement is rotating in front of the vertical displacement, So translateY is 50%, the spin to the right to continue translateX and translateY value change, can draw an array detailImageTranslateArray based on reasoning, The following array is rotated clockwise from left to right. The first value of each term is the value of translateX and the second value is the value of translateY

detailImageTranslateArray:[['50%'.'0'], ['0'.'50%'], ['50%'.'100%'], ['100%'.'50%']],
Copy the code

The initial state of the image is the first value of the above array [-50%,0], so we can get the value of translateX and Y in the next state after the image rotation according to the value of the current translateX and Y, and then bind the value to the style of the image to complete the rotation

Click the left or right button to trigger the following function, which handles the image rotation logic

// Handle image rotation handleImageRotate:function(dir){// Images can be rotated only after loadingif(! this.isDetailImageLoaded)returnTransform-origin :top leftletangleDelta = dir === 1? : 90-90; This.detailrotateangel = (this.detailRotateAngel + angleDelta)%360; // Fix the translate valueletcurrentIndex; This. DetailImageTranslateArray. ForEach ((item, index) = > {/ / find the current tranlate valuesif(item[0]===this.detailImageTranslateX && item[1]===this.detailImageTranslateY){ currentIndex = index; }}); // Take the next valuelet nextIndex = currentIndex+dir;
        if(nextIndex === this.detailImageTranslateArray.length){
          nextIndex = 0;
        }else if(nextIndex === -1){ nextIndex = this.detailImageTranslateArray.length - 1; } / / update tranlate the value of the enclosing detailImageTranslateX = this. DetailImageTranslateArray [nextIndex] [0]; this.detailImageTranslateY = this.detailImageTranslateArray[nextIndex][1]; / / modify the height of the outer div enclosing processImageScaleInRotate (); },Copy the code

The direction of rotation is determined by the dir parameter passed in, the Angle of rotation is calculated, and the value of translate is calculated. Therefore, the rotation of the image is determined by three values in data: DetailRotateAngel, detailImageTranslateX, detailImageTranslateY respectively represent rotation Angle, displacement in x direction, and displacement in Y direction. Finally, bind them to the IMG label by calculating the attribute

// The style of the larger image, note that the width and height of the rotation must be reset and the translate value detailImageStyle:function() {return {
      width:this.detailImageWidth+'px',
      height:this.detailImageHeight+'px', // Pay attention to the order: first rotate then move transform:'rotate('+this.detailRotateAngel+'deg)' +' '
                +'translate('+this.detailImageTranslateX+', '+this.detailImageTranslateY+') '}},Copy the code

Rotate and translate. The image can rotate normally, but there is a huge problem. The image can be rotated. For example, when a very long image changes from vertical to horizontal, its width must be no wider than the width of the outermost div, so it needs to dynamically bind width and height to the image, as shown in the code above

The width and height of the image are derived from two calculated properties

// detailImageHeight:function() {if(! This.isdetailimageloaded){// The loading height is fixedreturn this.loadingDefaultHeight
    }else{
      returnEnclosing processImageScaleInRotate (). Height}}, / / details a larger width detailImageWidth:function() {if(! This.isdetailimageloaded){// Width of the outer divlet outerDiv = this.$refs.wrapper;
      letclientWidth = outerDiv? outerDiv.clientWidth:1;return clientWidth
    }else{
      return this.processImageScaleInRotate().width
    }
},
Copy the code

Here is divided into the state of the load and the load calculation, if the image is in loading, the fixed height, width is the width of the outer div, this also is in order to beautiful and set a fixed value, if the image loaded, call processImageScaleInRotate method is high, wide the method is as follows

/ / image rotation to recalculate the details pictures of wide high processImageScaleInRotate:function(){// Get the original width and height of the imageletnw = this.detailImageNaturalWidth, nh = this.detailImageNaturalHeight; // According to the rotation Angle, calculate whether the graph is in the initial state or rotated by 90 degreesletangel = this.detailRotateAngel; // The width and height of the image after rotationlet imageRotatedWidth,imageRotatedHeight;
    let clientWidth = this.$refs.wrapper.clientWidth;
    letratio = nh / nw; // Is the initial statelet isInitialState = true;
    if(angel === 90 || angel === 270 || angel === -90 || angel === -270){
      isInitialState = false; // Rotate once from the initial stateif(nh > clientWidth){
        imageRotatedWidth = clientWidth;
        imageRotatedHeight = imageRotatedWidth / ratio;
      }else{ imageRotatedWidth = nh; imageRotatedHeight = imageRotatedWidth / ratio; }}else{// Rotation once to the initial state isInitialState =true;
      if(nw > clientWidth){
        imageRotatedWidth = clientWidth;
        imageRotatedHeight = imageRotatedWidth * ratio;
      }else{ imageRotatedWidth = nw; imageRotatedHeight = imageRotatedWidth * ratio; }} width and height are easily reversed when rotatedreturn{ width:isInitialState? imageRotatedWidth:imageRotatedHeight, height:isInitialState? imageRotatedHeight:imageRotatedWidth } },Copy the code

This method is relatively complex. We can find that no matter how the rotation is done, the [width and height] data of the picture can only exist in two states, the initial state and the state after rotation. It is easy to understand that these two states are determined by the rotation Angle of the picture detailRotateAngel. When the Angle is 90,270,-90,-270 degrees, it is the state after one rotation, otherwise, it is the initial state, and then calculate the width and height respectively for these two states

Calculation of initial state: First, obtain the original width and height of the picture NW and NH, and then calculate its ratio. When NW >clientWidth, it indicates that the original width of the picture is greater than the maximum width of the outer div. In this case, the width of the picture is clientWidth; otherwise, the width is its original width. The height is ratio times the width. This ensures that the width of the image does not exceed the width of the div.

Calculation after one rotation from the initial state: same principle, except that the value to be determined is NH (original height), because the width and height of the picture are replaced

If the height of the outer div does not change, the image will overflow

 <div class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}">
 <div>
Copy the code

We’re dynamically binding height to the outer div. OuterDivHeight is a calculated property

// The height of the outer div changes as the image rotates.function(){// According to the rotation Angle, calculate whether the graph is in the initial state or rotated by 90 degreeslet angel = this.detailRotateAngel;
    if(angel = = = 90 | | angel = = = 270 | | angel = = = - 90 | | angel = = = - 270) {/ / by the initial state of rotating a situationreturn this.detailImageWidth
    }else{// Initial statereturn this.detailImageHeight
    }
}
Copy the code

The principle is very simple, same as above, also according to the state of the picture to calculate, so the whole rotation logic is over.

Is it a long graph

This logic is actually very simple, the code is as follows

CalcImageIsLongImage:function() {letself = this; This.imagelist. forEach((item,index)=>{this.imagelist. forEach((item,index)=>{let image = new Image();
      image.onload = function() {let ratio = image.naturalHeight / image.naturalWidth;
        if(ratio > self.longImageLimitRatio){// Pass$setThe self method modifies the value in the array.$set(self.isLongImageList,index,true)}}; image.src = item; })},Copy the code

/ / Select * from mounted (prop); / / Select * from mounted (isLongImageList); / / Select * from mounted (isLongImageList); / / Select * from mounted (isLongImageList); / / Select * from mounted (isLongImageList);

Full-screen large picture component

This component is relatively simple, the component is fixed, the CSS of the outer div is as follows, the width and height of the screen is full, and the Z-index is as large as possible

  position: fixed;
  left:0;
  top:0;
  width:100vw;
  height:100vh;
  z-index:10000;
Copy the code

The image below is a very long image with a length of 4 screen heights, which initially requires the entire screen to be able to display the image completely

.img{
  max-width: 100vw;
  max-height: 100vh;
}
Copy the code

The maximum width and height are full screen, because the maximum height is limited, so the height of the picture will not exceed the screen height, no scroll bar will appear, so now it is required to click the picture to view the original picture, at this time only need to set

.img{
    max-height: none;
}
Copy the code

There is no maximum height limit on the image, so the image is displayed at its original height. If the image height exceeds the screen height, a scroll bar appears and the image is widened to its original width