1. 程式人生 > 實用技巧 >React圖片預覽元件,支援縮放、旋轉、上一張下一張功能

React圖片預覽元件,支援縮放、旋轉、上一張下一張功能

1、功能需求:由於專案業務需要一個圖片預覽的功能,又不想引入太多元件依賴,所以決定自己編寫一套,實現了圖片放大縮小、旋轉、檢視下一張或上一張圖片功能,如圖1.0截圖所示。

2、外部資源:這裡的icon圖示採用的是 iconfont 裡面的圖示,自己可以自行尋找自己喜歡的圖示代替,或者使用預設的圖示,預設的圖示css地址為

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

3、功能說明:該元件支援滑鼠滾輪放大縮小及esc關閉功能,也可通過配置進行禁用,根據專案實際應用進行配置。這裡採用的 less 進行樣式編寫。

4、元件名稱:photo-preview。

5、元件截圖:

                   圖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 { width: 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; &::before { content: ' '; display: block; width: 100%; height: 100%; position: absolute; margin-left: 20px; margin-bottom: 20px; } img { position: absolute; width: 100%; height: 100%; } } } .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; } } } } // 漸現 @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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import '_less/photo-preview';

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元素樣式
                width: '0px',
                height: '0px',
            },
            rotateDeg: 0, // 圖片旋轉角度
        };

        // this.bigImgRef = React.createRef();
    }

    // 預覽圖片超出window寬或高的處理
    beyondWindow = () => {
        const { imgParentStyle, rotateDeg } = this.state;
        const iWidth = parseFloat(imgParentStyle.width);
        const iHeight = parseFloat(imgParentStyle.height);
        let ips = imgParentStyle;
        if (rotateDeg % 360 === 90 || rotateDeg % 360 === 270) {
            if (iHeight > window.innerWidth) {
                ips = { ...ips, left: `${(iHeight - iWidth) / 2 + 20}px` };
            } else {
                ips = { ...ips, left: 'auto' };
            }
            if (iWidth > window.innerHeight) {
                ips = { ...ips, top: `${(iWidth - iHeight) / 2 + 20}px` };
            } else {
                ips = { ...ips, top: 'auto' };
            }
        } else {
            if (iWidth > window.innerWidth) {
                ips = { ...ips, left: '20px' };
            } else {
                ips = { ...ips, left: 'auto' };
            }
            if (iHeight > window.innerHeight) {
                ips = { ...ips, top: '20px' };
            } 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: `${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: `${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: {
                    width: '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: {
                    width: '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: {
                width: '0px',
                height: '0px',
            },
        });
        document.body.style.cssText = '';
        window.removeEventListener('mousewheel', this._psMousewheelEvent);
        window.removeEventListener('keydown', this._psKeydownEvent);
        window.removeEventListener('resize', this._psWindowResize);
    };

    // 滑鼠滾輪事件
    _psMousewheelEvent = (event) => {
        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: {
                        width: `${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.style.cssText = 'overflow:hidden';
        }
    };

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

    render() {
        const { alt, url } = this.props;
        const { bigUrl, tool, figureEl, loadEl, imgAttr, imgParentStyle, imgIndex, imgs } = this.state;
        const iParentStyle = { ...imgParentStyle };
        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">
                                      {loadEl ? (
                                          <div className="photo-preview__loading"></div>
                                      ) : (
                                          <div className="photo-preview__img-wrap" style={iParentStyle}>
                                              {/* ref={this.bigImgRef} */}
                                              <img src={imgAttr.src} alt={imgAttr.alt} />
                                          </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 PhotoPreview from '_components/photo-preview';

// 模擬圖片列表資料
const atlasImgList = [
    {
        url: 'http://dummyimage.com/200x100/ff3838&text=Hello',
        bigUrl: 'http://dummyimage.com/400x200/ff3838&text=Hello',
        alt: 'Hello',
    },
    {
        url: 'http://dummyimage.com/200x100/ff9f1a&text=Photo',
        bigUrl: 'http://dummyimage.com/400x200/ff9f1a&text=Photo',
        alt: 'Photo',
    },
    {
        url: 'http://dummyimage.com/200x100/c56cf0&text=Preview',
        bigUrl: 'http://dummyimage.com/400x200/c56cf0&text=Preview',
        alt: 'Preview',
    },
    {
        url: 'http://dummyimage.com/100x100/3ae374&text=!',
        bigUrl: 'http://dummyimage.com/200x200/3ae374&text=!',
        alt: '!',
    },
];

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

export default Test;
View Code