预计目标

image.pngimage.pngimage.png

需要的功能

图片大小限制

  1. event.preventDefault()
  2. const file = event.target.files[0]
  3. const fileName = file.name
  4. const fileSize = file.size
  5. const fileExt = fileName.split('.').pop()
  6. const accept = '.jpg,.jpeg,.png'
  7. const maxSize = 15 * 1024 * 1024
  8. if (accept && accept.indexOf(fileExt) === -1) {
  9. return Toast.show('文件类型不支持')
  10. }
  11. if (fileSize > maxSize) {
  12. return Toast.show('文件体积超出上传限制')
  13. }

图片上传

  1. try {
  2. Toast.loading()
  3. const data = await courseUploader(newFile)
  4. const fileUrl = `https://${data.Location}`
  5. this.setState({ imageList: [fileUrl] }, Toast.hide)
  6. } catch (error) {
  7. Toast.show('上传失败')
  8. } finally {
  9. this.isLoading = false
  10. }

图片压缩

  1. // 图片压缩
  2. export const imageCompress = (file) => {
  3. return new Promise((resolve, reject) => {
  4. const reader = new FileReader()
  5. const image = new Image()
  6. reader.readAsDataURL(file)
  7. reader.onload = function(e) {
  8. image.src = e.target.result
  9. image.onerror = () => {
  10. return reject('图像加载失败')
  11. }
  12. image.onload = function() {
  13. const canvas = document.createElement('canvas')
  14. const context = canvas.getContext('2d')
  15. const originWidth = this.width
  16. const originHeight = this.height
  17. const maxWidth = 1000
  18. const maxHeight = 1000
  19. let targetWidth = originWidth
  20. let targetHeight = originHeight
  21. if (originWidth > maxWidth || originHeight > maxHeight) {
  22. if (originWidth / originHeight > maxWidth / maxHeight) {
  23. targetWidth = maxWidth
  24. targetHeight = Math.round(maxWidth * (originHeight / originWidth))
  25. } else {
  26. targetHeight = maxHeight
  27. targetWidth = Math.round(maxHeight * (originWidth / originHeight))
  28. }
  29. }
  30. canvas.width = targetWidth
  31. canvas.height = targetHeight
  32. context.clearRect(0, 0, targetWidth, targetHeight)
  33. context.drawImage(image, 0, 0, targetWidth, targetHeight)
  34. const dataUrl = canvas.toDataURL('image/jpeg', 0.92)
  35. return resolve(dataURLtoFile(dataUrl))
  36. }
  37. }
  38. })
  39. }

图片base64转file

  1. // 图片base64转file
  2. export const dataURLtoFile = (dataurl, filename = 'file') => {
  3. let arr = dataurl.split(',')
  4. let mime = arr[0].match(/:(.*?);/)[1]
  5. let suffix = mime.split('/')[1]
  6. let bstr = atob(arr[1])
  7. let n = bstr.length
  8. let u8arr = new Uint8Array(n)
  9. while (n--) {
  10. u8arr[n] = bstr.charCodeAt(n)
  11. }
  12. return new File([u8arr], `${filename}.${suffix}`, {
  13. type: mime
  14. })
  15. }

判断移动端图片是 竖图还是横图
Exif-js github文档

orientation值 旋转角度
1
3 180°
6 顺时针90°
8 逆时针90°
  1. import EXIF from 'exif-js'
  2. // 只在移动端生效
  3. EXIF.getData(file, function() {
  4. const orient = EXIF.getTag(this, 'Orientation')
  5. if (orient === 6) {
  6. // 竖图
  7. // 做向右旋转90度度处理
  8. } else {
  9. // 不做特殊处理
  10. }
  11. })

canvas图片旋转

  1. const canvas = document.createElement('canvas')
  2. const context = canvas.getContext('2d')
  3. ...
  4. context.clearRect(0, 0, targetWidth, targetHeight) // 清空内容区
  5. ...
  6. canvas.width = targetHeight // targetHeight 当前图片高度 把canvas重绘
  7. canvas.height = targetWidth // targetWidth 当前图片宽度 把canvas重绘
  8. context.translate(targetHeight / 2, targetWidth / 2) // 设置当前图的中心区域
  9. context.rotate(90 * Math.PI / 180) // 向右旋转90度
  10. context.drawImage(image, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)

