Skip to content
On this page

图片预览组件

组件

js
import React from 'react'
import PropTypes from 'prop-types'
import { Icon } from 'antd'

class ImagePreview extends React.Component {
    static defaultProps = {
      images: []
    }
    static propTypes = {
      images: PropTypes.array,
      onClose: PropTypes.func,
      current: PropTypes.number
    };
    containerRef = React.createRef();
    imgRef = React.createRef();
    cacheRef = React.createRef();

    constructor (props) {
      super(props)
      this.state = {
        currentImageIndex: this.props.current || 0,
        scale: 1,
        isDragging: false,
        translateX: 0,
        translateY: 0
      }
    }
    componentDidMount () {
      document.addEventListener('mousemove', this.handleMouseMove)
      document.addEventListener('mouseup', this.handleMouseUp)
    }
    handleZoomIn = () => {
      const step = 0.25
      this.setState({ scale: this.state.scale + step })
    };

    handleZoomOut = () => {
      if (this.state.scale === 1) return
      const step = 0.25
      const newScale = this.state.scale - step
      this.setState({ scale: newScale > step ? newScale : step })
    };

    handleNextImage = () => {
      const nextIndex = (this.state.currentImageIndex + 1) % this.props.images.length
      this.setState({ currentImageIndex: nextIndex })
    };

    handlePreviousImage = () => {
      const previousIndex = (this.state.currentImageIndex - 1 + this.props.images.length) % this.props.images.length
      this.setState({ currentImageIndex: previousIndex })
    };

    handleMouseDown = (event) => {
      event.preventDefault()
      const {left, top, width, height} = event.target.getBoundingClientRect() // img
      const {clientWidth, clientHeight} = document.documentElement
      const right = clientWidth - left - width
      const bottom = clientHeight - top - height

      const maxTranslateX = (clientWidth / 2 > width / 2) > 0
        ? (clientWidth / 2 - width / 2) / this.state.scale
        : (width / 2 - clientWidth / 2) / this.state.scale
      const maxTranslateY = (clientHeight / 2 - height / 2) > 0
        ? (clientHeight / 2 - height / 2) / this.state.scale
        : (height / 2 - clientHeight / 2) / this.state.scale

      this.cacheRef.current = {
        left: Math.ceil(left),
        top: Math.ceil(top),
        right: Math.ceil(right),
        bottom: Math.ceil(bottom),
        translateX: this.state.translateX,
        translateY: this.state.translateY,
        maxTranslateX,
        maxTranslateY,
        clientWidth,
        clientHeight,
        startX: event.clientX,
        startY: event.clientY,
        scale: this.state.scale
      }
      this.setState({
        isDragging: true
      })
    };

    handleMouseUp = (event) => {
      if (this.state.isDragging) {
        this.setState({ isDragging: false })
      }
    };

