<template> <view class="uni-file-picker"> <view v-if="title" class="uni-file-picker__header"> <text class="file-title">{{ title }}</text> <text class="file-count">{{ filesList.length }}/{{ limitLength }}</text> </view> <upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly" :image-styles="imageStyles" :files-list="filesList" :limit="limitLength" :disablePreview="disablePreview" :delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile"> <slot> <view class="is-add"> <view class="icon-add"></view> <view class="icon-add rotate"></view> </view> </slot> </upload-image> <upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly" :list-styles="listStyles" :files-list="filesList" :showType="showType" :delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile"> <slot><button type="primary" size="mini">选择文件</button></slot> </upload-file> </view> </template> <script> import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js' import { get_file_ext, get_extname, get_files_and_is_max, get_file_info, get_file_data } from './utils.js' import uploadImage from './upload-image.vue' import uploadFile from './upload-file.vue' let fileInput = null /** * FilePicker 文件选择上传 * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 * @tutorial https://ext.dcloud.net.cn/plugin?id=4079 * @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定 * @property {Boolean} disabled = [true|false] 组件禁用 * @value true 禁用 * @value false 取消禁用 * @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮 * @value true 只读 * @value false 取消只读 * @property {String} return-type = [array|object] 限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖 * @value array 规定 value 属性的类型为数组 * @value object 规定 value 属性的类型为对象 * @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效 * @value true 禁用图片预览 * @value false 取消禁用图片预览 * @property {Boolean} del-icon = [true|false] 是否显示删除按钮 * @value true 显示删除按钮 * @value false 不显示删除按钮 * @property {Boolean} auto-upload = [true|false] 是否自动上传,值为true则只触发@select,可自行上传 * @value true 自动上传 * @value false 取消自动上传 * @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分 * @property {String} title 组件标题,右侧显示上传计数 * @property {String} mode = [list|grid] 选择文件后的文件列表样式 * @value list 列表显示 * @value grid 宫格显示 * @property {String} file-mediatype = [image|video|all] 选择文件类型 * @value image 只选择图片 * @value video 只选择视频 * @value all 选择所有文件 * @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同 * @property {Object} list-style mode:list 时的样式 * @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同 * @event {Function} select 选择文件后触发 * @event {Function} progress 文件上传时触发 * @event {Function} success 上传成功触发 * @event {Function} fail 上传失败触发 * @event {Function} delete 文件从列表移除时触发 */ export default { name: 'uniFilePicker', components: { uploadImage, uploadFile }, options: { virtualHost: true }, emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'], props: { // #ifdef VUE3 modelValue: { type: [Array, Object], default () { return [] } }, // #endif // #ifndef VUE3 value: { type: [Array, Object], default () { return [] } }, // #endif disabled: { type: Boolean, default: false }, disablePreview: { type: Boolean, default: false }, delIcon: { type: Boolean, default: true }, // 自动上传 autoUpload: { type: Boolean, default: true }, // 最大选择个数 ,h5只能限制单选或是多选 limit: { type: [Number, String], default: 9 }, // 列表样式 grid | list | list-card mode: { type: String, default: 'grid' }, // 选择文件类型 image/video/all fileMediatype: { type: String, default: 'image' }, // 文件类型筛选 fileExtname: { type: [Array, String], default () { return [] } }, title: { type: String, default: '' }, listStyles: { type: Object, default () { return { // 是否显示边框 border: true, // 是否显示分隔线 dividline: true, // 线条样式 borderStyle: {} } } }, imageStyles: { type: Object, default () { return { width: 'auto', height: 'auto' } } }, readonly: { type: Boolean, default: false }, returnType: { type: String, default: 'array' }, sizeType: { type: Array, default () { return ['original', 'compressed'] } }, sourceType: { type: Array, default () { return ['album', 'camera'] } }, provider: { type: String, default: '' // 默认上传到 unicloud 内置存储 extStorage 扩展存储 } }, data() { return { files: [], localValue: [] } }, watch: { // #ifndef VUE3 value: { handler(newVal, oldVal) { this.setValue(newVal, oldVal) }, immediate: true }, // #endif // #ifdef VUE3 modelValue: { handler(newVal, oldVal) { this.setValue(newVal, oldVal) }, immediate: true }, // #endif }, computed: { filesList() { let files = [] this.files.forEach(v => { files.push(v) }) return files }, showType() { if (this.fileMediatype === 'image') { return this.mode } return 'list' }, limitLength() { if (this.returnType === 'object') { return 1 } if (!this.limit) { return 1 } if (this.limit >= 9) { return 9 } return this.limit } }, created() { // TODO 兼容不开通服务空间的情况 if (!(uniCloud.config && uniCloud.config.provider)) { this.noSpace = true uniCloud.chooseAndUploadFile = chooseAndUploadFile } this.form = this.getForm('uniForms') this.formItem = this.getForm('uniFormsItem') if (this.form && this.formItem) { if (this.formItem.name) { this.rename = this.formItem.name this.form.inputChildrens.push(this) } } }, methods: { /** * 公开用户使用,清空文件 * @param {Object} index */ clearFiles(index) { if (index !== 0 && !index) { this.files = [] this.$nextTick(() => { this.setEmit() }) } else { this.files.splice(index, 1) } this.$nextTick(() => { this.setEmit() }) }, /** * 公开用户使用,继续上传 */ upload() { let files = [] this.files.forEach((v, index) => { if (v.status === 'ready' || v.status === 'error') { files.push(Object.assign({}, v)) } }) return this.uploadFiles(files) }, async setValue(newVal, oldVal) { const newData = async (v) => { const reg = /cloud:\/\/([\w.]+\/?)\S*/ let url = '' if(v.fileID){ url = v.fileID }else{ url = v.url } if (reg.test(url)) { v.fileID = url v.url = await this.getTempFileURL(url) } if(v.url) v.path = v.url return v } if (this.returnType === 'object') { if (newVal) { await newData(newVal) } else { newVal = {} } } else { if (!newVal) newVal = [] for(let i =0 ;i < newVal.length ;i++){ let v = newVal[i] await newData(v) } } this.localValue = newVal if (this.form && this.formItem &&!this.is_reset) { this.is_reset = false this.formItem.setValue(this.localValue) } let filesData = Object.keys(newVal).length > 0 ? newVal : []; this.files = [].concat(filesData) }, /** * 选择文件 */ choose() { if (this.disabled) return if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType === 'array') { uni.showToast({ title: `您最多选择 ${this.limitLength} 个文件`, icon: 'none' }) return } this.chooseFiles() }, /** * 选择文件并上传 */ chooseFiles() { const _extname = get_extname(this.fileExtname) // 获取后缀 uniCloud .chooseAndUploadFile({ type: this.fileMediatype, compressed: false, sizeType: this.sizeType, sourceType: this.sourceType, // TODO 如果为空,video 有问题 extension: _extname.length > 0 ? _extname : undefined, count: this.limitLength - this.files.length, //默认9 onChooseFile: this.chooseFileCallback, onUploadProgress: progressEvent => { this.setProgress(progressEvent, progressEvent.index) } }) .then(result => { this.setSuccessAndError(result.tempFiles) }) .catch(err => { console.log('选择失败', err) }) }, /** * 选择文件回调 * @param {Object} res */ async chooseFileCallback(res) { const _extname = get_extname(this.fileExtname) const is_one = (Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) || this.returnType === 'object' // 如果这有一个文件 ,需要清空本地缓存数据 if (is_one) { this.files = [] } let { filePaths, files } = get_files_and_is_max(res, _extname) if (!(_extname && _extname.length > 0)) { filePaths = res.tempFilePaths files = res.tempFiles } let currentData = [] for (let i = 0; i < files.length; i++) { if (this.limitLength - this.files.length <= 0) break files[i].uuid = Date.now() let filedata = await get_file_data(files[i], this.fileMediatype) filedata.progress = 0 filedata.status = 'ready' this.files.push(filedata) currentData.push({ ...filedata, file: files[i] }) } this.$emit('select', { tempFiles: currentData, tempFilePaths: filePaths }) res.tempFiles = files // 停止自动上传 if (!this.autoUpload || this.noSpace) { res.tempFiles = [] } res.tempFiles.forEach((fileItem, index) => { this.provider && (fileItem.provider = this.provider); const fileNameSplit = fileItem.name.split('.') const ext = fileNameSplit.pop() const fileName = fileNameSplit.join('.').replace(/[\s\/\?<>\\:\*\|":]/g, '_') fileItem.cloudPath = fileName + '_' + Date.now() + '_' + index + '.' + ext }) }, /** * 批传 * @param {Object} e */ uploadFiles(files) { files = [].concat(files) return uploadCloudFiles.call(this, files, 5, res => { this.setProgress(res, res.index, true) }) .then(result => { this.setSuccessAndError(result) return result; }) .catch(err => { console.log(err) }) }, /** * 成功或失败 */ async setSuccessAndError(res, fn) { let successData = [] let errorData = [] let tempFilePath = [] let errorTempFilePath = [] for (let i = 0; i < res.length; i++) { const item = res[i] const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index if (index === -1 || !this.files) break if (item.errMsg === 'request:fail') { this.files[index].url = item.path this.files[index].status = 'error' this.files[index].errMsg = item.errMsg // this.files[index].progress = -1 errorData.push(this.files[index]) errorTempFilePath.push(this.files[index].url) } else { this.files[index].errMsg = '' this.files[index].fileID = item.url const reg = /cloud:\/\/([\w.]+\/?)\S*/ if (reg.test(item.url)) { this.files[index].url = await this.getTempFileURL(item.url) }else{ this.files[index].url = item.url } this.files[index].status = 'success' this.files[index].progress += 1 successData.push(this.files[index]) tempFilePath.push(this.files[index].fileID) } } if (successData.length > 0) { this.setEmit() // 状态改变返回 this.$emit('success', { tempFiles: this.backObject(successData), tempFilePaths: tempFilePath }) } if (errorData.length > 0) { this.$emit('fail', { tempFiles: this.backObject(errorData), tempFilePaths: errorTempFilePath }) } }, /** * 获取进度 * @param {Object} progressEvent * @param {Object} index * @param {Object} type */ setProgress(progressEvent, index, type) { const fileLenth = this.files.length const percentNum = (index / fileLenth) * 100 const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) let idx = index if (!type) { idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid) } if (idx === -1 || !this.files[idx]) return // fix by mehaotian 100 就会消失,-1 是为了让进度条消失 this.files[idx].progress = percentCompleted - 1 // 上传中 this.$emit('progress', { index: idx, progress: parseInt(percentCompleted), tempFile: this.files[idx] }) }, /** * 删除文件 * @param {Object} index */ delFile(index) { this.$emit('delete', { index, tempFile: this.files[index], tempFilePath: this.files[index].url }) this.files.splice(index, 1) this.$nextTick(() => { this.setEmit() }) }, /** * 获取文件名和后缀 * @param {Object} name */ getFileExt(name) { const last_len = name.lastIndexOf('.') const len = name.length return { name: name.substring(0, last_len), ext: name.substring(last_len + 1, len) } }, /** * 处理返回事件 */ setEmit() { let data = [] if (this.returnType === 'object') { data = this.backObject(this.files)[0] this.localValue = data?data:null } else { data = this.backObject(this.files) if (!this.localValue) { this.localValue = [] } this.localValue = [...data] } // #ifdef VUE3 this.$emit('update:modelValue', this.localValue) // #endif // #ifndef VUE3 this.$emit('input', this.localValue) // #endif }, /** * 处理返回参数 * @param {Object} files */ backObject(files) { let newFilesData = [] files.forEach(v => { newFilesData.push({ extname: v.extname, fileType: v.fileType, image: v.image, name: v.name, path: v.path, size: v.size, fileID:v.fileID, url: v.url, // 修改删除一个文件后不能再上传的bug, #694 uuid: v.uuid, status: v.status, cloudPath: v.cloudPath }) }) return newFilesData }, async getTempFileURL(fileList) { fileList = { fileList: [].concat(fileList) } const urls = await uniCloud.getTempFileURL(fileList) return urls.fileList[0].tempFileURL || '' }, /** * 获取父元素实例 */ getForm(name = 'uniForms') { let parent = this.$parent; let parentName = parent.$options.name; while (parentName !== name) { parent = parent.$parent; if (!parent) return false; parentName = parent.$options.name; } return parent; } } } </script> <style> .uni-file-picker { /* #ifndef APP-NVUE */ box-sizing: border-box; overflow: hidden; width: 100%; /* #endif */ flex: 1; } .uni-file-picker__header { padding-top: 5px; padding-bottom: 10px; /* #ifndef APP-NVUE */ display: flex; /* #endif */ justify-content: space-between; } .file-title { font-size: 14px; color: #333; } .file-count { font-size: 14px; color: #999; } .is-add { /* #ifndef APP-NVUE */ display: flex; /* #endif */ align-items: center; justify-content: center; } .icon-add { width: 50px; height: 5px; background-color: #f1f1f1; border-radius: 2px; } .rotate { position: absolute; transform: rotate(90deg); } </style>