<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> <view v-if="subtitle" class="file-subtitle"> <view>{{ subtitle }}</view> </view> <upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly" :image-styles="imageStyles" :files-list="url" :limit="limitLength" :disablePreview="disablePreview" :delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile" > <slot> <view class="is-add"> <image :src="imgsrc" class="add-icon"></image> </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'; import sheep from '@/sheep'; let fileInput = null; /** * FilePicker 文件选择上传 * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 * @tutorial https://ext.dcloud.net.cn/plugin?id=4079 * @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定 * @property {String|Array} url url数据 * @property {Boolean} disabled = [true|false] 组件禁用 * @value true 禁用 * @value false 取消禁用 * @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮 * @value true 只读 * @value false 取消只读 * @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: 'sUploader', components: { uploadImage, uploadFile, }, options: { virtualHost: true, }, emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'update:url'], props: { modelValue: { type: [Array, Object], default() { return []; }, }, url: { type: [Array, String], default() { return []; }, }, 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, }, sizeType: { type: Array, default() { return ['original', 'compressed']; }, }, driver: { type: String, default: 'local', // local=本地 | oss | unicloud }, subtitle: { type: String, default: '', }, }, data() { return { files: [], localValue: [], imgsrc: sheep.$url.static('/static/img/shop/upload-camera.png'), }; }, watch: { modelValue: { handler(newVal, oldVal) { this.setValue(newVal, oldVal); }, immediate: true, }, }, computed: { returnType() { if (this.limit > 1) { return 'array'; } return 'object'; }, 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() { if (this.driver === 'local') { 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, // 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) { res.tempFiles = []; } }, /** * 批传 * @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', { 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 = []; let updateUrl = []; if (this.returnType === 'object') { data = this.backObject(this.files)[0]; this.localValue = data ? data : null; updateUrl = data ? data.url : ''; } else { data = this.backObject(this.files); if (!this.localValue) { this.localValue = []; } this.localValue = [...data]; if (this.localValue.length > 0) { this.localValue.forEach((item) => { updateUrl.push(item.url); }); } } this.$emit('update:modelValue', this.localValue); this.$emit('update:url', updateUrl); }, /** * 处理返回参数 * @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, }); }); 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 lang="scss" scoped> .uni-file-picker { /* #ifndef APP-NVUE */ box-sizing: border-box; overflow: hidden; /* width: 100%; */ /* #endif */ /* flex: 1; */ position: relative; } .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; } .add-icon { width: 57rpx; height: 49rpx; } .file-subtitle { position: absolute; left: 50%; transform: translateX(-50%); bottom: 0; width: 140rpx; height: 36rpx; z-index: 1; display: flex; justify-content: center; color: #fff; font-weight: 500; background: rgba(#000, 0.3); font-size: 24rpx; } </style>