uni-file-picker.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <template>
  2. <view class="uni-file-picker">
  3. <view v-if="title" class="uni-file-picker__header">
  4. <text class="file-title">{{ title }}</text>
  5. <text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
  6. </view>
  7. <upload-image
  8. v-if="fileMediatype === 'image' && showType === 'grid'"
  9. :readonly="readonly"
  10. :image-styles="imageStyles"
  11. :files-list="filesList"
  12. :limit="limitLength"
  13. :disablePreview="disablePreview"
  14. :delIcon="delIcon"
  15. @uploadFiles="uploadFiles"
  16. @choose="choose"
  17. @delFile="delFile"
  18. >
  19. <slot>
  20. <view class="is-add">
  21. <view class="icon-add"></view>
  22. <view class="icon-add rotate"></view>
  23. </view>
  24. </slot>
  25. </upload-image>
  26. <upload-file
  27. v-if="fileMediatype !== 'image' || showType !== 'grid'"
  28. :readonly="readonly"
  29. :list-styles="listStyles"
  30. :files-list="filesList"
  31. :showType="showType"
  32. :delIcon="delIcon"
  33. @uploadFiles="uploadFiles"
  34. @choose="choose"
  35. @delFile="delFile"
  36. >
  37. <slot>
  38. <view class="title">
  39. <view class="select">请选择pdf格式文件上传</view>
  40. <image class="black" src="/static/img/black.png" mode=""></image>
  41. </view>
  42. </slot>
  43. </upload-file>
  44. </view>
  45. </template>
  46. <script>
  47. import {chooseAndUploadFile} from './choose-and-upload-file.js'
  48. import uploadImage from './upload-image.vue'
  49. import uploadFile from './upload-file.vue'
  50. let fileInput = null
  51. /**
  52. * FilePicker 文件选择上传
  53. * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
  54. * @tutorial https://ext.dcloud.net.cn/plugin?id=4079
  55. * @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定
  56. * @property {Boolean} disabled=[true|false] 组件禁用
  57. * @value true 禁用
  58. * @value false 取消禁用
  59. * @property {Boolean} readonly=[true|false] 组件只读,不可选择,不显示进度,不显示删除按钮
  60. * @value true 只读
  61. * @value false 取消只读
  62. * @property {String} return-type=[array|object] 限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖
  63. * @value array 规定 value 属性的类型为数组
  64. * @value object 规定 value 属性的类型为对象
  65. * @property {Boolean} disable-preview=[true|false] 禁用图片预览,仅 mode:grid 时生效
  66. * @value true 禁用图片预览
  67. * @value false 取消禁用图片预览
  68. * @property {Boolean} del-icon=[true|false] 是否显示删除按钮
  69. * @value true 显示删除按钮
  70. * @value false 不显示删除按钮
  71. * @property {Boolean} auto-upload=[true|false] 是否自动上传,值为true则只触发@select,可自行上传
  72. * @value true 自动上传
  73. * @value false 取消自动上传
  74. * @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分
  75. * @property {String} title 组件标题,右侧显示上传计数
  76. * @property {String} mode=[list|grid] 选择文件后的文件列表样式
  77. * @value list 列表显示
  78. * @value grid 宫格显示
  79. * @property {String} file-mediatype=[image|video|all] 选择文件类型
  80. * @value image 只选择图片
  81. * @value video 只选择视频
  82. * @value all 选择所有文件
  83. * @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同
  84. * @property {Object} list-style mode:list 时的样式
  85. * @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同
  86. * @event {Function} select 选择文件后触发
  87. * @event {Function} progress 文件上传时触发
  88. * @event {Function} success 上传成功触发
  89. * @event {Function} fail 上传失败触发
  90. * @event {Function} delete 文件从列表移除时触发
  91. */
  92. export default {
  93. name: 'uniFilePicker',
  94. components: {
  95. uploadImage,
  96. uploadFile
  97. },
  98. props: {
  99. value: {
  100. type: [Array, Object],
  101. default() {
  102. return []
  103. }
  104. },
  105. disabled: {
  106. type: Boolean,
  107. default: false
  108. },
  109. disablePreview: {
  110. type: Boolean,
  111. default: false
  112. },
  113. delIcon: {
  114. type: Boolean,
  115. default: true
  116. },
  117. // 自动上传
  118. autoUpload: {
  119. type: Boolean,
  120. default: true
  121. },
  122. // 最大选择个数 ,h5只能限制单选或是多选
  123. limit: {
  124. type: [Number, String],
  125. default: 9
  126. },
  127. // 列表样式 grid | list | list-card
  128. mode: {
  129. type: String,
  130. default: 'grid'
  131. },
  132. // inputUrl: {
  133. // type: Boolean,
  134. // default: false
  135. // },
  136. // 选择文件类型 image/video/all
  137. fileMediatype: {
  138. type: String,
  139. default: 'image'
  140. },
  141. // 文件类型筛选
  142. fileExtname: {
  143. type: [Array, String],
  144. default() {
  145. return []
  146. }
  147. },
  148. title: {
  149. type: String,
  150. default: ''
  151. },
  152. listStyles: {
  153. type: Object,
  154. default() {
  155. return {
  156. // 是否显示边框
  157. border: true,
  158. // 是否显示分隔线
  159. dividline: true,
  160. // 线条样式
  161. borderStyle: {}
  162. }
  163. }
  164. },
  165. imageStyles: {
  166. type: Object,
  167. default() {
  168. return {
  169. width: 'auto',
  170. height: 'auto'
  171. }
  172. }
  173. },
  174. readonly: {
  175. type: Boolean,
  176. default: false
  177. },
  178. returnType: {
  179. type: String,
  180. default: 'array'
  181. }
  182. },
  183. watch: {
  184. value: {
  185. handler(newVal) {
  186. let newFils = []
  187. let newData = [].concat(newVal || [])
  188. newData.forEach(v => {
  189. const files = this.files.find(i => i.url === v.url)
  190. const reg = /cloud:\/\/([\w.]+\/?)\S*/
  191. if (!v.path) {
  192. v.path = v.url
  193. }
  194. if (reg.test(v.url)) {
  195. this.getTempFileURL(v, v.url)
  196. }
  197. newFils.push(files ? files : v)
  198. })
  199. let data = null
  200. if (this.returnType === 'object') {
  201. data = this.backObject(newFils)[0]
  202. } else {
  203. data = this.backObject(newFils)
  204. }
  205. this.formItem && this.formItem.setValue(data)
  206. this.files = newFils
  207. },
  208. immediate: true
  209. }
  210. },
  211. data() {
  212. return {
  213. files: [],
  214. }
  215. },
  216. computed: {
  217. filesList() {
  218. let files = []
  219. this.files.forEach(v => {
  220. files.push(v)
  221. })
  222. return files
  223. },
  224. showType() {
  225. if (this.fileMediatype === 'image') {
  226. return this.mode
  227. }
  228. return 'list'
  229. },
  230. limitLength() {
  231. if (this.returnType === 'object') {
  232. return 1
  233. }
  234. if (!this.limit) {
  235. return 1
  236. }
  237. if (this.limit >= 9) {
  238. return 9
  239. }
  240. return this.limit
  241. },
  242. extname(){
  243. if (!Array.isArray(this.fileExtname)) {
  244. let extname = this.fileExtname.replace(/(\[|\])/g,'')
  245. return extname.split(',')
  246. } else {
  247. return this.fileExtname
  248. }
  249. return []
  250. }
  251. },
  252. created() {
  253. // TODO 兼容不开通服务空间的情况
  254. if(!(uniCloud.config && uniCloud.config.provider)){
  255. this.noSpace = true
  256. uniCloud.chooseAndUploadFile = chooseAndUploadFile
  257. }
  258. this.tempData = {}
  259. this.form = this.getForm('uniForms')
  260. this.formItem = this.getForm('uniFormsItem')
  261. if (this.form && this.formItem) {
  262. if (this.formItem.name) {
  263. this.rename = this.formItem.name
  264. this.form.inputChildrens.push(this)
  265. }
  266. }
  267. },
  268. methods: {
  269. /**
  270. * 获取父元素实例
  271. */
  272. getForm(name = 'uniForms') {
  273. let parent = this.$parent;
  274. let parentName = parent.$options.name;
  275. while (parentName !== name) {
  276. parent = parent.$parent;
  277. if (!parent) return false;
  278. parentName = parent.$options.name;
  279. }
  280. return parent;
  281. },
  282. /**
  283. * 继续上传
  284. */
  285. upload() {
  286. // TODO 先放弃这个实现 ,不能全部上传
  287. // if (this.$uploadFiles) {
  288. // this.$uploadFiles()
  289. // } else {
  290. // uni.showToast({
  291. // title: '请先选择文件',
  292. // icon: 'none'
  293. // })
  294. // }
  295. let files = []
  296. this.files.forEach((v, index) => {
  297. if (v.status === 'ready' || v.status === 'error') {
  298. files.push(Object.assign({}, v))
  299. }
  300. })
  301. this.uploadFiles(files)
  302. },
  303. /**
  304. * 选择文件
  305. */
  306. choose() {
  307. if (this.disabled) return
  308. if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType === 'array') {
  309. uni.showToast({
  310. title: `您最多选择 ${this.limitLength} 个文件`,
  311. icon: 'none'
  312. })
  313. return
  314. }
  315. // uni.showActionSheet({
  316. // itemList: ['填写 url 地址', '选择文件'],
  317. // success: (res) => {
  318. // if (res.tapIndex === 1) {
  319. // this.chooseFiles()
  320. // }
  321. // },
  322. // fail: function(res) {}
  323. // });
  324. this.chooseFiles()
  325. },
  326. /**
  327. * 选择文件并上传
  328. */
  329. chooseFiles() {
  330. uniCloud
  331. .chooseAndUploadFile({
  332. type: this.fileMediatype,
  333. compressed: false,
  334. // TODO 如果为空,video 有问题
  335. extension: this.extname.length > 0 ? this.extname : undefined,
  336. count: this.limitLength - this.files.length, //默认9
  337. onChooseFile: async res => {
  338. if ((Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) || this.returnType === 'object') {
  339. this.files = []
  340. }
  341. let filePaths = []
  342. let files = []
  343. if (this.extname && this.extname.length > 0) {
  344. res.tempFiles.forEach(v => {
  345. let fileFullName = this.getFileExt(v.name)
  346. const extname = fileFullName.ext.toLowerCase()
  347. if (this.extname.indexOf(extname) !== -1) {
  348. files.push(v)
  349. filePaths.push(v.path)
  350. }
  351. })
  352. if (files.length !== res.tempFiles.length) {
  353. uni.showToast({
  354. title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`,
  355. icon: 'none',
  356. duration: 5000
  357. })
  358. }
  359. } else {
  360. filePaths = res.tempFilePaths
  361. files = res.tempFiles
  362. }
  363. let currentData = []
  364. for (let i = 0; i < files.length; i++) {
  365. if (this.limitLength - this.files.length <= 0) break
  366. files[i].uuid = Date.now()
  367. let filedata = await this.getFileData(files[i], this.fileMediatype)
  368. filedata.file = files[i]
  369. filedata.progress = 0
  370. filedata.status = 'ready'
  371. this.files.push(filedata)
  372. currentData.push(filedata)
  373. }
  374. this.$emit('select', {
  375. tempFiles: currentData,
  376. tempFilePaths: filePaths
  377. })
  378. res.tempFiles = files
  379. // 停止自动上传
  380. if (!this.autoUpload || this.noSpace) {
  381. res.tempFiles = []
  382. // TODO 先放弃这个实现 ,不能全部上传
  383. // return new Promise((resolve) => {
  384. // this.$uploadFiles = () => {
  385. // // this._is_uploading = true
  386. // resolve(res)
  387. // }
  388. // })
  389. } else {
  390. // this.upload()
  391. }
  392. },
  393. onUploadProgress: progressEvent => {
  394. this.setProgress(progressEvent, progressEvent.index)
  395. }
  396. })
  397. .then(result => {
  398. console.log(2)
  399. this.upload()
  400. // this.setSuccessAndError(result.tempFiles)
  401. })
  402. .catch(err => {
  403. console.log('选择失败', err)
  404. })
  405. },
  406. /**
  407. * 批传
  408. * @param {Object} e
  409. */
  410. uploadFiles(files) {
  411. files = [].concat(files)
  412. this.uploadCloudFiles(files, 5, res => {
  413. this.setProgress(res, res.index, true)
  414. })
  415. .then(result => {
  416. this.setSuccessAndError(result)
  417. })
  418. .catch(err => {
  419. console.log('err', err)
  420. })
  421. },
  422. /**
  423. * 成功或失败
  424. */
  425. async setSuccessAndError(res, fn) {
  426. let successData = []
  427. let errorData = []
  428. let tempFilePath = []
  429. let errorTempFilePath = []
  430. for (let i = 0; i < res.length; i++) {
  431. // const index = item.index
  432. const item = res[i]
  433. const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index
  434. if (index === -1 || !this.files) break
  435. if (item.errMsg === 'request:fail') {
  436. this.files[index].url = item.path
  437. this.files[index].status = 'error'
  438. this.files[index].errMsg = item.errMsg
  439. // this.files[index].progress = -1
  440. errorData.push(this.files[index])
  441. errorTempFilePath.push(this.files[index].url)
  442. } else {
  443. this.files[index].errMsg = ''
  444. this.files[index].url = item.url
  445. this.files[index].status = 'success'
  446. this.files[index].progress += 1
  447. successData.push(this.files[index])
  448. tempFilePath.push(this.files[index].url)
  449. }
  450. }
  451. if (successData.length > 0) {
  452. this.setEmit()
  453. // 状态改变返回
  454. this.$emit('success', {
  455. tempFiles: this.backObject(successData),
  456. tempFilePaths: tempFilePath
  457. })
  458. }
  459. if (errorData.length > 0) {
  460. this.$emit('fail', {
  461. tempFiles: this.backObject(errorData),
  462. tempFilePaths: errorTempFilePath
  463. })
  464. }
  465. },
  466. /**
  467. * 获取进度
  468. * @param {Object} progressEvent
  469. * @param {Object} index
  470. * @param {Object} type
  471. */
  472. setProgress(progressEvent, index, type) {
  473. const fileLenth = this.files.length
  474. const percentNum = (index / fileLenth) * 100
  475. const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
  476. let idx = index
  477. if (!type) {
  478. idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid)
  479. }
  480. if (idx === -1 || !this.files[idx]) return
  481. // fix by mehaotian 100 就会消失,-1 是为了让进度条消失
  482. this.files[idx].progress = percentCompleted - 1
  483. // 上传中
  484. this.$emit('progress', {
  485. index: idx,
  486. progress: parseInt(percentCompleted),
  487. tempFile: this.files[idx]
  488. })
  489. },
  490. /**
  491. * 删除
  492. * @param {Object} index
  493. */
  494. delFile(index) {
  495. this.$emit('delete', {
  496. tempFile: this.files[index],
  497. tempFilePath: this.files[index].url
  498. })
  499. this.files.splice(index, 1)
  500. this.$nextTick(()=>{
  501. this.setEmit()
  502. })
  503. },
  504. /**
  505. * 获取文件名和后缀
  506. * @param {Object} name
  507. */
  508. getFileExt(name) {
  509. const last_len = name.lastIndexOf('.')
  510. const len = name.length
  511. return {
  512. name: name.substring(0, last_len),
  513. ext: name.substring(last_len + 1, len)
  514. }
  515. },
  516. /**
  517. * 获取图片信息
  518. * @param {Object} filepath
  519. */
  520. getFileInfo(filepath) {
  521. return new Promise((resolve, reject) => {
  522. uni.getImageInfo({
  523. src: filepath,
  524. success(res) {
  525. resolve(res)
  526. },
  527. fail(err) {
  528. reject(err)
  529. }
  530. })
  531. })
  532. },
  533. /**
  534. * 获取封装数据
  535. */
  536. async getFileData(files, type = 'image') {
  537. // 最终需要上传数据库的数据
  538. let fileFullName = this.getFileExt(files.name)
  539. const extname = fileFullName.ext.toLowerCase()
  540. let filedata = {
  541. name: files.name,
  542. uuid: files.uuid,
  543. extname: extname || '',
  544. cloudPath: files.cloudPath,
  545. file:files.file,
  546. fileType: files.fileType,
  547. url: files.path || files.path,
  548. size: files.size, //单位是字节
  549. image: {},
  550. path: files.path,
  551. video: {}
  552. }
  553. if (type === 'image') {
  554. const imageinfo = await this.getFileInfo(files.path)
  555. delete filedata.video
  556. filedata.image.width = imageinfo.width
  557. filedata.image.height = imageinfo.height
  558. filedata.image.location = imageinfo.path
  559. }else{
  560. delete filedata.image
  561. }
  562. return filedata
  563. },
  564. /**
  565. * 批量上传
  566. */
  567. uploadCloudFiles(files, max = 5, onUploadProgress) {
  568. files = JSON.parse(JSON.stringify(files))
  569. const len = files.length
  570. let count = 0
  571. let self = this
  572. console.log(1)
  573. return new Promise(resolve => {
  574. while (count < max) {
  575. next()
  576. }
  577. function next() {
  578. let cur = count++
  579. if (cur >= len) {
  580. !files.find(item => !item.url && !item.errMsg) && resolve(files)
  581. return
  582. }
  583. const fileItem = files[cur]
  584. const index = self.files.findIndex(v => v.uuid === fileItem.uuid)
  585. fileItem.url = ''
  586. delete fileItem.errMsg
  587. uni.uploadFile({
  588. filePath: fileItem.path,
  589. cloudPath: fileItem.cloudPath,
  590. fileType: fileItem.fileType,
  591. onUploadProgress: res => {
  592. res.index = index
  593. onUploadProgress && onUploadProgress(res)
  594. }
  595. })
  596. .then(res => {
  597. fileItem.url = res.fileID
  598. fileItem.index = index
  599. if (cur < len) {
  600. next()
  601. }
  602. })
  603. .catch(res => {
  604. fileItem.errMsg = res.errMsg || res.message
  605. fileItem.index = index
  606. if (cur < len) {
  607. next()
  608. }
  609. })
  610. }
  611. })
  612. },
  613. setEmit() {
  614. let data = []
  615. if (this.returnType === 'object') {
  616. data = this.backObject(this.files)[0]
  617. } else {
  618. data = this.backObject(this.files)
  619. }
  620. this.$emit('input', data)
  621. },
  622. backObject(files) {
  623. let newFilesData = JSON.parse(JSON.stringify(files))
  624. newFilesData.map(v => {
  625. delete v.path
  626. delete v.uuid
  627. delete v.video
  628. delete v.progress
  629. delete v.errMsg
  630. delete v.status
  631. delete v.cloudPath
  632. return v
  633. })
  634. return newFilesData
  635. },
  636. async getTempFileURL(file, fileList) {
  637. fileList = {
  638. fileList: [].concat(fileList)
  639. }
  640. const urls = await uniCloud.getTempFileURL(fileList)
  641. file.path = urls.fileList[0].tempFileURL || ''
  642. const index = this.files.findIndex(v => v.path === file.path)
  643. if (index !== -1) {
  644. this.$set(this.files, index, file)
  645. }
  646. }
  647. }
  648. }
  649. </script>
  650. <style lang="scss">
  651. .title{
  652. display: flex;
  653. align-items: center;
  654. justify-content: space-between;
  655. font-weight: 500;
  656. .lable{
  657. font-size: 28rpx;
  658. color: #343434;
  659. }
  660. .text{
  661. font-size: 26rpx;
  662. color: #0099FF;
  663. }
  664. .input{
  665. font-size: 28rpx;
  666. }
  667. .select {
  668. color: #aaa;
  669. font-size: 28rpx;
  670. flex: 1;
  671. text-align: right;
  672. }
  673. .black{
  674. width: 15rpx;
  675. height: 26rpx;
  676. margin-left: 20rpx;
  677. }
  678. }
  679. </style>
  680. <style>
  681. .uni-file-picker {
  682. /* #ifndef APP-NVUE */
  683. box-sizing: border-box;
  684. overflow: hidden;
  685. /* #endif */
  686. }
  687. .uni-file-picker__header {
  688. padding-top: 5px;
  689. padding-bottom: 10px;
  690. /* #ifndef APP-NVUE */
  691. display: flex;
  692. /* #endif */
  693. justify-content: space-between;
  694. }
  695. .file-title {
  696. font-size: 14px;
  697. color: #333;
  698. }
  699. .file-count {
  700. font-size: 14px;
  701. color: #999;
  702. }
  703. .is-add {
  704. /* #ifndef APP-NVUE */
  705. display: flex;
  706. /* #endif */
  707. align-items: center;
  708. justify-content: center;
  709. }
  710. .icon-add {
  711. width: 50px;
  712. height: 5px;
  713. background-color: #f1f1f1;
  714. border-radius: 2px;
  715. }
  716. .rotate {
  717. position: absolute;
  718. transform: rotate(90deg);
  719. }
  720. </style>