    checkPosition = () => {
      const imgEl = this.imgRef.current
      if (!imgEl) return
      const {
        left, top, right, bottom,
        maxTranslateX, maxTranslateY
      } = this.cacheRef.current
      const imgELRect = imgEl.getBoundingClientRect()
      const curLeft = imgELRect.left
      const curTop = imgELRect.top
      if (left > 0 && top > 0 && right > 0 && bottom > 0) {
        this.setTranslate({
          translateX: 0,
          translateY: 0
        })
        return
      }
      const isLeft = curLeft < left
      const isTop = curTop < top
      let _translateX = isLeft
        ? Math.max(-maxTranslateX, this.state.translateX)
        : Math.min(maxTranslateX, this.state.translateX)
      let _translateY = isTop
        ? Math.max(-maxTranslateY, this.state.translateY)
        : Math.min(maxTranslateY, this.state.translateY)

      this.setTranslate({
        translateX: _translateX,
        translateY: _translateY
      })
    }
    setTranslate = ({translateX, translateY}) => {
      this.setState({
        translateX: translateX,
        translateY: translateY
      })
      this.cacheRef.current = {
        ...this.cacheRef.current,
        translateX: translateX,
        translateY: translateY
      }
    }
    handleMouseMove = (event) => {
      if (this.state.isDragging) {
        const {translateX, translateY, startX, startY, scale} = this.cacheRef.current
        requestAnimationFrame(() => {
          this.setState({
            translateX: translateX + (event.clientX - startX) / scale,
            translateY: translateY + (event.clientY - startY) / scale
          })
        })
      }
    };
    handleWheel = (event) => {
      event.preventDefault()
      const _step = 0.25
      const delta = event.deltaY > 0 ? -_step : _step
      this.setState({ scale: Math.max(this.state.scale + delta, 0.5) })
    }
    clickContainer = (event) => {
      event.stopPropagation()
      if (event.target.tagName === 'IMG') {
        return
      }
      this.props.onClose && this.props.onClose()
    }
    handleReset = () => {
      this.setState({
        scale: 1,
        translateX: 0,
        translateY: 0
      })
    }
    render () {
      const { images } = this.props
      const { currentImageIndex, scale, isDragging, translateX, translateY } = this.state
      const currentImage = images[currentImageIndex]
      return <div className={styles.imagePreViewWrap} >
        <div className={styles.ImagePreviewMask} onClick={() => { this.props.onClose && this.props.onClose() }} />

        <div className={styles['image-container']}
          ref={this.containerRef}
          style={{
            transform: `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`,
            cursor: isDragging ? 'grabbing' : 'grab'
          }}
          onWheel={this.handleWheel}
          onMouseDown={this.handleMouseDown}
          onClick={this.clickContainer}
          onTransitionEnd={() => this.checkPosition()}
        >
          <img src={currentImage} alt="Preview" ref={this.imgRef} />
        </div>

        <Icon type="close" className={styles['controls-close']} onClick={() => { this.props.onClose && this.props.onClose() }} />

        <div className={styles.toolbar} >
          <button className={styles.btn} onClick={this.handleZoomIn} ><Icon type="plus" /></button>
          <button className={styles.btn} onClick={this.handleZoomOut} ><Icon type="minus" /></button>
          <button className={styles.btn} onClick={this.handleReset} ><Icon type="reload" /></button>
        </div>

        {currentImageIndex !== 0 && <Icon type="left" className={styles['controls-left']} onClick={this.handlePreviousImage} />}
        {currentImageIndex < images.length - 1 && <Icon type="right" className={styles['controls-right']} onClick={this.handleNextImage} />}
      </div>
    }
}
export default ImagePreview

样式

css
.imagePreViewWrap {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 999;
    width: 100%;
    height: 100%;
    overflow: hidden;
    white-space: nowrap;
    box-sizing: border-box
}
.ImagePreviewMask{
    position:absolute;
    top:0;
    left:0;
    right:0;
    bottom:0;
    background-color: rgba(0, 0, 0, 0.5);
    width:100%;
    height: 100%;
}

.imagePreView {
    width: 80%;
    height: 80%;
    object-fit: cover;
}

.toolbar {
    display: block;
    position: fixed;
    bottom: 50px;
    left: 50%;
    bottom: 20px;
    background-color: #333333;
    border-radius: 5px;
    overflow: hidden;
    transform: translateX(-50%);
}

.toolbar .btn {
    padding: 0;
    margin: 0;
    background-color: transparent;
    background-position: center !important;
    background-repeat: no-repeat !important;
    height: 50px;
    width: 50px;
    border-radius: 0;
    color: #fff;
    text-align: center;
    cursor: pointer;
    border: none;
    outline: none;
}

.toolbar .btn:hover {
    background-color: #707070;
}

.image-container {
    height: 100%;
    overflow: hidden;
    transition: transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) 0s;
    user-select: none;
    display: flex;
    justify-content: center;
    align-items: center;
}

.image-container img {
    max-width: 100%;
    max-height: 80%;
    object-fit: contain;
    user-select: none;
}
.image-container.grab{
    cursor: grab;
}
.image-container.grabbing{
    cursor: grabbing;
}


.controls-left{
  position: fixed;
  left: 20px;
  top:50%;
  transform: translateY(-50%);
  z-index: 999;
  font-size: 40px;
  color: #fff;
  cursor: pointer;
  user-select: none;
}
.controls-right{
  position: fixed;
  right: 20px;
  top:50%;
  transform: translateY(-50%);
  z-index: 999;
  font-size: 40px;
  color: #fff;
  cursor: pointer;
  user-select: none;
}
.controls-close{
  position: fixed;
  right: 20px;
  top:20px;
  z-index: 999;
  font-size: 22px;
  color: #fff;
  cursor: pointer;
  user-select: none;
}

粤ICP备2024285819号