React图片预览组件,支持缩放、旋转、上一张下一张功能

1、功能需求:由于项目业务需要一个图片预览的功能,又不想引入太多组件依赖,所以决定自己编写一套,实现了图片放大缩小、旋转、查看下一张或上一张图片功能,如图1.0截图所示。

2、外部资源:这里的icon图标采用的是 iconfont 里面的图标,可自行寻找自己喜欢的图标代替,或者使用默认的图标,默认的图标css地址为

  https://at.alicdn.com/t/font_1966765_c473t2y8dvr.css

3、功能说明:该组件支持鼠标滚轮放大缩小及esc关闭功能,也可通过配置进行禁用,根据项目实际应用进行配置。这里采用的 less 进行样式编写。

4、组件名称:Photo-preview。

5、组件截图:

     React图片预览组件,支持缩放、旋转、上一张下一张功能

                     图1.0截图    

6、组件代码:

less 样式:

.photo-preview__thumb-img {
    cursor: pointer;
}

.photo-preview {
    margin: 0;
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    right: 0;
    z-index: 999999;
    background-color: rgba(0, 0, 0, 0.5);
    animation: fadeIn 0.4s;

    .photo-preview__in {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        overflow: auto;
        user-select: none;
        display: flex;
        justify-content: center;
        align-items: center;

        &::-webkit-scrollbar {
             15px;
            height: 15px;
        }

        &::-webkit-scrollbar-track {
            border-radius: 0;
        }

        &::-webkit-scrollbar-thumb {
            border-radius: 0;
            background-color: silver;
        }

        .photo-preview__img-wrap {
            transition-duration: 0.2s;
            position: absolute;
            .photo-preview__img-placeholder {
                display: block;
                width: 100%;
                height: 100%;
                position: absolute;
                pointer-events: none;
            }
            img {
                position: absolute;
                width: 100%;
                height: 100%;
                // cursor: move;
            }
        }
    }

    .photo-preview__loading {
        position: relative;
        &::before {
            content: ' ';
            display: block;
            border-top: 5px solid #999999;
            border-right: 5px solid #999999;
            border-bottom: 5px solid #999999;
            border-left: 5px solid #ffffff;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            animation: rotating 0.8s linear 0s infinite;
        }
    }

    .photo-preview__tool {
        border-radius: 45px;
        padding: 5px 10px;
        height: 45px;
        background-color: #ffffff;
        opacity: 0.3;
        position: fixed;
        top: 20px;
        right: 20px;
        user-select: none;
        transition-duration: 0.5s;
        display: flex;

        &:hover {
            opacity: 0.9;
        }

        .iconfont {
            font-size: 25px;
            text-align: center;
            width: 35px;
            height: 35px;
            line-height: 35px;
            color: #444444;
            // display: inline-block;
            transition-duration: 0.4s;
            margin: 0 2px;
            cursor: pointer;
        }

        .icon-close:hover {
            transform: scale(1.15);
        }

        .icon-turn-left {
            transform: rotate(50deg);
        }

        .icon-turn-left:hover {
            transform: rotate(0deg);
        }

        .icon-turn-right {
            transform: rotate(-50deg);
        }

        .icon-turn-right:hover {
            transform: rotate(0deg);
        }
        .icon-go-left,
        .icon-go-right {
            &[data-disable='true'] {
                // pointer-events: none;
                cursor: wait;
            }
        }
    }
}

body[photo-preview-show='true'] {
    overflow: hidden;
}

