图片预览组件
组件
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;
}
xxxsjan Docs