314 lines
9.0 KiB
Vue
314 lines
9.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, defineComponent, reactive, ref, toRef, watch } from 'vue'
|
|
import { PREFIX } from '../_constants'
|
|
import { getMainClass } from '../_utils'
|
|
import { useTranslate } from '../../locale'
|
|
import NutButton from '../button/button.vue'
|
|
import { useFormDisabled } from '../form/form'
|
|
import NutIcon from '../icon/icon.vue'
|
|
import NutProgress from '../progress/progress.vue'
|
|
import type { FileItem } from './type'
|
|
import { uploaderEmits, uploaderProps } from './uploader'
|
|
import type { ChooseFile, OnProgressUpdateResult, UploadFileSuccessCallbackResult, UploadOptions } from './use-uploader'
|
|
import { chooseFile, createUploader } from './use-uploader'
|
|
|
|
const props = defineProps(uploaderProps)
|
|
const emit = defineEmits(uploaderEmits)
|
|
defineExpose({ submit, chooseImage, clearUploadQueue })
|
|
const fileList = ref(props.fileList as Array<FileItem>)
|
|
const uploadQueue = ref<Promise<any>[]>([])
|
|
const disabled = useFormDisabled(toRef(props, 'disabled'))
|
|
|
|
watch(
|
|
() => props.fileList,
|
|
() => {
|
|
fileList.value = props.fileList
|
|
},
|
|
)
|
|
|
|
function fileItemClick(fileItem: FileItem) {
|
|
emit('fileItemClick', { fileItem })
|
|
}
|
|
|
|
function executeUpload(fileItem: FileItem, index: number) {
|
|
const { type, url, formData } = fileItem
|
|
|
|
const uploadOption: UploadOptions = {
|
|
url: props.url ? props.url : '',
|
|
filePath: url,
|
|
name: props.name,
|
|
fileType: type,
|
|
header: props.headers,
|
|
timeout: +props?.timeout,
|
|
xhrState: +props.xhrState,
|
|
formData,
|
|
file: fileItem as any,
|
|
}
|
|
|
|
uploadOption.onStart = (option: UploadOptions) => {
|
|
fileItem.status = 'ready'
|
|
fileItem.message = translate('readyUpload')
|
|
clearUploadQueue(index)
|
|
emit('start', option)
|
|
}
|
|
uploadOption.onProgress = (event: OnProgressUpdateResult, option: UploadOptions) => {
|
|
fileItem.status = 'uploading'
|
|
fileItem.message = translate('uploading')
|
|
|
|
fileItem.percentage = event?.progress
|
|
emit('progress', { event, option, percentage: fileItem.percentage })
|
|
}
|
|
|
|
uploadOption.onSuccess = (data: UploadFileSuccessCallbackResult, option: UploadOptions) => {
|
|
fileItem.status = 'success'
|
|
fileItem.message = translate('success')
|
|
emit('success', {
|
|
data,
|
|
responseText: data,
|
|
option,
|
|
fileItem,
|
|
})
|
|
emit('update:fileList', fileList.value)
|
|
}
|
|
uploadOption.onFailure = (data, option: UploadOptions) => {
|
|
fileItem.status = 'error'
|
|
fileItem.message = translate('error')
|
|
emit('failure', {
|
|
data,
|
|
responseText: data,
|
|
option,
|
|
fileItem,
|
|
})
|
|
}
|
|
|
|
const task = createUploader(uploadOption)
|
|
if (props.beforeUpload) {
|
|
props.beforeUpload(uni.uploadFile, uploadOption)
|
|
}
|
|
|
|
else if (props.autoUpload) {
|
|
task.upload()
|
|
}
|
|
else {
|
|
uploadQueue.value.push(
|
|
new Promise((resolve, reject) => {
|
|
resolve(task)
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
function clearUploadQueue(index = -1) {
|
|
if (index > -1) {
|
|
uploadQueue.value.splice(index, 1)
|
|
}
|
|
else {
|
|
uploadQueue.value = []
|
|
fileList.value.splice(0, fileList.value.length)
|
|
}
|
|
}
|
|
function submit() {
|
|
Promise.all(uploadQueue.value).then((res) => {
|
|
res.forEach(i => i.upload())
|
|
})
|
|
}
|
|
|
|
function readFile(files: ChooseFile[]) {
|
|
files.forEach((file, index: number) => {
|
|
let fileType = file.type
|
|
const filepath = (file.tempFilePath || file.path || file.url) as string
|
|
const fileItem = reactive({} as FileItem)
|
|
|
|
if (file.fileType) {
|
|
fileType = file.fileType
|
|
}
|
|
else {
|
|
const imgReg = /\.(png|jpeg|jpg|webp|gif)$/i
|
|
if (!fileType && (imgReg.test(filepath) || filepath.includes('data:image')))
|
|
fileType = 'image'
|
|
}
|
|
fileItem.uid = new Date().getTime().toString() + Math.random().toString(36).substring(2, 9)
|
|
fileItem.path = filepath
|
|
fileItem.name = file.name || filepath
|
|
fileItem.status = 'ready'
|
|
fileItem.message = translate('waitingUpload')
|
|
fileItem.type = fileType!
|
|
fileItem.formData = props.data
|
|
if (props.isPreview)
|
|
fileItem.url = fileType === 'video' ? file.url : filepath
|
|
|
|
fileList.value.push(fileItem)
|
|
executeUpload(fileItem, index)
|
|
})
|
|
}
|
|
|
|
function filterFiles(files: ChooseFile[]) {
|
|
const maximum = (props.maximum as number) * 1
|
|
const maximize = (props.maximize as number) * 1
|
|
const oversizes = new Array<ChooseFile>()
|
|
files = files.filter((file: ChooseFile) => {
|
|
if (file.size > maximize) {
|
|
oversizes.push(file)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if (oversizes.length)
|
|
emit('oversize', oversizes)
|
|
|
|
const currentFileLength = files.length + fileList.value.length
|
|
if (currentFileLength > maximum)
|
|
files.splice(files.length - (currentFileLength - maximum))
|
|
|
|
return files
|
|
}
|
|
|
|
async function onDelete(file: FileItem, index: number) {
|
|
clearUploadQueue(index)
|
|
if (props.beforeDelete == null || await props.beforeDelete(file, fileList)) {
|
|
fileList.value.splice(index, 1)
|
|
emit('delete', {
|
|
file,
|
|
fileList: fileList.value,
|
|
index,
|
|
})
|
|
}
|
|
else {
|
|
// console.log('用户阻止了删除!');
|
|
}
|
|
}
|
|
|
|
function chooseImage(event: InputEvent) {
|
|
if (disabled.value)
|
|
return
|
|
|
|
const maximum = (props.maximum as number) * 1
|
|
|
|
chooseFile({
|
|
accept: props.accept,
|
|
multiple: props.multiple,
|
|
capture: props.capture,
|
|
maxDuration: +props.maxDuration,
|
|
sizeType: props.sizeType,
|
|
camera: props.camera,
|
|
maxCount: maximum - fileList.value.length,
|
|
}, props, fileList.value).then((files) => {
|
|
const filteredFiles: ChooseFile[] = filterFiles(
|
|
new Array<ChooseFile>().slice.call(files),
|
|
)
|
|
|
|
readFile(filteredFiles)
|
|
|
|
emit('change', { fileList: fileList.value, event })
|
|
})
|
|
}
|
|
|
|
const classes = computed(() => {
|
|
return getMainClass(props, componentName)
|
|
})
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
const componentName = `${PREFIX}-uploader`
|
|
const { translate } = useTranslate(componentName)
|
|
export default defineComponent({
|
|
name: componentName,
|
|
options: {
|
|
virtualHost: true,
|
|
addGlobalClass: true,
|
|
styleIsolation: 'shared',
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<view :class="classes" :style="customStyle">
|
|
<view v-if="$slots.default" class="nut-uploader__slot">
|
|
<slot />
|
|
<template v-if="Number(maximum) - fileList.length">
|
|
<NutButton custom-class="nut-uploader__input" @click="(chooseImage as any)" />
|
|
</template>
|
|
</view>
|
|
|
|
<view
|
|
v-for="(item, index) in fileList"
|
|
:key="item.uid"
|
|
class="nut-uploader__preview"
|
|
:class="[listType]"
|
|
>
|
|
<view v-if="listType === 'picture' && !$slots.default" class="nut-uploader__preview-img">
|
|
<view v-if="item.status !== 'success'" class="nut-uploader__preview__progress">
|
|
<template v-if="item.status !== 'ready'">
|
|
<NutIcon v-if="item.status === 'error'" name="failure" custom-color="#fff" />
|
|
<NutIcon v-else name="loading" custom-color="#fff" />
|
|
</template>
|
|
<view class="nut-uploader__preview__progress__msg">
|
|
{{ item.message }}
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="isDeletable && !disabled" class="close" @click="onDelete(item, index)">
|
|
<slot name="deleteIcon">
|
|
<NutIcon name="failure" />
|
|
</slot>
|
|
</view>
|
|
|
|
<image
|
|
v-if="(item?.type?.includes('image') || item?.type?.includes('video')) && item.url"
|
|
class="nut-uploader__preview-img__c"
|
|
:mode="mode"
|
|
:src="item.url"
|
|
@click="fileItemClick(item)"
|
|
/>
|
|
<view v-else class="nut-uploader__preview-img__file">
|
|
<view class="nut-uploader__preview-img__file__name" @click="fileItemClick(item)">
|
|
<view class="file-name__tips">
|
|
{{ item.name }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="tips">
|
|
{{ item.name }}
|
|
</view>
|
|
</view>
|
|
<view v-else-if="listType === 'list'" class="nut-uploader__preview-list">
|
|
<view class="nut-uploader__preview-img__file__name" :class="[item.status]" @click="fileItemClick(item)">
|
|
<NutIcon name="link" custom-class="nut-uploader__preview-img__file__link" />
|
|
<view class="file-name__tips">
|
|
{{ item.name }}
|
|
</view>
|
|
<NutIcon
|
|
v-if="isDeletable && !disabled"
|
|
name="del"
|
|
custom-color="#808080"
|
|
custom-class="nut-uploader__preview-img__file__del"
|
|
@click="onDelete(item, index)"
|
|
/>
|
|
</view>
|
|
|
|
<NutProgress
|
|
v-if="item.status === 'uploading'"
|
|
size="small"
|
|
:percentage="item.percentage"
|
|
stroke-color="linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)"
|
|
:show-text="false"
|
|
/>
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-if="listType === 'picture' && !$slots.default && Number(maximum) - fileList.length"
|
|
class="nut-uploader__upload"
|
|
:class="[listType]"
|
|
>
|
|
<slot name="uploadIcon">
|
|
<NutIcon name="photograph" custom-color="#808080" />
|
|
</slot>
|
|
<NutButton custom-class="nut-uploader__input" :class="{ disabled }" @click="(chooseImage as any)" />
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './index';
|
|
</style>
|