// 渐现
@keyframes fadeIn {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

// 旋转
@keyframes rotating {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}
View Code

js 组件代码:

/**
 * @param {type: number, desc: 当前点击的图片索引} imgIndex
 * @param {type: array, desc: 传入的图片列表,结构也应该是[{bigUrl:'imgUrl', alt:'图片描述'}]} imgs
 * @param {type: string, desc: 弹框显示出来的大图} bigUrl
 * @param {type: string, desc: 默认显示的小图片} url
 * @param {type: string, desc: 图片描述} alt
 * @param {type: object, desc: 操作按钮显示,默认都显示,如果对象中指定哪个按钮为false那么表示不显示, 
    example : {
        toSmall: bool,  //缩小按钮是否显示
        toBig: bool,   //放大按钮是否显示
        turnLeft: bool, //左转按钮是否显示
        turnRight: bool  //右转按钮是否显示
        close: bool, //关闭按钮是否显示
        esc: bool, //键盘中的esc键事件是否触发
        mousewheel: bool, // 鼠标滚轮事件是否触发
    }} tool
 *
 * 示例: @example
 *  <PhotoPreview 
 *      bigUrl={item.bigUrl} 
 *      url={item.url} 
 *      alt={item.alt} 
 *      tool={{ turnLeft: false, turnRight: false }} 
 *   />
 * 
 */
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import '@/less/components/photo-preview.less';

class PhotoPreview extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            bigUrl: props.bigUrl === '' ? props.url : props.bigUrl,
            tool: Object.assign(PhotoPreview.defaultProps.tool, props.tool),
            imgIndex: props.imgIndex,
            imgs: props.imgs,

            loadEl: true, // loading元素显示隐藏
            figureEl: false, // 生成图片预览元素
            imgOriginalWidth: 0, // 当前大图默认宽度值
            imgOriginalHeight: 0, // 当前大图默认高度值
            imgAttr: {
                // 大图的地址及描述
                src: '',
                alt: '',
            },
            imgParentStyle: {
                // 大图父级div元素样式
                 '0px',
                height: '0px',
            },
            rotateDeg: 0, // 图片旋转角度
            increaseNum: 20, // 图片放大时距离空隙
        };

        // 获取相关元素
        this.bigImgRef = React.createRef();
        this.ppiRef = React.createRef();
    }

    // 预览图片超出window宽或高的处理
    beyondWindow = () => {
        const { imgParentStyle, rotateDeg, increaseNum } = this.state;
        const iWidth = parseFloat(imgParentStyle.width) + increaseNum * 2;
        const iHeight = parseFloat(imgParentStyle.height) + increaseNum * 2;
        const ppiEl = this.ppiRef.current;

        let ips = imgParentStyle;
        if (rotateDeg % 360 === 90 || rotateDeg % 360 === 270) {
            if (iHeight > window.innerWidth) {
                ips = { ...ips, left: `${(iHeight - iWidth) / 2 + increaseNum}px` };
            } else {
                ips = { ...ips, left: 'auto' };
            }
            if (iWidth > window.innerHeight) {
                ips = { ...ips, top: `${(iWidth - iHeight) / 2 + increaseNum}px` };
            } else {
                ips = { ...ips, top: 'auto' };
            }
        } else if (
            (rotateDeg % 360 === -90 && iWidth > iHeight) ||
            (rotateDeg % 360 === -270 && iWidth > iHeight)
        ) {
            // 如果是-90或-270,并且图片宽大于高的话,那么则需要做兼容处理
            let left = 'auto';
            let top = 'auto';
            if (iHeight > ppiEl.clientWidth) {
                left = `${-(iHeight / 2) + increaseNum * 2}px`;
            }
            if (iWidth > ppiEl.clientHeight) {
                top = `${iHeight / 2 + increaseNum / 2}px`;
            }
            ips = { ...ips, left: `${left}`, top: `${top}` };
        } else if (
            (rotateDeg % 360 === -90 && iHeight > iWidth) ||
            (rotateDeg % 360 === -270 && iHeight > iWidth)
        ) {
            // 如果是-90或-270,并且图片高大于宽的话,那么则需要做兼容处理
            let left = 'auto';
            let top = 'auto';
            if (iHeight > ppiEl.clientWidth) {
                left = `${iWidth / 2}px`;
            }
            if (iWidth > ppiEl.clientHeight) {
                top = `${-(iWidth / 2) + increaseNum * 2}px`;
            }

            ips = { ...ips, left: `${left}`, top: `${top}` };
        } else {
            if (iWidth > window.innerWidth) {
                ips = { ...ips, left: `${increaseNum}px` };
            } else {
                ips = { ...ips, left: 'auto' };
            }
            if (iHeight > window.innerHeight) {
                ips = { ...ips, top: `${increaseNum}px` };
            } else {
                ips = { ...ips, top: 'auto' };
            }
        }
        this.setState({
            imgParentStyle: ips,
        });
    };

    // 图片缩小事件
    toSmallEvent = () => {
        const { tool, imgParentStyle, imgOriginalWidth, imgOriginalHeight } = this.state;
        if (tool.toSmall === false) {
            return;
        }
        let width = parseFloat(imgParentStyle.width) / 1.5;
        let height = parseFloat(imgParentStyle.height) / 1.5;
        // 图片缩小不能超过5倍
        if (width < imgOriginalWidth / 5) {
            width = imgOriginalWidth / 5;
            height = imgOriginalHeight / 5;
        }
        this.setState(
            {
                imgParentStyle: Object.assign(imgParentStyle, {
                     `${width}px`,
                    height: `${height}px`,
                }),
            },
            () => {
                this.beyondWindow();
            }
        );
    };

    // 图片放大事件
    toBigEvent = () => {
        const { tool, imgParentStyle, imgOriginalWidth, imgOriginalHeight } = this.state;
        if (tool.toBig === false) {
            return;
        }
        let width = parseFloat(imgParentStyle.width) * 1.5;
        let height = parseFloat(imgParentStyle.height) * 1.5;
        // 图片放大不能超过5倍
        if (width > imgOriginalWidth * 5) {
            width = imgOriginalWidth * 5;
            height = imgOriginalHeight * 5;
        }
        this.setState(
            {
                imgParentStyle: Object.assign(imgParentStyle, {
                     `${width}px`,
                    height: `${height}px`,
                }),
            },
            () => {
                this.beyondWindow();
            }
        );
    };

    // 向左旋转事件
    turnLeftEvent = () => {
        const { tool, rotateDeg, imgParentStyle } = this.state;
        if (tool.turnLeft === false) {
            return;
        }
        const iRotateDeg = rotateDeg - 90;
        this.setState(
            {
                imgParentStyle: Object.assign(imgParentStyle, {
                    transform: `rotate(${iRotateDeg}deg)`,
                }),
                rotateDeg: iRotateDeg,
            },
            () => {
                this.beyondWindow();
            }
        );
    };

    // 向右旋转事件
    turnRightEvent = () => {
        const { tool, rotateDeg, imgParentStyle } = this.state;
        if (tool.turnRight === false) {
            return;
        }
        const iRotateDeg = rotateDeg + 90;
        this.setState(
            {
                imgParentStyle: Object.assign(imgParentStyle, {
                    transform: `rotate(${iRotateDeg}deg)`,
                }),
                rotateDeg: iRotateDeg,
            },
            () => {
                this.beyondWindow();
            }
        );
    };

    // 上一张图片
    goLeftEvent = () => {
        const { imgIndex, imgs, loadEl } = this.state;
        // 如果还在loading加载中,不予许上一张下一张操作
        if (loadEl) {
            return;
        }
        const nImgIndex = imgIndex - 1;
        // console.log(nImgIndex);
        if (nImgIndex < 0) {
            return;
        }
        this.setState(
            {
                imgIndex: nImgIndex,
                rotateDeg: 0,
                imgParentStyle: {
                     '0px',
                    height: '0px',
                },
            },
            () => {
                this.photoShow(imgs[nImgIndex].bigUrl, imgs[nImgIndex].alt, false);
            }
        );
    };

    // 下一张图片
    goRightEvent = () => {
        const { imgIndex, imgs, loadEl } = this.state;
        // 如果还在loading加载中,不予许上一张下一张操作
        if (loadEl) {
            return;
        }
        const nImgIndex = imgIndex + 1;
        // console.log(nImgIndex);
        if (nImgIndex > imgs.length - 1) {
            return;
        }
        this.setState(
            {
                imgIndex: nImgIndex,
                rotateDeg: 0,
                imgParentStyle: {
                     '0px',
                    height: '0px',
                },
            },
            () => {
                // 如果不存在大图,那么直接拿小图代替。
                const bigUrl = imgs[nImgIndex].bigUrl || imgs[nImgIndex].url;
                this.photoShow(bigUrl, imgs[nImgIndex].alt);
            }
        );
    };

    // 关闭事件
    closeEvent = () => {
        // 恢复到默认值
        const { imgIndex, imgs } = this.props;
        this.setState({
            imgIndex,
            imgs,
            figureEl: false,
            rotateDeg: 0,
            imgParentStyle: {
                 '0px',
                height: '0px',
            },
        });
        window.removeEventListener('mousewheel', this._psMousewheelEvent);
        window.removeEventListener('keydown', this._psKeydownEvent);
        window.removeEventListener('resize', this._psWindowResize);
        document.body.removeAttribute('photo-preview-show');
    };

    // 大图被执行拖拽操作
    bigImgMouseDown = (event) => {
        event.preventDefault();
        const ppiEl = this.ppiRef.current;
        const bigImgEl = this.bigImgRef.current;
        const diffX = event.clientX - bigImgEl.offsetLeft;
        const diffY = event.clientY - bigImgEl.offsetTop;
        // 鼠标移动的时候
        bigImgEl.onmousemove = (ev) => {
            const moveX = parseFloat(ev.clientX - diffX);
            const moveY = parseFloat(ev.clientY - diffY);
            const mx = moveX > 0 ? -moveX : Math.abs(moveX);
            const my = moveY > 0 ? -moveY : Math.abs(moveY);
            let sl = ppiEl.scrollLeft + mx * 0.1;
            let sr = ppiEl.scrollTop + my * 0.1;
            if (sl <= 0) {
                sl = 0;
            } else if (sl >= ppiEl.scrollWidth - ppiEl.clientWidth) {
                sl = ppiEl.scrollWidth - ppiEl.clientWidth;
            }
            if (sr <= 0) {
                sr = 0;
            } else if (sr >= ppiEl.scrollHeight - ppiEl.clientHeight) {
                sr = ppiEl.scrollHeight - ppiEl.clientHeight;
            }
            ppiEl.scrollTo(sl, sr);
        };
        // 鼠标抬起的时候
        bigImgEl.onmouseup = () => {
            bigImgEl.onmousemove = null;
            bigImgEl.onmouseup = null;
        };
        // 鼠标离开的时候
        bigImgEl.onmouseout = () => {
            bigImgEl.onmousemove = null;
            bigImgEl.onmouseup = null;
        };
    };

    // 鼠标滚轮事件
    _psMousewheelEvent = (event) => {
        // event.preventDefault();
        const { figureEl, tool } = this.state;
        if (figureEl && tool.mousewheel) {
            if (event.wheelDelta > 0) {
                this.toBigEvent();
            } else {
                this.toSmallEvent();
            }
        }
    };

    // 键盘按下事
    _psKeydownEvent = (event) => {
        const { figureEl, tool } = this.state;
        if (event.keyCode === 27 && tool.esc && figureEl) {
            this.closeEvent();
        }
    };

    // 窗口发生改变的时候
    _psWindowResize = () => {
        const { figureEl } = this.state;
        if (figureEl) {
            this.beyondWindow();
        }
    };

    // 图片展示
    photoShow = (url, alt, winEventToggle) => {
        // 图片加载并处理
        this.setState({
            loadEl: true,
            figureEl: true,
        });
        const img = new Image();
        img.src = url;
        img.onload = async () => {
            this.setState(
                {
                    loadEl: false,
                    imgOriginalWidth: img.width,
                    imgOriginalHeight: img.height,
                    imgAttr: {
                        src: url,
                        alt,
                    },
                    imgParentStyle: {
                         `${img.width}px`,
                        height: `${img.height}px`,
                    },
                },
                () => {
                    this.beyondWindow();
                }
            );
        };

        // 是否需再次执行window事件
        const wev = winEventToggle || true;
        if (wev) {
            // console.log('wev');
            // window触发事件
            window.addEventListener('mousewheel', this._psMousewheelEvent);
            window.addEventListener('keydown', this._psKeydownEvent);
            window.addEventListener('resize', this._psWindowResize);
            document.body.setAttribute('photo-preview-show', 'true');
        }
    };

    UNSAFE_componentWillReceiveProps(newProps) {
        console.log(`new-props:${newProps.nImgIndex}`);
    }

    render() {
        const { alt, url } = this.props;
        const {
            bigUrl,
            tool,
            figureEl,
            loadEl,
            imgAttr,
            imgParentStyle,
            imgIndex,
            imgs,
            increaseNum,
        } = this.state;
        const iParentStyle = { ...imgParentStyle };
        const iSpanStyle = {
             `${parseFloat(imgParentStyle.width) + increaseNum * 2}px`,
            height: `${parseFloat(imgParentStyle.height) + increaseNum * 2}px`,
        };
        return (
            <>
                <img
                    onClick={this.photoShow.bind(this, bigUrl, alt)}
                    src={url}
                    alt={alt}
                    className="photo-preview__thumb-img"
                />
                {figureEl
                    ? ReactDOM.createPortal(
                          <>
                              <figure className="photo-preview">
                                  <div className="photo-preview__in" ref={this.ppiRef}>
                                      {loadEl ? (
                                          <div className="photo-preview__loading"></div>
                                      ) : (
                                          <div
                                              className="photo-preview__img-wrap"
                                              style={iParentStyle}
                                          >
                                              <span
                                                  className="photo-preview__img-placeholder"
                                                  style={{
                                                      ...iSpanStyle,
                                                      marginLeft: `-${increaseNum}px`,
                                                      marginTop: `-${increaseNum}px`,
                                                  }}
                                              ></span>
                                              <img
                                                  src={imgAttr.src}
                                                  alt={imgAttr.alt}
                                                  onMouseDown={this.bigImgMouseDown}
                                                  ref={this.bigImgRef}
                                              />
                                          </div>
                                      )}
                                      <div className="photo-preview__tool">
                                          {tool.toSmall ? (
                                              <i
                                                  className="iconfont icon-to-small"
                                                  onClick={this.toSmallEvent}
                                              ></i>
                                          ) : null}
                                          {tool.toBig ? (
                                              <i
                                                  className="iconfont icon-to-big"
                                                  onClick={this.toBigEvent}
                                              ></i>
                                          ) : null}
                                          {tool.turnLeft ? (
                                              <i
                                                  className="iconfont icon-turn-left"
                                                  onClick={this.turnLeftEvent}
                                              ></i>
                                          ) : null}
                                          {tool.turnRight ? (
                                              <i
                                                  className="iconfont icon-turn-right"
                                                  onClick={this.turnRightEvent}
                                              ></i>
                                          ) : null}

                                          {imgIndex !== '' && imgs.length > 1 ? (
                                              <>
                                                  <i
                                                      className="iconfont icon-go-left"
                                                      onClick={this.goLeftEvent}
                                                      data-disable={loadEl ? 'true' : 'false'}
                                                  ></i>
                                                  <i
                                                      className="iconfont icon-go-right"
                                                      onClick={this.goRightEvent}
                                                      data-disable={loadEl ? 'true' : 'false'}
                                                  ></i>
                                              </>
                                          ) : null}

                                          {tool.close ? (
                                              <i
                                                  className="iconfont icon-close"
                                                  onClick={this.closeEvent}
                                              ></i>
                                          ) : null}
                                      </div>
                                  </div>
                              </figure>
                          </>,
                          document.body
                      )
                    : null}
            </>
        );
    }
}

