| | |
| | | <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 |
| | | }, |
| | | 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'] |
| | | } |
| | | } |
| | | }, |
| | | 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)) |
| | | } |
| | | }) |
| | | 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 || this.noSpace) { |
| | | res.tempFiles = [] |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 批传 |
| | | * @param {Object} e |
| | | */ |
| | | uploadFiles(files) { |
| | | files = [].concat(files) |
| | | uploadCloudFiles.call(this, files, 5, res => { |
| | | this.setProgress(res, res.index, true) |
| | | }) |
| | | .then(result => { |
| | | this.setSuccessAndError(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 = [] |
| | | 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 |
| | | }) |
| | | }) |
| | | 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; |
| | | /* #endif */ |
| | | } |
| | | |
| | | .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> |
| | | <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
|
| | | },
|
| | | 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']
|
| | | }
|
| | | }
|
| | | },
|
| | | 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))
|
| | | }
|
| | | })
|
| | | 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 || this.noSpace) {
|
| | | res.tempFiles = []
|
| | | }
|
| | | },
|
| | |
|
| | | /**
|
| | | * 批传
|
| | | * @param {Object} e
|
| | | */
|
| | | uploadFiles(files) {
|
| | | files = [].concat(files)
|
| | | uploadCloudFiles.call(this, files, 5, res => {
|
| | | this.setProgress(res, res.index, true)
|
| | | })
|
| | | .then(result => {
|
| | | this.setSuccessAndError(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 = []
|
| | | 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
|
| | | })
|
| | | })
|
| | | 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;
|
| | | /* #endif */
|
| | | }
|
| | |
|
| | | .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>
|