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; .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); } }
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元素樣式 width: '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: `${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', }, }); 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: { 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.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 = { width: `${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;
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;