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 { 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元素样式 '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;
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;