完整代码

index.js

  1. import { PureComponent, Fragment, createRef } from 'react'
  2. import DocumentTitle from 'react-document-title'
  3. import Textarea from 'react-textarea-autosize'
  4. import router from 'umi/router'
  5. import styled from 'styled-components'
  6. import classnames from 'classnames'
  7. import { courseUploader } from '@@/utils/cos'
  8. import { imageCompress } from '@@/utils/imageCrop'
  9. import Toast from '@@/components/Toast'
  10. import withStyled from '@@/styles/withStyled'
  11. import notrService from '@@/services/note'
  12. import wx from 'weixin-js-sdk'
  13. import iconCloseGray from '@@/assets/icons/icon-close-gray.png'
  14. import iconImg from '@@/assets/icons/icon-img.png'
  15. const NoteController = withStyled(styled.div`
  16. position: fixed;
  17. top: 0;
  18. left: 0;
  19. right: 0;
  20. z-index: 100;
  21. margin: 0 auto;
  22. padding: 9px 24px;
  23. display: flex;
  24. max-width: 480PX;
  25. align-items: center;
  26. justify-content: flex-end;
  27. width: 100%;
  28. background-color: #F9FAFC;
  29. `)
  30. const NoteChooseImage = withStyled(styled.div`
  31. margin: 0 24px 0 0;
  32. width: 24px;
  33. height: 24px;
  34. background-size: 100%;
  35. background-repeat: no-repeat;
  36. background-image: url(${iconImg});
  37. &.notouch {
  38. filter: opacity(0.5);
  39. }
  40. `)
  41. const InputForImage = withStyled(styled.input`
  42. display: none;
  43. `)
  44. const NotePublish = withStyled(styled.div`
  45. padding: 9px 20px;
  46. font-size: 14px;
  47. color: #222222;
  48. background-color: ${(props) => props.theme.primaryColor};
  49. border-radius: 16px;
  50. `)
  51. const ImageArea = withStyled(styled.div`
  52. padding: 0 24px 24px;
  53. display: flex;
  54. flex-wrap: wrap;
  55. width: 100%;
  56. height: auto;
  57. `)
  58. const ImageDisplay = withStyled(styled.div`
  59. position: relative;
  60. margin: 0 0 10px;
  61. width: 96px;
  62. height: 96px;
  63. background-position: center;
  64. background-size: 100%;
  65. background-repeat: no-repeat;
  66. background-image: url(${(props) => props.image});
  67. border-radius: 8px;
  68. &:nth-of-type(3n-1) {
  69. margin: 0 auto 10px;
  70. }
  71. `)
  72. const ImageDelete = withStyled(styled.div`
  73. position: absolute;
  74. top: 4PX;
  75. right: 4PX;
  76. width: 16PX;
  77. height: 16PX;
  78. background-size: 100%;
  79. background-repeat: no-repeat;
  80. background-image: url(${iconCloseGray});
  81. border-radius: 50%;
  82. `)
  83. const textareaStyle = {
  84. marginTop: 60, paddingLeft: 24, paddingRight: 24, paddingBottom: 24, width: '100%', fontSize: 16, color: '#475669', lineHeight: 2, border: 0, resize: 'none'
  85. }
  86. class CreatePage extends PureComponent {
  87. constructor(props) {
  88. super(props)
  89. const { match: { params: { uniqueId } } } = this.props
  90. this.uniqueId = uniqueId
  91. this.inputNode = createRef()
  92. this.isLoading = false
  93. this.state = {
  94. text: '',
  95. notouch: false,
  96. imageList: []
  97. }
  98. }
  99. action = {
  100. handleImageClick: () => {
  101. const { imageList } = this.state
  102. if (imageList.length !== 0) {
  103. return Toast.show('只能上传一张图片')
  104. }
  105. this.inputNode.current.click()
  106. },
  107. handleImageChange: async (event) => {
  108. event.preventDefault()
  109. if (this.isLoading) {
  110. return false
  111. }
  112. const file = event.target.files[0]
  113. const fileName = file.name
  114. const fileSize = file.size
  115. const fileExt = fileName.split('.').pop()
  116. const accept = '.jpg,.jpeg,.png'
  117. const maxSize = 15 * 1024 * 1024
  118. if (accept && accept.indexOf(fileExt) === -1) {
  119. return Toast.show('文件类型不支持')
  120. }
  121. if (fileSize > maxSize) {
  122. return Toast.show('文件体积超出上传限制')
  123. }
  124. this.isLoading = true
  125. const newFile = await imageCompress(file)
  126. try {
  127. Toast.loading()
  128. const data = await courseUploader(newFile)
  129. const fileUrl = `https://${data.Location}`
  130. this.setState({ imageList: [fileUrl], notouch: true }, Toast.hide)
  131. } catch (error) {
  132. Toast.show('上传失败')
  133. } finally {
  134. this.isLoading = false
  135. }
  136. },
  137. handleDelete: (index, event) => {
  138. event.stopPropagation()
  139. const imageList = [...this.state.imageList]
  140. imageList.splice(index, 1)
  141. this.setState({ imageList, notouch: false })
  142. },
  143. handleNoteChange: (e) => {
  144. this.setState({ text: e.target.value })
  145. },
  146. handleFetch: async () => {
  147. const len = this.state.text.length
  148. if (len >= 10 && len <= 1000) {
  149. try {
  150. Toast.loading()
  151. await notrService.writeNote(this.uniqueId, {
  152. noteContent: this.state.text,
  153. imageUrl: this.state.imageList
  154. })
  155. setTimeout(() => {
  156. Toast.show('笔记发表成功')
  157. const pathname = `/note/${this.uniqueId}`
  158. router.replace({
  159. pathname,
  160. query: {
  161. tabKey: 'mine'
  162. }
  163. })
  164. }, 500)
  165. } catch (error) {
  166. setTimeout(() => { Toast.show(error.message) }, 500)
  167. }
  168. } else {
  169. Toast.show('笔记内容字数错误,字数在10-1000内')
  170. }
  171. },
  172. previewImage: (event) => {
  173. event.stopPropagation()
  174. wx.previewImage({
  175. current: this.state.imageList[0], // 当前显示图片的http链接
  176. urls: this.state.imageList // 需要预览的图片http链接列表
  177. })
  178. }
  179. }
  180. render() {
  181. const { text, imageList, notouch } = this.state
  182. return (
  183. <DocumentTitle title='写笔记'>
  184. <Fragment>
  185. <NoteController>
  186. <InputForImage ref={this.inputNode} type='file' accept='image/*' onClick={(e) => { e.target.value = '' }} onChange={this.action.handleImageChange} />
  187. <NoteChooseImage className={classnames({ notouch })} onClick={this.action.handleImageClick} />
  188. <NotePublish onClick={this.action.handleFetch}>发表</NotePublish>
  189. </NoteController>
  190. <Textarea minRows={5} placeholder='记录学习后的珍贵收获~' value={text} onChange={this.action.handleNoteChange} style={textareaStyle} />
  191. {imageList.length !== 0 && (
  192. <ImageArea>
  193. {imageList.map((item, index) => (
  194. <ImageDisplay image={item} key={index} onClick={this.action.previewImage}>
  195. <ImageDelete onClick={(event) => this.action.handleDelete(index, event)} />
  196. </ImageDisplay>
  197. ))}
  198. </ImageArea>
  199. )}
  200. </Fragment>
  201. </DocumentTitle>
  202. )
  203. }
  204. }
  205. export default CreatePage