PhotoPreview.defaultProps = {
    bigUrl: '',
    alt: '',
    tool: {
        toSmall: true, // 缩小按钮
        toBig: true, // 放大按钮
        turnLeft: true, // 左转按钮
        turnRight: true, // 右转按钮
        close: true, // 关闭按钮
        esc: true, // esc键触发
        mousewheel: true, // 鼠标滚轮事件是否触发
    },
    imgIndex: '',
    imgs: [],
};
PhotoPreview.propTypes = {
    bigUrl: PropTypes.string,
    url: PropTypes.string.isRequired,
    alt: PropTypes.string,
    tool: PropTypes.object,
    imgIndex: PropTypes.number,
    imgs: PropTypes.array,
};
export default PhotoPreview;
View Code

js 组件案例代码:

import React from 'react';
// 导入图片预览组件
import PhotoPreview from '@/components/photo-preview';

// 模拟图片列表数据
const atlasImgList = [
    {
        url: 'http://dummyimage.com/200x100/ff3838&text=Hello',
        bigUrl: 'http://dummyimage.com/800x400/ff3838&text=Hello',
        alt: 'Hello',
    },
    {
        url: 'http://dummyimage.com/200x100/ff9f1a&text=Photo',
        bigUrl: 'http://dummyimage.com/800x400/ff9f1a&text=Photo',
        alt: 'Photo',
    },
    {
        url: 'http://dummyimage.com/200x100/c56cf0&text=Preview',
        bigUrl: 'http://dummyimage.com/800x400/c56cf0&text=Preview',
        alt: 'Preview',
    },
    {
        url: 'http://dummyimage.com/100x100/3ae374&text=!',
        bigUrl: 'http://dummyimage.com/400x400/3ae374&text=!',
        alt: '!',
    },
];

const Test = () => {
    return (
        <>
            {atlasImgList.map((item, index) => {
                return (
                    <PhotoPreview
                        key={index}
                        imgIndex={index}
                        imgs={atlasImgList}
                        url={item.url}
                        bigUrl={item.bigUrl}
                        alt={item.alt}
                        // tool={{ turnLeft: false, turnRight: false }}
                    />
                );
            })}
        </>
    );
};

export default Test;
View Code

相关推荐