You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
651 lines
16 KiB
651 lines
16 KiB
<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)) |
|
} |
|
}) |
|
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 || this.noSpace) { |
|
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 = [] |
|
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>
|
|
|