@tuniao/tnui-vue3-uniapp
Version:
TuniaoUI for vue3 uniapp
378 lines (340 loc) • 10.4 kB
text/typescript
import { computed, getCurrentInstance, nextTick, ref, watch } from 'vue'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants'
import { debugWarn, isBoolean, isPromise, throwError } from '../../../../utils'
import { useFormItem } from '../../../form'
import useUploadHandleFunction from './use-upload-handle-function'
import type { ImageUploadList, ImageUploadListItem } from '../types'
import type { ImageUploadProps } from '../image-upload'
export const useImageUpload = (props: ImageUploadProps) => {
const { emit } = getCurrentInstance()!
const {
chooseImage,
uploadProcess,
checkFileSizeAndExtension,
showErrorTips,
} = useUploadHandleFunction(props)
const { formItem } = useFormItem()
// 文件列表
const fileList = ref<ImageUploadList>([])
// 监听文件列表变化
let isInnerUpdate = false
watch(
() => props.modelValue,
(val) => {
if (isInnerUpdate) {
isInnerUpdate = false
return
}
fileList.value = val.map((item) => ({
url: item,
status: 'done',
progress: 100,
}))
},
{
immediate: true,
}
)
// 判断是否超过最大上传数
const isExceedMaxCount = computed<boolean>(
() => fileList.value.length >= props.limit
)
// 当前剩余可选文件数量
const currentRemainFileCount = computed<number>(() => {
if (props.multiple) {
return props.limit - fileList.value.length
} else {
return props.limit - fileList.value.length > 0 ? 1 : 0
}
})
// 选择文件
const chooseFile = async () => {
const { disabled, action, customUploadHandler } = props
if (disabled) return
// 如果没有设置action或者没有自定义图片上传处理函数,则直接返回
if (!action && !customUploadHandler) {
showErrorTips('请设置action或者自定义图片上传处理函数')
debugWarn('TnImageUpload', '请设置action或者自定义图片上传处理函数')
return
}
// 选择前已有文件的数量
const prevUploadedFileCount = fileList.value.length
chooseImage(currentRemainFileCount.value)
.then((res) => {
let selectFile = res
// 判断尺寸和格式是否正确
const checkFailFiles = checkFileSizeAndExtension(selectFile)
if (checkFailFiles.length) {
showErrorTips('文件格式或大小不符合要求')
emit('oversizeOrNoSupport', checkFailFiles)
// 剔除不符合要求的文件
selectFile = selectFile.filter(
(item) => !checkFailFiles.includes(item)
)
}
fileList.value.push(
...selectFile.map<ImageUploadListItem>((item) => {
const url = (item as UniApp.ChooseImageSuccessCallbackResultFile)
.path
return {
url,
status: 'ready',
progress: 0,
file: item,
}
})
)
if (props.autoUpload && selectFile.length)
uploadFile(prevUploadedFileCount)
})
.catch((err) => {
debugWarn('TnImageUpload', `选择图片失败: ${err}`)
showErrorTips(err?.errMsg || '选择图片失败')
})
}
// 处理上传事件
const handleUploadEvent = (
item: ImageUploadListItem,
index: number,
uploadSingle = false
) => {
uploadProcess(item)
.then((res) => {
if (res) {
handleUploadSuccess(item)
} else {
handleUploadError(item, '上传失败')
}
})
.catch((err) => {
handleUploadError(item, err)
})
.finally(() => {
if (!uploadSingle) uploadFile(index + 1)
})
}
// 上传文件
const uploadFile = (startIndex: number, uploadSingle = false) => {
const { autoUpload, beforeUpload } = props
const autoNextUpload = autoUpload && !uploadSingle
// 判断是否全部文件上传完毕
if (startIndex >= fileList.value.length) {
if (props.autoRemoveFaildFile) handleUploadCompleteFailFile()
return
}
const fileItem = fileList.value[startIndex]
// 如果当前上传完毕自动上传下一张
if (fileItem.progress === 100) {
fileItem.status = 'done'
fileItem.uploadTask = undefined
if (autoNextUpload) uploadFile(startIndex + 1)
return
}
// 上传前回调
if (!beforeUpload) {
handleUploadEvent(fileItem, startIndex, uploadSingle)
return
}
const shouldUpload = beforeUpload(fileItem.file!)
const isBeforeUploadPromiseOrBoolean = [
isPromise(shouldUpload),
isBoolean(shouldUpload),
].includes(true)
if (!isBeforeUploadPromiseOrBoolean) {
throwError(
'[TnImageUpload]',
'beforeUpload返回值必须是Promise或者Boolean'
)
}
if (isPromise(shouldUpload)) {
shouldUpload
.then((res) => {
if (res) handleUploadEvent(fileItem, startIndex, uploadSingle)
else {
removeFile(startIndex)
if (autoNextUpload) uploadFile(startIndex)
}
})
.catch((err) => {
debugWarn('TnImageUpload', `beforeUpload出错: ${err}`)
fileItem.status = 'failed'
})
} else {
if (shouldUpload) handleUploadEvent(fileItem, startIndex, uploadSingle)
else {
removeFile(startIndex)
if (autoNextUpload) uploadFile(startIndex)
}
}
}
// 获取上传成功的文件url
const getUploadSuceesFileUrlValue = () => {
return fileList.value
.filter((item) => item.status === 'done')
.map((item) => item.url)
}
// 已上传文件列表发生改变
const uploadSuccessFileListChange = () => {
isInnerUpdate = true
const value = getUploadSuceesFileUrlValue()
emit(UPDATE_MODEL_EVENT, value)
nextTick(() => {
emit(CHANGE_EVENT, value)
if (props.validateEvent) {
formItem?.validate?.('change').catch((err) => {
debugWarn(err)
})
}
})
}
// 处理文件上传成功
const handleUploadSuccess = (item: ImageUploadListItem) => {
item.status = 'done'
item.progress = 100
item.uploadTask = undefined
item.file = undefined
emit('success', item)
uploadSuccessFileListChange()
}
// 处理上传文件发生错误
const handleUploadError = (item: ImageUploadListItem, errorMsg: string) => {
item.status = 'failed'
item.progress = 0
item.uploadTask = undefined
item.file = undefined
showErrorTips(errorMsg)
emit('fail', new Error(errorMsg), item)
}
// 处理上传完成后失败的文件
const handleUploadCompleteFailFile = () => {
const tempFileList = [...fileList.value]
tempFileList.forEach((item, index) => {
if (item.status === 'failed') {
removeFile(index)
}
})
}
// 重新上传文件
const retryUploadFile = (index: number) => {
const fileItem = fileList.value[index]
fileItem.status = 'ready'
fileItem.progress = 0
uploadFile(index, true)
}
// 重新上传全部文件
const retryAllUpload = () => {
// 查找出第一张上传失败的图片
const firstFailedFileIndex = fileList.value.findIndex(
(item) => item.status === 'failed'
)
uploadFile(firstFailedFileIndex)
}
// 手动上传文件
const customUploadHandle = () => {
if (!fileList.value.length) return
uploadFile(0)
}
// 移除文件
const removeFile = (index: number) => {
const fileItem = fileList.value[index]
// 如果文件正在上传中,取消上传
if (
fileItem.status === 'uploading' &&
fileItem.uploadTask &&
fileItem.progress > 0 &&
fileItem.progress < 100
) {
fileItem.uploadTask.abort()
}
fileList.value.splice(index, 1)
// 如果文件是已经完成的,重新计算上传成功的文件url
if (fileItem.status === 'done') {
emit('remove', fileItem.url)
uploadSuccessFileListChange()
}
}
// 删除文件
const removeFileEvent = (index: number) => {
const { disabled, beforeRemove } = props
if (disabled) return
// 获取待删除的文件
const fileItem = fileList.value[index]
if (!fileItem) return
uni.showModal({
title: '操作提示',
content: '确认需要移除该图片吗?',
showCancel: true,
cancelText: '取 消',
confirmText: '确 认',
success: (res) => {
if (res.confirm) {
// 删除前回调
if (!beforeRemove) {
removeFile(index)
return
}
const shouldRemove = beforeRemove(fileItem)
const isShouldRemovePromiseOrBoolean = [
isPromise(shouldRemove),
isBoolean(shouldRemove),
].includes(true)
if (!isShouldRemovePromiseOrBoolean) {
throwError(
'[TnImageUpload]',
'beforeRemove返回值必须是Promise或者Boolean'
)
}
if (isPromise(shouldRemove)) {
shouldRemove
.then((res) => {
if (res) removeFile(index)
})
.catch((err) => {
debugWarn('TnImageUpload', `beforeRemove出错: ${err}`)
})
} else {
if (shouldRemove) removeFile(index)
}
}
},
})
}
// 清空文件列表
const clearAllFile = () => {
// 如果文件正在上传中,取消上传
fileList.value.forEach((item) => {
if (
item.status === 'uploading' &&
item.uploadTask &&
item.progress > 0 &&
item.progress < 100
) {
item.uploadTask.abort()
}
})
fileList.value = []
uploadSuccessFileListChange()
}
// 点击图片预览图片
const previewImage = (index: number) => {
const previewImageList = fileList.value
.filter((item) => item.status === 'done')
.map((item) => item.url)
uni.previewImage({
current: index,
urls: previewImageList,
})
emit('preview', previewImageList[index])
}
return {
fileList,
isExceedMaxCount,
chooseFile,
retryUploadFile,
retryAllUpload,
customUploadHandle,
removeFileEvent,
clearAllFile,
previewImage,
}
}