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([]) // 监听文件列表变化 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( () => fileList.value.length >= props.limit ) // 当前剩余可选文件数量 const currentRemainFileCount = computed(() => { 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((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, } }