diff --git a/config/config.js b/config/config.js index ac13ede4054a113c2dc5117fc15eae54feeb5e11..2cd3e7e7dc19eb2916017c91a6301807338d3636 100644 --- a/config/config.js +++ b/config/config.js @@ -113,6 +113,12 @@ export default { icon: 'smile', component: './Welcome', }, + { + path: '/preview', + name: 'preview', + icon: 'smile', + component: './demo/preview', + }, { component: './404', }, diff --git a/package.json b/package.json index 7a1f66fa6daf21417fc19f28eb6a0451250b677f..82f1cecf9771a7bc0b9603507f35dca026fd64e2 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,11 @@ "moment": "^2.24.0", "omit.js": "^1.0.2", "path-to-regexp": "^3.0.0", + "ppfish": "^1.7.1", "prop-types": "^15.7.2", "qs": "^6.7.0", + "rc-animate": "^2.10.0", "react": "^16.8.6", - "react-amap": "^1.2.8", "react-copy-to-clipboard": "^5.0.1", "react-document-title": "^2.0.3", "react-dom": "^16.8.6", diff --git a/src/components/ImageCropUpload/ReactImageCrop.jsx b/src/components/ImageCropUpload/ReactImageCrop.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cfc8b1a2fccbb9433922cc6b3e49d622e7399ae6 --- /dev/null +++ b/src/components/ImageCropUpload/ReactImageCrop.jsx @@ -0,0 +1,746 @@ +import React, { Component } from 'react'; +import { Button, Icon, Slider } from 'antd'; +import PropTypes from 'prop-types'; +import mimes from '@/utils/common'; +import { data2blob, effectRipple } from '@/utils/common'; +import styles from './ReactImageCrop.less'; + +class ReactImageCrop extends Component { + static defaultProps = { + // 图片上传格式 + imgFormat: 'png', + // 图片背景 jpg情况下生效 + imgBgc: '#fff', + // 剪裁图片的宽 + width: 200, + // 剪裁图片的高 + height: 200, + // 不显示旋转功能 + noRotate: true, + // 关闭 圆形图像预览 + noCircle: false, + // FIX 这个功能有bug + // 关闭 旋转图像功能 + noSquare: false, + }; + + static propTypes = { + width: PropTypes.number, + height: PropTypes.number, + imgFormat: PropTypes.string, + imgBgc: PropTypes.string, + noCircle: PropTypes.bool, + noSquare: PropTypes.bool, + noRotate: PropTypes.bool, + off: PropTypes.func.isRequired, + upload: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.canvasRef = React.createRef(); + this.fileinput = React.createRef(); + + let allowImgFormat = ['jpg', 'png'], + tempImgFormat = + allowImgFormat.indexOf(this.props.imgFormat) === -1 + ? "jpg" + : this.props.imgFormat, + nMime = mimes[tempImgFormat]; + + this.state = { + step: 1, // 步骤 + file: null, + // 浏览器是否支持该控件 + isSupported: typeof FormData === 'function', + // 浏览器是否支持触屏事件 + isSupportTouch: document.hasOwnProperty('ontouchstart'), + // 原图片拖动事件初始值 + sourceImgMouseDown: { + on: false, + mime: nMime, + mX: 0, // 鼠标按下的坐标 + mY: 0, + x: 0, // scale原图坐标 + y: 0, + }, + // 原图展示属性 + scale: { + zoomAddOn: false, // 按钮缩放事件开启 + zoomSubOn: false, // 按钮缩放事件开启 + range: 1, // 最大100 + + x: 0, + y: 0, + width: 0, + height: 0, + maxWidth: 0, + maxHeight: 0, + minWidth: 0, // 最宽 + minHeight: 0, + naturalWidth: 0, // 原宽 + naturalHeight: 0, + }, + // 原图容器宽高 + sourceImgContainer: { + // sic + width: 240, + height: 184, // 如果生成图比例与此一致会出现bug,先改成特殊的格式吧,哈哈哈 + }, + mime: mimes['jpg'], + // 需求图宽高比 + ratio: this.props.width / this.props.height, + // 原图地址、生成图片地址 + sourceImg: null, + sourceImgUrl: '', + createImgUrl: '', + // 生成图片预览的容器大小 + previewContainer: { + width: 100, + height: 100, + }, + // 滑动条数字 + sliderValue: 0, + }; + } + + // 原图蒙版属性 + get sourceImgMasking() { + const { sourceImgContainer, ratio } = this.state; + const { width, height } = this.props; + let sic = sourceImgContainer, + sicRatio = sic.width / sic.height, // 原图容器宽高比 + x = 0, + y = 0, + w = sic.width, + h = sic.height, + scale = 1; + if (ratio < sicRatio) { + scale = sic.height / height; + w = sic.height * ratio; + x = (sic.width - w) / 2; + } + if (ratio > sicRatio) { + scale = sic.width / width; + h = sic.width / ratio; + y = (sic.height - h) / 2; + } + return { + scale, // 蒙版相对需求宽高的缩放 + x, + y, + width: w, + height: h, + }; + } + + // 原图样式 + get sourceImgStyle() { + const { scale } = this.state; + const sourceImgMasking = this.sourceImgMasking; + const top = scale.y + sourceImgMasking.y + 'px'; + const left = scale.x + sourceImgMasking.x + 'px'; + return { + position: 'absolute', + top, + left, + width: scale.width + 'px', + height: scale.height + 'px', // 兼容 Opera + }; + } + + // 原图遮罩样式 + get sourceImgShadeStyle() { + const { sourceImgContainer } = this.state; + const sourceImgMasking = this.sourceImgMasking; + const sic = sourceImgContainer; + const sim = sourceImgMasking; + const w = sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2; + const h = sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2; + return { + width: w + 'px', + height: h + 'px', + }; + } + + get previewStyle() { + const { ratio, previewContainer } = this.state; + const pc = previewContainer; + let w = pc.width; + let h = pc.height; + const pcRatio = w / h; + if (ratio < pcRatio) { + w = pc.height * ratio; + } + if (ratio > pcRatio) { + h = pc.width / ratio; + } + return { + width: w + 'px', + height: h + 'px', + }; + } + + changeFile = e => { + e.preventDefault(); + console.log('bb', e.target); + let files = e.target.files || e.dataTransfer.files; + this.setState({ + file: files[0], + }); + this.setSourceImg(files[0]); + this.setStep(2); + }; + + // 设置图片源 + setSourceImg = file => { + // this.$emit('src-file-set', file.name, file.type, file.size); + const fr = new FileReader(); + fr.onload = e => { + this.setState({ sourceImgUrl: fr.result }); + this.startCrop(); + }; + fr.readAsDataURL(file); + }; + + // 剪裁前准备工作 + startCrop = () => { + let that = this; + const { width, height } = that.props; + const { ratio, scale, sourceImgUrl } = that.state; + let sim = this.sourceImgMasking, + img = new Image(); + img.src = sourceImgUrl; + img.onload = () => { + let nWidth = img.naturalWidth, + nHeight = img.naturalHeight, + nRatio = nWidth / nHeight, + w = sim.width, + h = sim.height, + x = 0, + y = 0; + // 图片像素不达标 + if (nWidth < width || nHeight < height) { + that.hasError = true; + that.errorMsg = `图片最低像素为(宽*高):${width}*${height}`; + return false; + } + if (ratio > nRatio) { + h = w / nRatio; + y = (sim.height - h) / 2; + } + if (ratio < nRatio) { + w = h * nRatio; + x = (sim.width - w) / 2; + } + this.setState({ + scale: { + ...scale, + range: 0, + x: x, + y: y, + width: w, + height: h, + minWidth: w, + minHeight: h, + maxWidth: nWidth * sim.scale, + maxHeight: nHeight * sim.scale, + naturalWidth: nWidth, + naturalHeight: nHeight, + }, + sourceImg: img, + }); + this.createImg(); + }; + }; + + prepareUpload = () => { + // const putExtra = { + // fname: this.file.name, + // } + // const config = { + // region: qiniu.region.z2 + // } + // var observable = qiniu.upload(this.file, this.file.name, token, putExtra, config) + // var subscription = observable.subscribe(observer) // 上传开始 + const { createImgUrl } = this.state, + blob = data2blob(createImgUrl); + this.props.upload({ + createImgUrl, + blob, + file: this.state.file, + }); + }; + + /* 图片选择区域函数绑定 */ + preventDefault = e => { + e.preventDefault(); + return false; + }; + + // 鼠标按下图片准备移动 + imgStartMove = e => { + e.preventDefault(); + // 支持触摸事件,则鼠标事件无效 + if (this.state.isSupportTouch && !e.targetTouches) { + return false; + } + let et = e.targetTouches ? e.targetTouches[0] : e, + { sourceImgMouseDown, scale } = this.state, + simd = sourceImgMouseDown; + simd.mX = et.screenX; + simd.mY = et.screenY; + simd.x = scale.x; + simd.y = scale.y; + simd.on = true; + }; + + // 鼠标按下状态下移动,图片移动 + imgMove = e => { + e.preventDefault(); + // 支持触摸事件,则鼠标事件无效 + if (this.state.isSupportTouch && !e.targetTouches) { + return false; + } + let et = e.targetTouches ? e.targetTouches[0] : e, + { + sourceImgMouseDown: { on, mX, mY, x, y }, + scale + } = this.state, + sim = this.sourceImgMasking, + nX = et.screenX, + nY = et.screenY, + dX = nX - mX, + dY = nY - mY, + rX = x + dX, + rY = y + dY; + if (!on) return; + if (rX > 0) { + rX = 0; + } + if (rY > 0) { + rY = 0; + } + if (rX < sim.width - scale.width) { + rX = sim.width - scale.width; + } + if (rY < sim.height - scale.height) { + rY = sim.height - scale.height; + } + this.setState({ + scale: { + ...this.state.scale, + x: rX, + y: rY, + }, + }); + }; + + // 生成需求图片 + createImg = e => { + let that = this; + const { mime, sourceImg, scale: { x, y, width, height } } = that.state; + const { imgFormat, imgBgc } = that.props; + const { scale } = this.sourceImgMasking; + let canvas = this.canvasRef.current; + const ctx = canvas.getContext('2d'); + if (e) { + // 取消鼠标按下移动状态 + that.setState({ + sourceImgMouseDown: { + ...that.state.sourceImgMouseDown, + on: false, + }, + }); + } + canvas.width = that.props.width; + canvas.height = that.props.height; + ctx.clearRect(0, 0, that.props.width, that.props.height); + + if (imgFormat === 'png') { + ctx.fillStyle = 'rgba(0,0,0,0)'; + } else { + // 如果jpg 为透明区域设置背景,默认白色 + ctx.fillStyle = imgBgc; + } + ctx.fillRect(0, 0, that.props.width, that.props.height); + + ctx.drawImage( + sourceImg, + x / scale, + y / scale, + width / scale, + height / scale, + ); + that.setState({ + createImgUrl: canvas.toDataURL(mime), + }); + }; + + handleClick = e => { + if (e.target !== this.fileinput.current) { + e.preventDefault(); + this.fileinput.current.click(); + } + }; + + // 点击波纹效果 + ripple = e => { + effectRipple(e); + }; + + setStep = val => { + if (val === 1) { + this.fileinput.current.value = null; + } + this.setState({ step: val }); + }; + + // 按钮按下开始缩小 + startZoomSub = e => { + let that = this, + { scale } = that.state; + this.setState({ + scale: { + ...scale, + zoomSubOn: true + } + }); + + function zoom() { + if (scale.zoomSubOn) { + let range = scale.range <= 0 ? 0 : --scale.range; + that.zoomImg(range); + setTimeout(function() { + zoom(); + }, 60); + } + } + zoom(); + }; + + // 缩放原图 + zoomImg = newRange => { + let that = this; + const { scale } = this.state; + const sourceImgMasking = this.sourceImgMasking; + console.log('a', sourceImgMasking); + const { maxWidth, maxHeight, minWidth, minHeight, width, height, x, y } = scale; + const sim = sourceImgMasking; + // 蒙版宽高 + const sWidth = sim.width; + const sHeight = sim.height; + // 新宽高 + const nWidth = minWidth + (maxWidth - minWidth) * newRange / 100; + const nHeight = minHeight + (maxHeight - minHeight) * newRange / 100; + // 新坐标(根据蒙版中心点缩放) + let nX = sWidth / 2 - nWidth / width * (sWidth / 2 - x); + let nY = sHeight / 2 - nHeight / height * (sHeight / 2 - y); + + // 判断新坐标是否超过蒙版限制 + if (nX > 0) { + nX = 0; + } + if (nY > 0) { + nY = 0; + } + if (nX < sWidth - nWidth) { + nX = sWidth - nWidth; + } + if (nY < sHeight - nHeight) { + nY = sHeight - nHeight; + } + + // 赋值处理 + this.setState({ + scale: { + ...scale, + x: nX, + y: nY, + width: nWidth, + height: nHeight, + range: newRange, + }, + }); + setTimeout(function() { + if (scale.range === newRange) { + that.createImg(); + } + }, 300); + }; + + // 按钮松开或移开取消缩小 + endZoomSub = e => { + let { scale } = this.state; + this.setState({ + scale: { + ...scale, + zoomSubOn: false, + }, + }); + }; + + // 按钮按下开始放大 + startZoomAdd = e => { + let that = this, + { scale } = that.state; + this.setState({ + scale: { + ...scale, + zoomAddOn: true + } + }); + + function zoom() { + if (scale.zoomAddOn) { + let range = scale.range >= 100 ? 100 : ++scale.range; + that.zoomImg(range); + setTimeout(function() { + zoom(); + }, 60); + } + } + zoom(); + }; + + // 按钮松开或移开取消放大 + endZoomAdd = e => { + const { scale } = this.state; + this.setState({ + scale: { + ...scale, + zoomAddOn: false, + }, + }); + }; + + zoomChange = value => { + console.log(value); + this.setState({ + sliderValue: value, + }); + this.zoomImg(value); + // this.zoomImg(e.target.value); + }; + + zoomSub = () => { + const { sliderValue } = this.state; + const value = sliderValue - 10; + this.setState({ + sliderValue: value > 0 ? value : 0, + }); + }; + + zoomAdd = () => { + const { sliderValue } = this.state; + const value = sliderValue + 10; + this.setState({ + sliderValue: value < 100 ? value : 100, + }); + }; + + + // 顺时针旋转图片 + rotateImg = e => { + let { + sourceImg, + scale: { naturalWidth, naturalHeight } + } = this.state, + width = naturalHeight, + height = naturalWidth, + canvas = this.canvasRef.current, + ctx = canvas.getContext("2d"); + canvas.width = width; + canvas.height = height; + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = "rgba(0,0,0,0)"; + ctx.fillRect(0, 0, width, height); + + ctx.translate(width, 0); + ctx.rotate(Math.PI * 90 / 180); + + ctx.drawImage(sourceImg, 0, 0, naturalWidth, naturalHeight); + let imgUrl = canvas.toDataURL(mimes["png"]); + + this.setState({ + sourceImgUrl: imgUrl + }); + this.startCrop(); + }; + + showFiles = () => { + const { sourceImgUrl, createImgUrl, file, scale, sliderValue } = this.state; + const { noRotate, noCircle, noSquare } = this.props; + + if (!file) { + return ''; + } + + return ( +
+
+
+ +
+
+
+
+ + + +
+ +
+ {/**/} + + +
+ {noRotate && ( +
+ +
+ )} +
+
+ {!noSquare && ( +
+ +
头像预览
+
+ )} + {!noCircle && ( +
+ +
头像预览
+
+ )} +
+
+ ); + }; + + render() { + const { width, height, off } = this.props; + const { step, isSupported } = this.state; + + return ( +
+ {/*
*/} + {/**/} + {/*
*/} +
+
+ + + + + + 点击,或拖动图片至此处 + + +
+
+ + {/**/} +
+ {this.showFiles()} +
+ +
+ + + + +
+ +
+ ); + } +} + +export default ReactImageCrop; diff --git a/src/components/ImageCropUpload/ReactImageCrop.less b/src/components/ImageCropUpload/ReactImageCrop.less new file mode 100644 index 0000000000000000000000000000000000000000..151b9602e7f2b6bb01ccc2da0f323b269354d838 --- /dev/null +++ b/src/components/ImageCropUpload/ReactImageCrop.less @@ -0,0 +1,451 @@ +@keyframes ricu { + 0% { + opacity: 0; + transform: scale(0) translatey(-60px); + } + 100% { + opacity: 1; + transform: scale(1) translatey(0); + } +} + +.ricu { + //position: fixed; + //display: block; + //box-sizing: border-box; + //z-index: 10000; + //top: 0; + //bottom: 0; + //left: 0; + //right: 0; + //width: 100%; + //height: 100%; + //background-color: rgba(0, 0, 0, 0.65); + //-webkit-tap-highlight-color: transparent; + + //.wrap { + //box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + //position: fixed; + //display: block; + //-webkit-box-sizing: border-box; + //box-sizing: border-box; + //z-index: 10000; + //top: 0; + //bottom: 0; + //left: 0; + //right: 0; + //margin: auto; + //width: 600px; + //height: 330px; + //padding: 25px; + //background-color: #fff; + //border-radius: 2px; + //animation: ricu 0.12s ease-in; + //} + + .close { + position: absolute; + right: -30px; + top: -30px; + } + + .icon4 { + position: relative; + display: block; + width: 30px; + height: 30px; + cursor: pointer; + transition: -webkit-transform 0.18s; + transform: rotate(0); + + &::after, + &::before { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + content: ""; + position: absolute; + top: 12px; + left: 4px; + width: 20px; + height: 3px; + transform: rotate(45deg); + background-color: #fff; + } + + &::after { + transform: rotate(-45deg); + } + + &:hover { + transform: rotate(90deg); + } + } + + .step1 { + .drop_area { + position: relative; + box-sizing: border-box; + padding: 35px; + height: 170px; + background-color: rgba(0, 0, 0, 0.03); + text-align: center; + border: 1px dashed rgba(0, 0, 0, 0.08); + overflow: hidden; + + &:hover { + cursor: pointer; + border-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.05); + } + + .ricu-icon1 { + display: block; + margin: 0 auto 6px; + width: 42px; + height: 42px; + overflow: hidden; + + .ricu-icon1-arrow { + display: block; + margin: 0 auto; + width: 0; + height: 0; + border-bottom: 14.7px solid rgba(0, 0, 0, 0.3); + border-left: 14.7px solid transparent; + border-right: 14.7px solid transparent; + } + + .ricu-icon1-body { + display: block; + width: 12.6px; + height: 14.7px; + margin: 0 auto; + background-color: rgba(0, 0, 0, 0.3); + } + + .ricu-icon1-bottom { + box-sizing: border-box; + display: block; + height: 12.6px; + border: 6px solid rgba(0, 0, 0, 0.3); + border-top: none; + } + } + + .ricu-hint { + display: block; + padding: 15px; + font-size: 14px; + color: #666; + line-height: 30px; + } + + .ricu-no-supported-hint { + display: block; + position: absolute; + top: 0; + left: 0; + padding: 30px; + width: 100%; + height: 60px; + line-height: 30px; + background-color: #eee; + text-align: center; + color: #666; + font-size: 14px; + } + } + } + + .step2 { + display: flex; + justify-content: space-around; + margin: 10px auto; + + .left { + position: relative; + width: 240px; + height: 180px; + overflow: hidden; + } + + .right { + display: flex; + flex: 1; + justify-content: space-around; + + img { + border: 1px solid rgba(0, 0, 0, 0.15); + } + + .text { + margin-top: 10px; + color: #bbb; + font-size: 14px; + text-align: center; + } + } + + .img_shade { + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18); + position: absolute; + background-color: rgba(241, 242, 243, 0.8); + } + + .img_shade_1 { + top: 0; + left: 0; + } + + .img_shade_2 { + bottom: 0; + right: 0; + } + } + + .operate { + position: absolute; + right: 20px; + bottom: 20px; + + button { + border: none; + position: relative; + float: left; + display: block; + margin-left: 10px; + width: 100px; + height: 36px; + line-height: 36px; + text-align: center; + cursor: pointer; + font-size: 14px; + color: #4a7; + border-radius: 2px; + overflow: hidden; + user-select: none; + outline: none; + + &:hover { + background-color: rgba(0, 0, 0, 0.03); + } + + &:focus, &:active { + border: none; + } + } + } + + .range { + position: relative; + margin: 30px 0 10px 0; + width: 240px; + height: 18px; + + .icon5, .icon6 { + position: absolute; + top: 0; + width: 18px; + height: 18px; + border-radius: 100%; + background-color: rgba(0, 0, 0, 0.08); + } + + .icon5:hover, .icon6:hover { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12); + cursor: pointer; + background-color: rgba(0, 0, 0, 0.14); + } + + .icon5 { + left: 0; + + &::before { + position: absolute; + content: ""; + display: block; + left: 3px; + top: 8px; + width: 12px; + height: 2px; + background-color: #fff; + } + } + + .icon6 { + right: 0; + + &::before { + position: absolute; + content: ""; + display: block; + left: 3px; + top: 8px; + width: 12px; + height: 2px; + background-color: #fff; + } + + &::after { + position: absolute; + content: ""; + display: block; + top: 3px; + left: 8px; + width: 2px; + height: 12px; + background-color: #fff; + } + } + + input[type="range"] { + display: block; + padding-top: 5px; + margin: 0 auto; + width: 180px; + height: 8px; + vertical-align: top; + background: transparent; + appearance: none; + cursor: pointer; + + &:focus { + outline: none; + } + + &::-webkit-slider-thumb { + -webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18); + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18); + -webkit-appearance: none; + appearance: none; + margin-top: -3px; + width: 12px; + height: 12px; + background-color: #61c091; + border-radius: 100%; + border: none; + -webkit-transition: 0.2s; + transition: 0.2s; + } + + &::-moz-range-thumb { + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18); + -moz-appearance: none; + appearance: none; + width: 12px; + height: 12px; + background-color: #61c091; + border-radius: 100%; + border: none; + -webkit-transition: 0.2s; + transition: 0.2s; + } + + &::-ms-thumb { + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18); + appearance: none; + width: 12px; + height: 12px; + background-color: #61c091; + border: none; + border-radius: 100%; + -webkit-transition: 0.2s; + transition: 0.2s; + } + + &:active::-moz-range-thumb { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + width: 14px; + height: 14px; + } + + &:active::-ms-thumb { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + width: 14px; + height: 14px; + } + + &:active::-webkit-slider-thumb { + -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23); + margin-top: -4px; + width: 14px; + height: 14px; + } + + &::-webkit-slider-runnable-track { + -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12); + width: 100%; + height: 6px; + cursor: pointer; + border-radius: 2px; + border: none; + background-color: rgba(68, 170, 119, 0.3); + } + + &::-moz-range-track { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12); + width: 100%; + height: 6px; + cursor: pointer; + border-radius: 2px; + border: none; + background-color: rgba(68, 170, 119, 0.3); + } + + &::-ms-track { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12); + width: 100%; + cursor: pointer; + background: transparent; + border-color: transparent; + color: transparent; + height: 6px; + border-radius: 2px; + border: none; + } + + &::-ms-fill-lower { + background-color: rgba(68, 170, 119, 0.3); + } + + &::-ms-fill-upper { + background-color: rgba(68, 170, 119, 0.15); + } + + &:focus::-webkit-slider-runnable-track { + background-color: rgba(68, 170, 119, 0.5); + } + + &:focus::-moz-range-track { + background-color: rgba(68, 170, 119, 0.5); + } + + &:focus::-ms-fill-lower { + background-color: rgba(68, 170, 119, 0.45); + } + + &:focus::-ms-fill-upper { + background-color: rgba(68, 170, 119, 0.25); + } + } + } + + .e-ripple { + position: absolute; + border-radius: 100%; + background-color: rgba(0, 0, 0, 0.15); + background-clip: padding-box; + pointer-events: none; + user-select: none; + transform: scale(0); + opacity: 1; + } + + .e-ripple.z-active { + opacity: 0; + transform: scale(2); + transition: opacity 1.2s ease-out, transform 0.6s ease-out, + -webkit-transform 0.6s ease-out; + } +} diff --git a/src/components/ImageCropUpload/index.jsx b/src/components/ImageCropUpload/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f34a4c544372ac801ed09bccb2adbb96c79f5c36 --- /dev/null +++ b/src/components/ImageCropUpload/index.jsx @@ -0,0 +1,186 @@ +import React, { Component } from 'react'; +import { Icon, Upload, Modal, message } from 'antd'; +import PropTypes from 'prop-types'; +import ReactImageCrop from './ReactImageCrop'; +import { data2blob } from '@/utils/common'; + +function getBase64(img, callback) { + const reader = new FileReader(); + reader.addEventListener('load', () => callback(reader.result)); + reader.readAsDataURL(img); +} + +class ReactImageCropUpload extends Component { + static defaultProps = { + // 上传接口地址,如果为空,图片不会上传 + url: '', + // 上传方法 + method: 'POST', + // 向服务器上传的文件名 + field: 'upload', + // 上传附带其他数据,格式 {k:v} + params: null, + // 上传header设置,格式 {k:v} + headers: null, + // 支持跨域 + withCredentials: false, + // 原名key,类似于id,触发事件会带上(如果一个页面多个图片上传控件,可以做区分 + ki: 0, + }; + + static propTypes = { + url: PropTypes.string, + method: PropTypes.string, + field: PropTypes.string, + params: PropTypes.object, + headers: PropTypes.object, + withCredentials: PropTypes.bool, + ki: PropTypes.number, + // handleCropUploadSuccess: PropTypes.func.isRequired, + // handleCropUploadFail: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.cropRef = React.createRef(); + this.state = { + visible: false, + } + } + + // 上传图片 + upload = ({ createImgUrl, blob, file }) => { + const child = this.cropRef.current; + const { url, params, headers, field, withCredentials, method, handleCropUploadSuccess, handleCropUploadFail, ki } = this.props; + const { imgFormat } = child.state; + const fmData = new FormData(); + fmData.append(field, data2blob(createImgUrl), field + "." + imgFormat); + + // 添加其他参数 + if (typeof params === 'object' && params) { + Object.keys(params).forEach(k => { + fmData.append(k, params[k]); + }); + } + + // 监听进度回调 + // const uploadProgress = function(event) { + // if (event.lengthComputable) { + // that.progress = 100 * Math.round(event.loaded) / event.total; + // } + // }; + // 上传文件 + // child.setStep(3); + new Promise(function(resolve, reject) { + let client = new XMLHttpRequest(); + client.open(method, url, true); + client.withCredentials = withCredentials; + client.onreadystatechange = function() { + if (this.readyState !== 4) { + return; + } + if (this.status === 200 || this.status === 201) { + resolve(JSON.parse(this.responseText)); + } else { + reject(this.status); + } + }; + // client.upload.addEventListener("progress", uploadProgress, false); //监听进度 + // 设置header + if (typeof headers === 'object' && headers) { + Object.keys(headers).forEach(k => { + client.setRequestHeader(k, headers[k]); + }); + } + client.send(fmData); + }).then( + // 上传成功 + function(resData) { + handleCropUploadSuccess(resData, field, ki); + }, + // 上传失败 + function(sts) { + handleCropUploadFail(sts, field, ki); + }, + ); + }; + + handleChange = info => { + if (info.file.status === 'uploading') { + this.setState({ loading: true }); + return; + } + if (info.file.status === 'done') { + // Get this url from response in real world. + getBase64(info.file.originFileObj, imageUrl => + this.setState({ + imageUrl, + loading: false, + }), + ); + } + }; + + beforeUpload = (file) => { + const imageType = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif']; + const isImage = imageType.findIndex(o => o === file.type) !== -1; + if (!isImage) { + message.error('请选择正确的图片类型!'); + return false; + } + const isLt2M = file.size / 1024 / 1024 < 2; + if (!isLt2M) { + message.error('图片大小不能超过2M!'); + return false; + } + + let reader = new FileReader(); + reader.readAsDataURL(file); + let _this = this; + reader.onload = (e) => { + _this.setState({ + cropSrc: e.target.result, + visible: true, + }) + } + return new Promise((resolve, reject) => { + let index = setInterval(() => { + if (this.blob) { // 监听裁剪是否完成 + window.clearInterval(index); + this.blob.uid = file.uid; // 需要给裁剪后的blob对象设置uid,否则会报错!!! + resolve(this.blob); // 执行后续的上传操作 + } + },300); + }); + }; + + render() { + const { imageUrl, visible } = this.state; + const uploadButton = ( +
+ +
Upload
+
+ ); + return ( +
+ + {imageUrl ? avatar : uploadButton} + + + + +
+ ); + } +} + +export default ReactImageCropUpload; diff --git a/src/components/ImagesPreview/index.jsx b/src/components/ImagesPreview/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9b21df046b827c0b5279b8165195955abd20b90e --- /dev/null +++ b/src/components/ImagesPreview/index.jsx @@ -0,0 +1,56 @@ +import React, { PureComponent } from 'react'; +import { PicturePreview } from 'ppfish'; +import styles from './index.less'; + +class ImagesPreview extends PureComponent { + constructor(props) { + super(props); + console.log('组件传值打印', this.props); + this.state = { + visible: false, + activeIndex: 0, + }; + } + + handleOpen = (index) => { + this.setState({ + visible: true, + activeIndex: index, + }); + }; + + handleClose = () => { + this.setState({ + visible: false, + }); + }; + + render() { + const { visible, activeIndex } = this.state; + return ( +
+
+
+ { + this.props.source && this.props.source.map((item, index) => +
+ {item.name} +
{item.name}
+
, + ) + } +
+
+ +
+ ); + } +} + +export default ImagesPreview; diff --git a/src/components/ImagesPreview/index.less b/src/components/ImagesPreview/index.less new file mode 100644 index 0000000000000000000000000000000000000000..830e429878aeb5f5668143e5ff9675c7cabb2429 --- /dev/null +++ b/src/components/ImagesPreview/index.less @@ -0,0 +1,24 @@ +.picpreview { + display: -ms-flexbox; + display: flex; + .pics { + display: -ms-flexbox; + display: flex; + -ms-flex: 1; + flex: 1; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-pack: center; + justify-content: start; + .item { + margin-left: 20px; + cursor: pointer; + } + .item:first-child { + margin-left: 0; + } + .name { + text-align: center; + } + } +} diff --git a/src/components/XqUpload/index.jsx b/src/components/XqUpload/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fc4414875ab2aca913013caafe5891341ab5ce4e --- /dev/null +++ b/src/components/XqUpload/index.jsx @@ -0,0 +1,134 @@ +import React, { Component } from 'react'; +import { AutoComplete, Icon, Input, Upload, message } from 'antd'; +import { PicturePreview } from 'ppfish'; +import styles from './index.less'; + +export default class XqUpload extends Component { + static defaultProps = { + defaultActiveFirstOption: false, + onPressEnter: () => {}, + onSearch: () => {}, + onChange: () => {}, + className: '', + placeholder: '', + dataSource: [], + defaultOpen: false, + onVisibleChange: () => {}, + }; + + static getDerivedStateFromProps(props) { + if ('open' in props) { + return { + searchMode: props.open, + }; + } + return null; + } + + constructor(props) { + super(props); + this.state = { + visible: false, + activeIndex: 0, + fileData: [ + { + uid: '-1', + name: 'image.png', + status: 'done', + url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + }, + { + uid: '-2', + name: 'image.png', + status: 'done', + url: 'https://nos.netease.com/ysf/3df2280d2319678a091138b0bbba82fe', + src: 'https://nos.netease.com/ysf/3df2280d2319678a091138b0bbba82fe', + }, + ], + }; + } + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + handlePreview = async file => { + const { fileData } = this.state; + this.setState({ + visible: true, + activeIndex: fileData.findIndex(item => item.uid === file.uid), + }); + }; + + handleClose = () => { + this.setState({ + visible: false, + }); + }; + + render() { + // const { className, placeholder, open, ...restProps } = this.props; + const { activeIndex, visible, fileData } = this.state; + const props = { + name: 'file', + multiple: true, + listType: 'picture-card', + fileList: fileData, + onPreview: this.handlePreview, + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + transformFile(file) { + return new Promise(resolve => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const canvas = document.createElement('canvas'); + const img = document.createElement('img'); + img.src = reader.result; + img.onload = () => { + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + ctx.fillStyle = 'red'; + ctx.textBaseline = 'middle'; + ctx.fillText('Ant Design', 20, 20); + canvas.toBlob(resolve); + }; + }; + }); + }, + onChange(info) { + console.log('info', info); + const { status } = info.file; + if (status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (status === 'done') { + message.success(`${info.file.name} file uploaded successfully.`); + } else if (status === 'error') { + message.error(`${info.file.name} file upload failed.`); + } + }, + }; + + const uploadButton = ( +
+ +
点击/拖拽
+
+ ); + return ( +
+ + { fileData.length >= 8 ? null : uploadButton } + + +
+ ); + } +} diff --git a/src/components/XqUpload/index.less b/src/components/XqUpload/index.less new file mode 100644 index 0000000000000000000000000000000000000000..8f40cc7f5a742d90bf1f2844f836c172089c1fad --- /dev/null +++ b/src/components/XqUpload/index.less @@ -0,0 +1,32 @@ +@import '~antd/es/style/themes/default.less'; + +.headerSearch { + :global(.anticon-search) { + font-size: 16px; + cursor: pointer; + } + .input { + width: 0; + background: transparent; + border-radius: 0; + transition: width 0.3s, margin-left 0.3s; + :global(.ant-select-selection) { + background: transparent; + } + input { + padding-right: 0; + padding-left: 0; + border: 0; + box-shadow: none !important; + } + &, + &:hover, + &:focus { + border-bottom: 1px solid @border-color-base; + } + &.show { + width: 210px; + margin-left: 8px; + } + } +} diff --git a/src/pages/demo/preview/index.jsx b/src/pages/demo/preview/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..da333908832ed6ed09c3cb806437b0fa7ae7b493 --- /dev/null +++ b/src/pages/demo/preview/index.jsx @@ -0,0 +1,77 @@ +import React, { Component } from 'react'; +import { Card, Button, Upload, Modal } from 'antd'; +import { PageHeaderWrapper } from '@ant-design/pro-layout'; +import { RichEditor } from 'ppfish'; +import ImagesPreview from '@/components/ImagesPreview'; +import XqUpload from '@/components/XqUpload'; +import ImageCropUpload from '@/components/ImageCropUpload'; +import styles from './style.less'; + +class Preview extends Component { + state = { + imgData: [ + { + id: 1, + name: '图片', + src: 'http://s6.sinaimg.cn/mw690/003tzcWugy70fFioNtre5&690', + }, + { + id: 2, + name: '图片2', + src: 'https://v.feituapp.com/attachment/images/6/2019/06/rMFlfimOLEIOzI5vFLT1O9vu5p191V.jpg', + }, + ], + visible: false, + }; + + handleClick() { + this.setState({ visible: true }); + } + + off() { + this.setState({ visible: false }); + } + + handleCropUploadSuccess(resData, field, ki) { + console.log('resData, field, ki===>>>>', resData, field, ki); + this.off() + } + + handleCropUploadFail(sts, field, ki) { + console.log('sts, field, ki===>>>>', sts, field, ki); + } + + render() { + const { imgData, visible } = this.state; + return ( + + + {/*
*/} + {/**/} + {/*
*/} + {/**/} + {/*
*/} + {/**/} + {/*
*/} + {/*
*/} + + + +
+ + + + + + + + + +
+ ); + } +} + +export default Preview; diff --git a/src/pages/demo/preview/style.less b/src/pages/demo/preview/style.less new file mode 100644 index 0000000000000000000000000000000000000000..006bfc24e0ae6cac017c0d63a7e2009cda3973fd --- /dev/null +++ b/src/pages/demo/preview/style.less @@ -0,0 +1,54 @@ +@import '~antd/es/style/themes/default.less'; + +.avatar { + width: 144px; + height: 144px; + margin-bottom: 12px; + overflow: hidden; + img { + width: 100%; + } +} + +.button_view { + width: 144px; + text-align: center; +} + +.main { + width: 368px; + margin: 0 auto; + @media screen and (max-width: @screen-sm) { + width: 95%; + } + + .icon { + margin-left: 16px; + color: rgba(0, 0, 0, 0.2); + font-size: 24px; + vertical-align: middle; + cursor: pointer; + transition: color 0.3s; + + &:hover { + color: @primary-color; + } + } + + .other { + margin-top: 24px; + line-height: 22px; + text-align: left; + + .register { + float: right; + } + } + + :global { + .antd-pro-login-submit { + width: 100%; + margin-top: 24px; + } + } +}