<template>
|
|
<view class="l-upload" ref="uploadRef" :style="[styles]">
|
|
<view class="l-upload__item" v-for="(file, index) in customFiles" :style="[itemStyle]" :key="file.url">
|
|
<view class="l-upload__item-inner" :style="[innerStyle]">
|
|
<slot name="file" :file="file" :index="index">
|
|
<image class="l-upload__image" v-if="file.type == 'image'" :src="file.url" :data-file="file"
|
|
:mode="imageFit" @click="onProofTap(index)" />
|
|
<video class="l-upload__image" v-if="file.type == 'video'" :src="file.url" :data-file="file"
|
|
:poster="file.thumb" :autoplay="false" objectFit="contain" @click="onFileClick(index)" />
|
|
<view class="l-upload__progress-mask" v-if="file.status != null && file.status != 'done'" :data-file="file" @click="onFileClick(index)">
|
|
<template v-if="file.status == 'loading'">
|
|
<l-loading class="l-upload__progress-loading" size="24px" color="white" />
|
|
<text class="l-upload__progress-text" v-if="file.percent != null">{{file.percent}}%</text>
|
|
<text class="l-upload__progress-text" v-else>{{loadingText}}</text>
|
|
</template>
|
|
<l-icon v-else class="l-upload__progress-icon"
|
|
:name="file.status == 'reload' ? 'refresh' : 'close-circle'" size="48rpx" aria-hidden />
|
|
<text v-if="file.status == 'reload' || file.status == 'failed'" class="l-upload__progress-text">
|
|
{{file.status == 'reload' ? reloadText : failedText}}
|
|
</text>
|
|
</view>
|
|
<view class="l-upload__delete-btn" aria-role="button" aria-label="删除" :data-index="index"
|
|
@click="onDelete(index)">
|
|
<l-icon name="close" size="16px" color="#fff" />
|
|
</view>
|
|
</slot>
|
|
</view>
|
|
</view>
|
|
<view class="l-upload__item l-upload__item--add"
|
|
:class="{'l-upload__item--disabled':disabled}"
|
|
:style="[itemStyle]"
|
|
aria-label="上传"
|
|
v-show="!multiple ? customFiles.length == 0: true"
|
|
@click="onAddTap">
|
|
<view class="l-upload__item-inner" :style="[innerStyle, {background: addBgColor}]">
|
|
<slot>
|
|
<l-icon class="l-upload__add-icon" :size="uploadIconSize" :name="uploadIcon" />
|
|
</slot>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
<script lang="ts">
|
|
// @ts-nocheck
|
|
import { defineComponent, ref, computed, watch, onUnmounted } from '@/uni_modules/lime-shared/vue';
|
|
import { unitConvert } from '@/uni_modules/lime-shared/unitConvert'
|
|
import uploadProps from './props'
|
|
import { chooseFiles } from './utils'
|
|
import { UploadFile, ChooseFileOptions } from './type';
|
|
export default defineComponent({
|
|
name: 'l-upload',
|
|
props: uploadProps,
|
|
emit: ['fail', 'remove', 'success', 'click', 'add', 'input', 'update:modelValue'],
|
|
setup(props, {expose, emit}){
|
|
const customFiles = ref<UploadFile[]>(props.value ?? props.modelValue ?? props.defaultFiles ?? []);
|
|
|
|
const styles = computed(()=> {
|
|
const style:Record<string, string> = {}
|
|
const gutter = unitConvert(props.gutter ?? 8) / 2 * -1;
|
|
style['margin-left'] = `${gutter}px`;
|
|
style['margin-right'] = `${gutter}px`;
|
|
style['margin-top'] = `${gutter}px`;
|
|
return style
|
|
})
|
|
|
|
const itemStyle = computed(() => {
|
|
const style:Record<string, string> = {}
|
|
|
|
let column = props.column ?? 4;
|
|
if(props.column) {
|
|
style['width'] = `${100 / column}%`
|
|
}
|
|
if(props.gridWidth) {
|
|
const gutter = unitConvert(props.gutter ?? 8)
|
|
style['width'] = `${unitConvert(props.gridWidth) + gutter}px`
|
|
}
|
|
return style
|
|
})
|
|
|
|
const innerStyle = computed(()=>{
|
|
const style:Record<string, string> = {};
|
|
|
|
const gutter = unitConvert(props.gutter ?? 8) / 2
|
|
style['margin'] = `${gutter}px`
|
|
|
|
// if(props.gridWidth) {
|
|
// style['width'] = props.gridWidth
|
|
// }
|
|
if(props.gridBgColor){
|
|
style.set('background', props.gridBgColor!)
|
|
}
|
|
if(props.gridHeight) {
|
|
style['height'] = props.gridHeight
|
|
}
|
|
return style
|
|
})
|
|
|
|
|
|
const onFileClick = (index : number) => {
|
|
const file = customFiles.value[index]
|
|
emit('click', { file })
|
|
}
|
|
const onProofTap = (index : number) => {
|
|
onFileClick(index);
|
|
if (props.disablePreview) return
|
|
uni.previewImage({
|
|
urls: customFiles.value.filter((file) : boolean => file.percent != -1).map((file) : string => file.url),
|
|
current: index
|
|
});
|
|
}
|
|
const onDelete = (index : number) => {
|
|
const delFile = customFiles.value[index]
|
|
customFiles.value = customFiles.value.filter((file, i) : boolean => index != i)
|
|
emit('remove', { index, file: delFile })
|
|
}
|
|
|
|
let last = 0;
|
|
const upload = (files: UploadFile[]) => {
|
|
if(!props.autoUpload || props.action == null || props.action!.length < 5) return
|
|
if(props.action == 'uniCloud') {
|
|
let uploadImgs:Promise<UniCloudUploadFileResult>[] = [];
|
|
files.forEach((file, index) =>{
|
|
const promise = new Promise<UniCloudUploadFileResult>((resolve, reject) =>{
|
|
uniCloud.uploadFile({
|
|
filePath: file.url,
|
|
cloudPath: file.name!.substring(file.name!.lastIndexOf('.')),
|
|
onUploadProgress: (res)=>{
|
|
file.status = 'loading'
|
|
file.percent = Math.floor(res.loaded / res.total * 100)
|
|
},
|
|
}).then(res=>{
|
|
file.path = res.fileID;
|
|
file.status = 'done'
|
|
resolve(res)
|
|
}).catch(err=>{
|
|
file.status = 'failed'
|
|
reject(err)
|
|
})
|
|
})
|
|
uploadImgs.push(promise as Promise<UniCloudUploadFileResult>)
|
|
})
|
|
|
|
Promise.all(uploadImgs).then(res =>{
|
|
emit('success', res)
|
|
}).catch(err => {
|
|
emit('fail', err)
|
|
})
|
|
} else {
|
|
let uploadImgs:Promise<UploadFileSuccess>[] = [];
|
|
files.forEach((file, index) =>{
|
|
const promise = new Promise<UploadFileSuccess>((resolve, reject) =>{
|
|
const task = uni.uploadFile({
|
|
url: props.action!,
|
|
filePath: file.url,
|
|
name: file.name,
|
|
formData: props.formData,
|
|
header: props.headers,
|
|
success: (res) => {
|
|
file.status = 'done'
|
|
if(res.statusCode == 200) {
|
|
if(typeof res.data == 'string') {
|
|
try{
|
|
const data = JSON.parse<UTSJSONObject>(res.data);
|
|
const url = data?.getString('url');
|
|
if(url != null) {
|
|
file.path = url
|
|
}
|
|
}catch(e){
|
|
//TODO handle the exception
|
|
}
|
|
}
|
|
}
|
|
resolve(res)
|
|
},
|
|
fail(err) {
|
|
file.status = 'failed'
|
|
reject(err)
|
|
}
|
|
});
|
|
task.onProgressUpdate((res) => {
|
|
file.status = 'loading'
|
|
file.percent = res.progress
|
|
});
|
|
})
|
|
uploadImgs.push(promise as Promise<UploadFileSuccess>)
|
|
})
|
|
Promise.all(uploadImgs).then(res =>{
|
|
emit('success', res)
|
|
}).catch(err => {
|
|
emit('fail', err)
|
|
})
|
|
}
|
|
};
|
|
const customLimit = computed(() : number => props.max == 0 ? 20 : props.max - customFiles.value.length)
|
|
const onAddTap = () => {
|
|
if (props.disabled) return;
|
|
chooseFiles({
|
|
mediaType: props.mediaType,
|
|
count: customLimit.value,
|
|
sourceType: props.sourceType,
|
|
sizeType: props.sizeType,
|
|
sizeLimit: props.sizeLimit
|
|
} as ChooseFileOptions).then((files) =>{
|
|
last = customFiles.value.length
|
|
customFiles.value = customFiles.value.concat(files)
|
|
const _files = customFiles.value.filter((it, i):boolean => i > last - 1)
|
|
upload(_files)
|
|
emit('add', _files)
|
|
})
|
|
}
|
|
const stop = watch(customFiles, (v : UploadFile[]) => {
|
|
emit('update:modelValue', v)
|
|
// #ifdef VUE2
|
|
emit('input', v)
|
|
// #endif
|
|
})
|
|
// #ifdef VUE2
|
|
const stopValue = watch(() => props.value, (v: UploadFile[])=>{
|
|
if(v.length != customFiles.value.length){
|
|
customFiles.value = v
|
|
}
|
|
})
|
|
// #endif
|
|
// #ifdef VUE3
|
|
const stopValue = watch(() => props.modelValue, (v: UploadFile[])=>{
|
|
if(v.length != customFiles.value.length){
|
|
customFiles.value = v
|
|
}
|
|
})
|
|
// #endif
|
|
// #ifdef VUE3
|
|
expose({
|
|
remove: onDelete
|
|
});
|
|
// #endif
|
|
|
|
|
|
onUnmounted(() => {
|
|
stopValue()
|
|
stop()
|
|
})
|
|
|
|
|
|
return {
|
|
styles,
|
|
customFiles,
|
|
itemStyle,
|
|
innerStyle,
|
|
onProofTap,
|
|
onDelete,
|
|
onAddTap,
|
|
onFileClick,
|
|
// #ifdef VUE2
|
|
remove: onDelete,
|
|
// #endif
|
|
}
|
|
}
|
|
})
|
|
|
|
</script>
|
|
<style lang="scss">
|
|
@import './index';
|
|
</style>
|