预计目标



需要的功能
图片大小限制
event.preventDefault()const file = event.target.files[0]const fileName = file.nameconst fileSize = file.sizeconst fileExt = fileName.split('.').pop()const accept = '.jpg,.jpeg,.png'const maxSize = 15 * 1024 * 1024if (accept && accept.indexOf(fileExt) === -1) {return Toast.show('文件类型不支持')}if (fileSize > maxSize) {return Toast.show('文件体积超出上传限制')}
图片上传
try {Toast.loading()const data = await courseUploader(newFile)const fileUrl = `https://${data.Location}`this.setState({ imageList: [fileUrl] }, Toast.hide)} catch (error) {Toast.show('上传失败')} finally {this.isLoading = false}
图片压缩
// 图片压缩export const imageCompress = (file) => {return new Promise((resolve, reject) => {const reader = new FileReader()const image = new Image()reader.readAsDataURL(file)reader.onload = function(e) {image.src = e.target.resultimage.onerror = () => {return reject('图像加载失败')}image.onload = function() {const canvas = document.createElement('canvas')const context = canvas.getContext('2d')const originWidth = this.widthconst originHeight = this.heightconst maxWidth = 1000const maxHeight = 1000let targetWidth = originWidthlet targetHeight = originHeightif (originWidth > maxWidth || originHeight > maxHeight) {if (originWidth / originHeight > maxWidth / maxHeight) {targetWidth = maxWidthtargetHeight = Math.round(maxWidth * (originHeight / originWidth))} else {targetHeight = maxHeighttargetWidth = Math.round(maxHeight * (originWidth / originHeight))}}canvas.width = targetWidthcanvas.height = targetHeightcontext.clearRect(0, 0, targetWidth, targetHeight)context.drawImage(image, 0, 0, targetWidth, targetHeight)const dataUrl = canvas.toDataURL('image/jpeg', 0.92)return resolve(dataURLtoFile(dataUrl))}}})}
图片base64转file
// 图片base64转fileexport const dataURLtoFile = (dataurl, filename = 'file') => {let arr = dataurl.split(',')let mime = arr[0].match(/:(.*?);/)[1]let suffix = mime.split('/')[1]let bstr = atob(arr[1])let n = bstr.lengthlet u8arr = new Uint8Array(n)while (n--) {u8arr[n] = bstr.charCodeAt(n)}return new File([u8arr], `${filename}.${suffix}`, {type: mime})}
判断移动端图片是 竖图还是横图
Exif-js github文档
| orientation值 | 旋转角度 |
|---|---|
| 1 | 0° |
| 3 | 180° |
| 6 | 顺时针90° |
| 8 | 逆时针90° |
import EXIF from 'exif-js'// 只在移动端生效EXIF.getData(file, function() {const orient = EXIF.getTag(this, 'Orientation')if (orient === 6) {// 竖图// 做向右旋转90度度处理} else {// 不做特殊处理}})
canvas图片旋转
const canvas = document.createElement('canvas')const context = canvas.getContext('2d')...context.clearRect(0, 0, targetWidth, targetHeight) // 清空内容区...canvas.width = targetHeight // targetHeight 当前图片高度 把canvas重绘canvas.height = targetWidth // targetWidth 当前图片宽度 把canvas重绘context.translate(targetHeight / 2, targetWidth / 2) // 设置当前图的中心区域context.rotate(90 * Math.PI / 180) // 向右旋转90度context.drawImage(image, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)
完整代码
index.js
import { PureComponent, Fragment, createRef } from 'react'import DocumentTitle from 'react-document-title'import Textarea from 'react-textarea-autosize'import router from 'umi/router'import styled from 'styled-components'import classnames from 'classnames'import { courseUploader } from '@@/utils/cos'import { imageCompress } from '@@/utils/imageCrop'import Toast from '@@/components/Toast'import withStyled from '@@/styles/withStyled'import notrService from '@@/services/note'import wx from 'weixin-js-sdk'import iconCloseGray from '@@/assets/icons/icon-close-gray.png'import iconImg from '@@/assets/icons/icon-img.png'const NoteController = withStyled(styled.div`position: fixed;top: 0;left: 0;right: 0;z-index: 100;margin: 0 auto;padding: 9px 24px;display: flex;max-width: 480PX;align-items: center;justify-content: flex-end;width: 100%;background-color: #F9FAFC;`)const NoteChooseImage = withStyled(styled.div`margin: 0 24px 0 0;width: 24px;height: 24px;background-size: 100%;background-repeat: no-repeat;background-image: url(${iconImg});&.notouch {filter: opacity(0.5);}`)const InputForImage = withStyled(styled.input`display: none;`)const NotePublish = withStyled(styled.div`padding: 9px 20px;font-size: 14px;color: #222222;background-color: ${(props) => props.theme.primaryColor};border-radius: 16px;`)const ImageArea = withStyled(styled.div`padding: 0 24px 24px;display: flex;flex-wrap: wrap;width: 100%;height: auto;`)const ImageDisplay = withStyled(styled.div`position: relative;margin: 0 0 10px;width: 96px;height: 96px;background-position: center;background-size: 100%;background-repeat: no-repeat;background-image: url(${(props) => props.image});border-radius: 8px;&:nth-of-type(3n-1) {margin: 0 auto 10px;}`)const ImageDelete = withStyled(styled.div`position: absolute;top: 4PX;right: 4PX;width: 16PX;height: 16PX;background-size: 100%;background-repeat: no-repeat;background-image: url(${iconCloseGray});border-radius: 50%;`)const textareaStyle = {marginTop: 60, paddingLeft: 24, paddingRight: 24, paddingBottom: 24, width: '100%', fontSize: 16, color: '#475669', lineHeight: 2, border: 0, resize: 'none'}class CreatePage extends PureComponent {constructor(props) {super(props)const { match: { params: { uniqueId } } } = this.propsthis.uniqueId = uniqueIdthis.inputNode = createRef()this.isLoading = falsethis.state = {text: '',notouch: false,imageList: []}}action = {handleImageClick: () => {const { imageList } = this.stateif (imageList.length !== 0) {return Toast.show('只能上传一张图片')}this.inputNode.current.click()},handleImageChange: async (event) => {event.preventDefault()if (this.isLoading) {return false}const file = event.target.files[0]const fileName = file.nameconst fileSize = file.sizeconst fileExt = fileName.split('.').pop()const accept = '.jpg,.jpeg,.png'const maxSize = 15 * 1024 * 1024if (accept && accept.indexOf(fileExt) === -1) {return Toast.show('文件类型不支持')}if (fileSize > maxSize) {return Toast.show('文件体积超出上传限制')}this.isLoading = trueconst newFile = await imageCompress(file)try {Toast.loading()const data = await courseUploader(newFile)const fileUrl = `https://${data.Location}`this.setState({ imageList: [fileUrl], notouch: true }, Toast.hide)} catch (error) {Toast.show('上传失败')} finally {this.isLoading = false}},handleDelete: (index, event) => {event.stopPropagation()const imageList = [...this.state.imageList]imageList.splice(index, 1)this.setState({ imageList, notouch: false })},handleNoteChange: (e) => {this.setState({ text: e.target.value })},handleFetch: async () => {const len = this.state.text.lengthif (len >= 10 && len <= 1000) {try {Toast.loading()await notrService.writeNote(this.uniqueId, {noteContent: this.state.text,imageUrl: this.state.imageList})setTimeout(() => {Toast.show('笔记发表成功')const pathname = `/note/${this.uniqueId}`router.replace({pathname,query: {tabKey: 'mine'}})}, 500)} catch (error) {setTimeout(() => { Toast.show(error.message) }, 500)}} else {Toast.show('笔记内容字数错误,字数在10-1000内')}},previewImage: (event) => {event.stopPropagation()wx.previewImage({current: this.state.imageList[0], // 当前显示图片的http链接urls: this.state.imageList // 需要预览的图片http链接列表})}}render() {const { text, imageList, notouch } = this.statereturn (<DocumentTitle title='写笔记'><Fragment><NoteController><InputForImage ref={this.inputNode} type='file' accept='image/*' onClick={(e) => { e.target.value = '' }} onChange={this.action.handleImageChange} /><NoteChooseImage className={classnames({ notouch })} onClick={this.action.handleImageClick} /><NotePublish onClick={this.action.handleFetch}>发表</NotePublish></NoteController><Textarea minRows={5} placeholder='记录学习后的珍贵收获~' value={text} onChange={this.action.handleNoteChange} style={textareaStyle} />{imageList.length !== 0 && (<ImageArea>{imageList.map((item, index) => (<ImageDisplay image={item} key={index} onClick={this.action.previewImage}><ImageDelete onClick={(event) => this.action.handleDelete(index, event)} /></ImageDisplay>))}</ImageArea>)}</Fragment></DocumentTitle>)}}export default CreatePage
imageCrop.js
import { getClientWidth } from '@@/utils/dom'import EXIF from 'exif-js'const clientWidth = getClientWidth()const maxImageWidth = clientWidth >= 480 ? 480 : clientWidth// 浏览器是否支持 webp 图片格式export const isWebpSupport = () => {const dataUrl = document.createElement('canvas').toDataURL('image/webp')return dataUrl.indexOf('data:image/webp') === 0}// 根据屏幕分辨率获取长度const getImageLength = (length) => {const imageLength = Math.floor(Number(length) || 0) || maxImageWidthreturn window.devicePixelRatio * imageLength}// 腾讯数据万象图片链接拼接// 参考文档 https://cloud.tencent.com/document/product/460/6929export const fasterImageUrl = (imageUrl, options) => {if (!imageUrl || !String(imageUrl).startsWith('http')) {return imageUrl}const { width } = Object.assign({}, options)const imageWidth = getImageLength(width)const formatSuffix = isWebpSupport() ? '/format/webp' : ''const widthSuffix = `/w/${imageWidth}`return `${imageUrl}?imageView2/2${formatSuffix}${widthSuffix}`}// 获取分享链接小图标export const getShareImageUrl = (imageUrl) => {if (!imageUrl || !String(imageUrl).startsWith('http')) {return imageUrl}return `${imageUrl}?imageView2/2/w/200`}// 图片base64转fileexport const dataURLtoFile = (dataurl, filename = 'file') => {let arr = dataurl.split(',')let mime = arr[0].match(/:(.*?);/)[1]let suffix = mime.split('/')[1]let bstr = atob(arr[1])let n = bstr.lengthlet u8arr = new Uint8Array(n)while (n--) {u8arr[n] = bstr.charCodeAt(n)}return new File([u8arr], `${filename}.${suffix}`, {type: mime})}// 图片压缩 并 判断是否需要旋转export const imageCompress = (file) => {return new Promise((resolve, reject) => {const reader = new FileReader()const image = new Image()reader.readAsDataURL(file)reader.onload = function(e) {image.src = e.target.resultimage.onerror = () => {return reject('图像加载失败')}image.onload = function() {const canvas = document.createElement('canvas')const context = canvas.getContext('2d')const originWidth = this.widthconst originHeight = this.heightconst maxWidth = 1000const maxHeight = 1000let targetWidth = originWidthlet targetHeight = originHeightif (originWidth > maxWidth || originHeight > maxHeight) {if (originWidth / originHeight > maxWidth / maxHeight) {targetWidth = maxWidthtargetHeight = Math.round(maxWidth * (originHeight / originWidth))} else {targetHeight = maxHeighttargetWidth = Math.round(maxHeight * (originWidth / originHeight))}}context.clearRect(0, 0, targetWidth, targetHeight)// 图片翻转问题解决方案// 在移动端,手机拍照后图片会左转90度,下面的函数恢复旋转问题// orient = 6时候,是图片竖着拍的EXIF.getData(file, function() {const orient = EXIF.getTag(this, 'Orientation')if (orient === 6) {canvas.width = targetHeightcanvas.height = targetWidthcontext.translate(targetHeight / 2, targetWidth / 2)context.rotate(90 * Math.PI / 180)context.drawImage(image, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)} else {canvas.width = targetWidthcanvas.height = targetHeightcontext.drawImage(image, 0, 0, targetWidth, targetHeight)}const dataUrl = canvas.toDataURL('image/jpeg', 0.8)return resolve(dataURLtoFile(dataUrl))})}}})}
腾讯云存储照片 cos.js+utils.js
cos.js
import COS from 'cos-js-sdk-v5'
import UUID from 'uuid/v4'
import utilsService from '@@/services/utils'
// 创建认证实例
const courseInstance = new COS({
getAuthorization: async (options, callback) => {
const uploadKey = await utilsService.getUploadKey(options)
callback(uploadKey)
}
})
// 获取分片上传实例
const getUploader = (instance, region, bucket) => {
return (file) => {
const fileName = file.name
const fileExt = fileName.split('.').pop()
const fileHash = UUID()
const fileKey = fileName === fileExt ? `client/${fileHash}` : `client/${fileHash}.${fileExt}`
return new Promise((resolve, reject) => {
instance.sliceUploadFile({
Region: region,
Bucket: bucket,
Key: fileKey,
Body: file
}, (error, data) => {
if (error) {
return reject(error)
}
return resolve(data)
})
})
}
}
export const courseUploader = getUploader(courseInstance, 'ap-beijing', 'course-1252068037')
utils.js
import { courseRequest as Axios } from '@@/utils/axios'
class UtilsService {
async getUploadKey() {
const { data: { credentials, expiredTime } } = await Axios.get('/util/get-temporary-key', {
params: {
durationSeconds: 60 * 60
}
})
const { tmpSecretId, tmpSecretKey, sessionToken } = credentials
return {
TmpSecretId: tmpSecretId,
TmpSecretKey: tmpSecretKey,
XCosSecurityToken: sessionToken,
ExpiredTime: expiredTime
}
}
}
export default new UtilsService()
莫名的坑
- 在安卓上无法上传图片的问题,不能触发onChange,解决办法 修改accept为: accept=’image/*’
详细解释:如果在input上面添加了accept字段,并且设置了一些值(png,jpeg,jpg),会发现,有的安卓是触发不了onChange,因为在accept的时候发生了拦截,使input的值未发生改变,所以建议使用accept=’image/*’,在onChange里面写格式判断。
- 自动打开相机,而没有让用户选择相机或相册,解决办法 去掉 capture**=”camera”**
- 手机拍照,或者手机里面度竖图,发生左转90度的变化,就需要单独判断是不是竖图,然后向右旋转
