index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import baseComponent from '../helpers/baseComponent'
  2. import classNames from '../helpers/libs/classNames'
  3. import { chooseMedia, uploadFile } from '../helpers/hooks/useNativeAPI'
  4. baseComponent({
  5. properties: {
  6. prefixCls: {
  7. type: String,
  8. value: 'wux-upload',
  9. },
  10. max: {
  11. type: Number,
  12. value: -1,
  13. observer: 'updated',
  14. },
  15. count: {
  16. type: Number,
  17. value: 9,
  18. observer: 'updated',
  19. },
  20. defaultFileType: {
  21. type: String,
  22. value: 'image',
  23. },
  24. compressed: {
  25. type: Boolean,
  26. value: true,
  27. },
  28. maxDuration: {
  29. type: Number,
  30. value: 60,
  31. },
  32. camera: {
  33. type: String,
  34. value: 'back',
  35. },
  36. sizeType: {
  37. type: Array,
  38. value: ['original', 'compressed'],
  39. },
  40. sourceType: {
  41. type: Array,
  42. value: ['album', 'camera'],
  43. },
  44. url: {
  45. type: String,
  46. value: '',
  47. },
  48. name: {
  49. type: String,
  50. value: 'file',
  51. },
  52. header: {
  53. type: Object,
  54. value: {},
  55. },
  56. formData: {
  57. type: Object,
  58. value: {},
  59. },
  60. uploaded: {
  61. type: Boolean,
  62. value: true,
  63. },
  64. disabled: {
  65. type: Boolean,
  66. value: false,
  67. },
  68. progress: {
  69. type: Boolean,
  70. value: false,
  71. },
  72. listType: {
  73. type: String,
  74. value: 'text',
  75. },
  76. defaultFileList: {
  77. type: Array,
  78. value: [],
  79. },
  80. fileList: {
  81. type: Array,
  82. value: [],
  83. observer(newVal) {
  84. if (this.data.controlled) {
  85. this.setData({
  86. uploadFileList: newVal,
  87. })
  88. }
  89. },
  90. },
  91. controlled: {
  92. type: Boolean,
  93. value: false,
  94. },
  95. showUploadList: {
  96. type: Boolean,
  97. value: true,
  98. },
  99. showRemoveIcon: {
  100. type: Boolean,
  101. value: true,
  102. },
  103. },
  104. data: {
  105. uploadMax: -1,
  106. uploadCount: 9,
  107. uploadFileList: [],
  108. isVideo: false,
  109. },
  110. computed: {
  111. classes: ['prefixCls, disabled, listType', function(prefixCls, disabled, listType) {
  112. const wrap = classNames(prefixCls, {
  113. [`${prefixCls}--${listType}`]: listType,
  114. [`${prefixCls}--disabled`]: disabled,
  115. })
  116. const files = `${prefixCls}__files`
  117. const file = `${prefixCls}__file`
  118. const thumb = `${prefixCls}__thumb`
  119. const remove = `${prefixCls}__remove`
  120. const select = `${prefixCls}__select`
  121. const button = `${prefixCls}__button`
  122. return {
  123. wrap,
  124. files,
  125. file,
  126. thumb,
  127. remove,
  128. select,
  129. button,
  130. }
  131. }],
  132. },
  133. methods: {
  134. /**
  135. * 计算最多可以选择的图片张数
  136. */
  137. updated() {
  138. const { count, max } = this.data
  139. const { uploadMax, uploadCount } = this.calcValue(count, max)
  140. // 判断是否需要更新
  141. if (this.data.uploadMax !== uploadMax || this.data.uploadCount !== uploadCount) {
  142. this.setData({
  143. uploadMax,
  144. uploadCount,
  145. })
  146. }
  147. },
  148. /**
  149. * 计算最多可以选择的图片张数
  150. */
  151. calcValue(count, max) {
  152. const realCount = parseInt(count)
  153. const uploadMax = parseInt(max) > -1 ? parseInt(max) : -1
  154. let uploadCount = realCount
  155. // 限制总数时
  156. if (uploadMax !== -1 && uploadMax <= 9 && realCount > uploadMax) {
  157. uploadCount = uploadMax
  158. }
  159. return {
  160. uploadMax,
  161. uploadCount,
  162. }
  163. },
  164. /**
  165. * 从本地相册选择图片或使用相机拍照
  166. */
  167. onSelect() {
  168. const {
  169. uploadCount,
  170. uploadMax,
  171. sizeType,
  172. sourceType,
  173. uploaded,
  174. disabled,
  175. uploadFileList: fileList,
  176. isVideo,
  177. compressed,
  178. maxDuration,
  179. camera,
  180. } = this.data
  181. const { uploadCount: count } = this.calcValue(uploadCount, uploadMax - fileList.length)
  182. const success = (res) => {
  183. this.tempFilePaths = res.tempFiles.map((item) => ({ url: item.tempFilePath, uid: this.getUid() }))
  184. this.triggerEvent('before', {...res, fileList })
  185. // 判断是否取消默认的上传行为
  186. if (uploaded) {
  187. this.uploadFile()
  188. }
  189. }
  190. // disabled
  191. if (disabled) return
  192. // choose video
  193. if (isVideo) {
  194. chooseMedia({
  195. mediaType: ['video'],
  196. sourceType,
  197. compressed,
  198. maxDuration,
  199. camera,
  200. success,
  201. })
  202. return
  203. }
  204. // choose image
  205. chooseMedia({
  206. mediaType: ['image'],
  207. count,
  208. sizeType,
  209. sourceType,
  210. success,
  211. })
  212. },
  213. /**
  214. * 上传文件改变时的回调函数
  215. * @param {Object} info 文件信息
  216. */
  217. onChange(info = {}) {
  218. if (!this.data.controlled) {
  219. this.setData({
  220. uploadFileList: info.fileList,
  221. })
  222. }
  223. this.triggerEvent('change', info)
  224. },
  225. /**
  226. * 开始上传文件的回调函数
  227. * @param {Object} file 文件对象
  228. */
  229. onStart(file) {
  230. const targetItem = {
  231. ...file,
  232. status: 'uploading',
  233. }
  234. this.onChange({
  235. file: targetItem,
  236. fileList: [...this.data.uploadFileList, targetItem],
  237. })
  238. },
  239. /**
  240. * 上传文件成功时的回调函数
  241. * @param {Object} file 文件对象
  242. * @param {Object} res 请求响应对象
  243. */
  244. onSuccess(file, res) {
  245. const fileList = [...this.data.uploadFileList]
  246. const index = fileList.map((item) => item.uid).indexOf(file.uid)
  247. if (index !== -1) {
  248. const targetItem = {
  249. ...file,
  250. status: 'done',
  251. res,
  252. }
  253. const info = {
  254. file: targetItem,
  255. fileList,
  256. }
  257. // replace
  258. fileList.splice(index, 1, targetItem)
  259. this.triggerEvent('success', info)
  260. this.onChange(info)
  261. }
  262. },
  263. /**
  264. * 上传文件失败时的回调函数
  265. * @param {Object} file 文件对象
  266. * @param {Object} res 请求响应对象
  267. */
  268. onFail(file, res) {
  269. const fileList = [...this.data.uploadFileList]
  270. const index = fileList.map((item) => item.uid).indexOf(file.uid)
  271. if (index !== -1) {
  272. const targetItem = {
  273. ...file,
  274. status: 'error',
  275. res,
  276. }
  277. const info = {
  278. file: targetItem,
  279. fileList,
  280. }
  281. // replace
  282. fileList.splice(index, 1, targetItem)
  283. this.triggerEvent('fail', info)
  284. this.onChange(info)
  285. }
  286. },
  287. /**
  288. * 监听上传进度变化的回调函数
  289. * @param {Object} file 文件对象
  290. * @param {Object} res 请求响应对象
  291. */
  292. onProgress(file, res) {
  293. const fileList = [...this.data.uploadFileList]
  294. const index = fileList.map((item) => item.uid).indexOf(file.uid)
  295. if (index !== -1) {
  296. const targetItem = {
  297. ...file,
  298. progress: res.progress,
  299. res,
  300. }
  301. const info = {
  302. file: targetItem,
  303. fileList,
  304. }
  305. // replace
  306. fileList.splice(index, 1, targetItem)
  307. this.triggerEvent('progress', info)
  308. this.onChange(info)
  309. }
  310. },
  311. /**
  312. * 上传文件,支持多图递归上传
  313. */
  314. uploadFile() {
  315. if (!this.tempFilePaths.length) return
  316. const { url, name, header, formData, disabled, progress } = this.data
  317. const file = this.tempFilePaths.shift()
  318. const { uid, url: filePath } = file
  319. if (!url || !filePath || disabled) return
  320. this.onStart(file)
  321. this.uploadTask[uid] = uploadFile({
  322. url,
  323. filePath,
  324. name,
  325. header,
  326. formData,
  327. success: (res) => this.onSuccess(file, res),
  328. fail: (res) => this.onFail(file, res),
  329. complete: (res) => {
  330. delete this.uploadTask[uid]
  331. this.triggerEvent('complete', res)
  332. this.uploadFile()
  333. },
  334. })
  335. // 判断是否监听上传进度变化
  336. if (progress) {
  337. this.uploadTask[uid].onProgressUpdate((res) => this.onProgress(file, res))
  338. }
  339. },
  340. /**
  341. * 点击文件时的回调函数
  342. * @param {Object} e 参数对象
  343. */
  344. onPreview(e) {
  345. this.triggerEvent('preview', {...e.currentTarget.dataset, fileList: this.data.uploadFileList })
  346. },
  347. /**
  348. * 点击删除图标时的回调函数
  349. * @param {Object} e 参数对象
  350. */
  351. onRemove(e) {
  352. const { file } = e.currentTarget.dataset
  353. const fileList = [...this.data.uploadFileList]
  354. const index = fileList.map((item) => item.uid).indexOf(file.uid)
  355. if (index !== -1) {
  356. const targetItem = {
  357. ...file,
  358. status: 'remove',
  359. }
  360. const info = {
  361. file: targetItem,
  362. fileList,
  363. }
  364. // delete
  365. fileList.splice(index, 1)
  366. this.triggerEvent('remove', {...e.currentTarget.dataset, ...info })
  367. this.onChange(info)
  368. }
  369. },
  370. /**
  371. * 中断上传任务
  372. * @param {String} uid 文件唯一标识
  373. */
  374. abort(uid) {
  375. const { uploadTask } = this
  376. if (uid) {
  377. if (uploadTask[uid]) {
  378. uploadTask[uid].abort()
  379. delete uploadTask[uid]
  380. }
  381. } else {
  382. Object.keys(uploadTask).forEach((uid) => {
  383. if (uploadTask[uid]) {
  384. uploadTask[uid].abort()
  385. delete uploadTask[uid]
  386. }
  387. })
  388. }
  389. },
  390. },
  391. /**
  392. * 组件生命周期函数,在组件实例进入页面节点树时执行
  393. */
  394. created() {
  395. this.index = 0
  396. this.createdAt = Date.now()
  397. this.getUid = () => `wux-upload--${this.createdAt}-${++this.index}`
  398. this.uploadTask = {}
  399. this.tempFilePaths = []
  400. },
  401. /**
  402. * 组件生命周期函数,在组件实例进入页面节点树时执
  403. */
  404. attached() {
  405. const { defaultFileType, defaultFileList, fileList, controlled } = this.data
  406. const uploadFileList = controlled ? fileList : defaultFileList
  407. const isVideo = defaultFileType === 'video'
  408. this.setData({ uploadFileList, isVideo })
  409. },
  410. /**
  411. * 组件生命周期函数,在组件实例被从页面节点树移除时执行
  412. */
  413. detached() {
  414. this.abort()
  415. },
  416. })