imageCrop.js

  1. import { getClientWidth } from '@@/utils/dom'
  2. import EXIF from 'exif-js'
  3. const clientWidth = getClientWidth()
  4. const maxImageWidth = clientWidth >= 480 ? 480 : clientWidth
  5. // 浏览器是否支持 webp 图片格式
  6. export const isWebpSupport = () => {
  7. const dataUrl = document.createElement('canvas').toDataURL('image/webp')
  8. return dataUrl.indexOf('data:image/webp') === 0
  9. }
  10. // 根据屏幕分辨率获取长度
  11. const getImageLength = (length) => {
  12. const imageLength = Math.floor(Number(length) || 0) || maxImageWidth
  13. return window.devicePixelRatio * imageLength
  14. }
  15. // 腾讯数据万象图片链接拼接
  16. // 参考文档 https://cloud.tencent.com/document/product/460/6929
  17. export const fasterImageUrl = (imageUrl, options) => {
  18. if (!imageUrl || !String(imageUrl).startsWith('http')) {
  19. return imageUrl
  20. }
  21. const { width } = Object.assign({}, options)
  22. const imageWidth = getImageLength(width)
  23. const formatSuffix = isWebpSupport() ? '/format/webp' : ''
  24. const widthSuffix = `/w/${imageWidth}`
  25. return `${imageUrl}?imageView2/2${formatSuffix}${widthSuffix}`
  26. }
  27. // 获取分享链接小图标
  28. export const getShareImageUrl = (imageUrl) => {
  29. if (!imageUrl || !String(imageUrl).startsWith('http')) {
  30. return imageUrl
  31. }
  32. return `${imageUrl}?imageView2/2/w/200`
  33. }
  34. // 图片base64转file
  35. export const dataURLtoFile = (dataurl, filename = 'file') => {
  36. let arr = dataurl.split(',')
  37. let mime = arr[0].match(/:(.*?);/)[1]
  38. let suffix = mime.split('/')[1]
  39. let bstr = atob(arr[1])
  40. let n = bstr.length
  41. let u8arr = new Uint8Array(n)
  42. while (n--) {
  43. u8arr[n] = bstr.charCodeAt(n)
  44. }
  45. return new File([u8arr], `${filename}.${suffix}`, {
  46. type: mime
  47. })
  48. }
  49. // 图片压缩 并 判断是否需要旋转
  50. export const imageCompress = (file) => {
  51. return new Promise((resolve, reject) => {
  52. const reader = new FileReader()
  53. const image = new Image()
  54. reader.readAsDataURL(file)
  55. reader.onload = function(e) {
  56. image.src = e.target.result
  57. image.onerror = () => {
  58. return reject('图像加载失败')
  59. }
  60. image.onload = function() {
  61. const canvas = document.createElement('canvas')
  62. const context = canvas.getContext('2d')
  63. const originWidth = this.width
  64. const originHeight = this.height
  65. const maxWidth = 1000
  66. const maxHeight = 1000
  67. let targetWidth = originWidth
  68. let targetHeight = originHeight
  69. if (originWidth > maxWidth || originHeight > maxHeight) {
  70. if (originWidth / originHeight > maxWidth / maxHeight) {
  71. targetWidth = maxWidth
  72. targetHeight = Math.round(maxWidth * (originHeight / originWidth))
  73. } else {
  74. targetHeight = maxHeight
  75. targetWidth = Math.round(maxHeight * (originWidth / originHeight))
  76. }
  77. }
  78. context.clearRect(0, 0, targetWidth, targetHeight)
  79. // 图片翻转问题解决方案
  80. // 在移动端,手机拍照后图片会左转90度,下面的函数恢复旋转问题
  81. // orient = 6时候,是图片竖着拍的
  82. EXIF.getData(file, function() {
  83. const orient = EXIF.getTag(this, 'Orientation')
  84. if (orient === 6) {
  85. canvas.width = targetHeight
  86. canvas.height = targetWidth
  87. context.translate(targetHeight / 2, targetWidth / 2)
  88. context.rotate(90 * Math.PI / 180)
  89. context.drawImage(image, -targetWidth / 2, -targetHeight / 2, targetWidth, targetHeight)
  90. } else {
  91. canvas.width = targetWidth
  92. canvas.height = targetHeight
  93. context.drawImage(image, 0, 0, targetWidth, targetHeight)
  94. }
  95. const dataUrl = canvas.toDataURL('image/jpeg', 0.8)
  96. return resolve(dataURLtoFile(dataUrl))
  97. })
  98. }
  99. }
  100. })
  101. }

腾讯云存储照片 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()

莫名的坑

  1. 在安卓上无法上传图片的问题,不能触发onChange,解决办法 修改accept为: accept=’image/*’

详细解释:如果在input上面添加了accept字段,并且设置了一些值(png,jpeg,jpg),会发现,有的安卓是触发不了onChange,因为在accept的时候发生了拦截,使input的值未发生改变,所以建议使用accept=’image/*’,在onChange里面写格式判断。

  1. 自动打开相机,而没有让用户选择相机或相册,解决办法 去掉 capture**=”camera”**
  2. 手机拍照,或者手机里面度竖图,发生左转90度的变化,就需要单独判断是不是竖图,然后向右旋转