预计目标
需要的功能
图片大小限制
event.preventDefault()
const file = event.target.files[0]
const fileName = file.name
const fileSize = file.size
const fileExt = fileName.split('.').pop()
const accept = '.jpg,.jpeg,.png'
const maxSize = 15 * 1024 * 1024
if (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.result
image.onerror = () => {
return reject('图像加载失败')
}
image.onload = function() {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const originWidth = this.width
const originHeight = this.height
const maxWidth = 1000
const maxHeight = 1000
let targetWidth = originWidth
let targetHeight = originHeight
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
targetWidth = maxWidth
targetHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
targetHeight = maxHeight
targetWidth = Math.round(maxHeight * (originWidth / originHeight))
}
}
canvas.width = targetWidth
canvas.height = targetHeight
context.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转file
export 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.length
let 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.props
this.uniqueId = uniqueId
this.inputNode = createRef()
this.isLoading = false
this.state = {
text: '',
notouch: false,
imageList: []
}
}
action = {
handleImageClick: () => {
const { imageList } = this.state
if (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.name
const fileSize = file.size
const fileExt = fileName.split('.').pop()
const accept = '.jpg,.jpeg,.png'
const maxSize = 15 * 1024 * 1024
if (accept && accept.indexOf(fileExt) === -1) {
return Toast.show('文件类型不支持')
}
if (fileSize > maxSize) {
return Toast.show('文件体积超出上传限制')
}
this.isLoading = true
const 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.length
if (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.state
return (
<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) || maxImageWidth
return window.devicePixelRatio * imageLength
}
// 腾讯数据万象图片链接拼接
// 参考文档 https://cloud.tencent.com/document/product/460/6929
export 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转file
export 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.length
let 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.result
image.onerror = () => {
return reject('图像加载失败')
}
image.onload = function() {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const originWidth = this.width
const originHeight = this.height
const maxWidth = 1000
const maxHeight = 1000
let targetWidth = originWidth
let targetHeight = originHeight
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
targetWidth = maxWidth
targetHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
targetHeight = maxHeight
targetWidth = 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 = targetHeight
canvas.height = targetWidth
context.translate(targetHeight / 2, targetWidth / 2)
context.rotate(90 * Math.PI / 180)
context.drawImage(image, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)
} else {
canvas.width = targetWidth
canvas.height = targetHeight
context.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度的变化,就需要单独判断是不是竖图,然后向右旋转