Files
cmgd-mini-app/uni_modules/nutui-uni/components/uploader/uploader.vue
2026-01-05 12:47:14 +08:00

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>