<script setup>
|
import {Plus} from '@element-plus/icons-vue'
|
import {uploadFile} from '@/api/basicData/common'
|
|
const props = defineProps({
|
fileList: {
|
type: Array,
|
default: () => [],
|
},
|
index: {
|
type: Number,
|
default: -1,
|
},
|
childrenKey: {
|
type: String,
|
default: 'images',
|
},
|
limit: {
|
type: Number,
|
default: 10,
|
},
|
fileSize: {
|
type: Number,
|
default: 10,
|
},
|
fileType: {
|
type: Array,
|
default: () => ['png', 'jpg', 'jpeg', 'webp'],
|
},
|
buttonText: {
|
type: String,
|
default: '上传图片',
|
},
|
disabled: {
|
type: Boolean,
|
default: false,
|
},
|
uploadFieldName: {
|
type: String,
|
default: 'files',
|
},
|
})
|
|
const emit = defineEmits(['update:fileList', 'change'])
|
const {proxy} = getCurrentInstance()
|
|
const uploadRef = ref()
|
const previewVisible = ref(false)
|
const previewUrl = ref('')
|
const uploadQueueTimer = ref(null)
|
const uploading = ref(false)
|
const queuedUidSet = ref(new Set())
|
|
const currentList = computed({
|
get() {
|
if (props.index > -1) {
|
const row = props.fileList?.[props.index]
|
return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
|
}
|
return Array.isArray(props.fileList) ? props.fileList : []
|
},
|
set(value) {
|
const nextList = Array.isArray(value) ? value : []
|
if (props.index > -1) {
|
const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
|
const currentRow = nextModelValue[props.index] || {}
|
nextModelValue[props.index] = {
|
...currentRow,
|
[props.childrenKey]: nextList,
|
}
|
emit('update:fileList', nextModelValue)
|
emit('change', nextList, nextModelValue)
|
return
|
}
|
emit('update:fileList', nextList)
|
emit('change', nextList, nextList)
|
},
|
})
|
|
const displayFileList = computed(() => {
|
return currentList.value.map((item, index) => ({
|
uid: getItemUid(item, index),
|
name: getItemName(item, index),
|
url: getItemUrl(item),
|
status: 'success',
|
rawData: item,
|
}))
|
})
|
|
const uploadTip = computed(() => {
|
return `支持 ${props.fileType.join('/')},单张不超过 ${props.fileSize}MB,最多上传 ${props.limit} 张图片`
|
})
|
|
function getItemUid(item, index) {
|
if (item?.id !== undefined && item?.id !== null) return `${item.id}`
|
return `${getItemName(item, index)}-${getItemUrl(item) || index}`
|
}
|
|
function getItemUrl(item) {
|
if (!item) return ''
|
if (typeof item === 'string') return item
|
return item.url || item.previewURL || ''
|
}
|
|
function getItemName(item, index = 0) {
|
if (!item) return `image-${index + 1}`
|
if (typeof item === 'string') return `image-${index + 1}`
|
return item.name || item.fileName || item.originalFilename || `image-${index + 1}`
|
}
|
|
function normalizeResponseItem(item, index) {
|
if (typeof item === 'string') {
|
return {
|
name: `image-${currentList.value.length + index + 1}`,
|
url: item,
|
}
|
}
|
return Object.assign({}, item, {
|
url: item.url || item.previewURL || item.previewUrl || '',
|
name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`,
|
})
|
}
|
|
function extractResponseArray(response) {
|
if (Array.isArray(response)) return response
|
if (Array.isArray(response?.data)) return response.data
|
if (Array.isArray(response?.data?.data)) return response.data.data
|
if (Array.isArray(response?.payload)) return response.payload
|
if (Array.isArray(response?.payload?.data)) return response.payload.data
|
if (Array.isArray(response?.rows)) return response.rows
|
if (Array.isArray(response?.result)) return response.result
|
return []
|
}
|
|
function validateFile(rawFile) {
|
let isValidType = false
|
const extension = rawFile.name.includes('.')
|
? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
|
: ''
|
|
if (props.fileType.length) {
|
isValidType = props.fileType.some((type) => {
|
const normalizedType = String(type).toLowerCase()
|
return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
|
})
|
} else {
|
isValidType = rawFile.type.includes('image')
|
}
|
|
if (!isValidType) {
|
proxy.$modal.msgError(`请上传 ${props.fileType.join('/')} 格式的图片`)
|
return false
|
}
|
|
const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
|
if (!isWithinSize) {
|
proxy.$modal.msgError(`图片大小不能超过 ${props.fileSize}MB`)
|
return false
|
}
|
|
return true
|
}
|
|
function scheduleUpload(uploadFiles) {
|
clearTimeout(uploadQueueTimer.value)
|
uploadQueueTimer.value = setTimeout(() => {
|
const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
|
if (!readyFiles.length) return
|
|
const remainCount = props.limit - currentList.value.length
|
if (remainCount <= 0) {
|
proxy.$modal.msgError(`最多上传 ${props.limit} 张图片`)
|
uploadRef.value?.clearFiles()
|
return
|
}
|
|
const selectedFiles = readyFiles.slice(0, remainCount)
|
if (selectedFiles.length < readyFiles.length) {
|
proxy.$modal.msgWarning(`最多上传 ${props.limit} 张图片,超出部分已忽略`)
|
}
|
|
selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
|
uploadSelectedFiles(selectedFiles)
|
}, 0)
|
}
|
|
async function uploadSelectedFiles(files) {
|
const validFiles = files.filter((file) => validateFile(file.raw))
|
const invalidFiles = files.filter((file) => !validFiles.includes(file))
|
|
invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
|
|
if (!validFiles.length) {
|
uploadRef.value?.clearFiles()
|
return
|
}
|
|
const formData = new FormData()
|
validFiles.forEach((file) => {
|
formData.append(props.uploadFieldName, file.raw)
|
})
|
|
uploading.value = true
|
proxy.$modal.loading('图片上传中,请稍候...')
|
|
try {
|
const response = await uploadFile(formData)
|
const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
|
|
if (!responseList.length) {
|
proxy.$modal.msgError('上传接口未返回数组数据')
|
return
|
}
|
console.log('responseList', responseList)
|
|
currentList.value = [...currentList.value, ...responseList]
|
console.log('currentList.value', currentList.value)
|
proxy.$modal.msgSuccess('上传成功')
|
} catch (error) {
|
proxy.$modal.msgError(error?.message || '上传失败')
|
} finally {
|
validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
|
invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
|
uploadRef.value?.clearFiles()
|
uploading.value = false
|
proxy.$modal.closeLoading()
|
}
|
}
|
|
function handleChange(file, uploadFiles) {
|
if (props.disabled || uploading.value) return
|
scheduleUpload(uploadFiles)
|
}
|
|
function handleRemove(file) {
|
const targetUrl = file.url || getItemUrl(file.rawData)
|
const nextList = currentList.value.filter((item, index) => {
|
const itemUrl = getItemUrl(item)
|
const itemName = getItemName(item, index)
|
return !(itemUrl === targetUrl && itemName === file.name)
|
})
|
currentList.value = nextList
|
}
|
|
function handlePreview(file) {
|
previewUrl.value = file.url || getItemUrl(file.rawData)
|
previewVisible.value = true
|
}
|
|
function handleExceed() {
|
proxy.$modal.msgError(`最多上传 ${props.limit} 张图片`)
|
}
|
|
onBeforeUnmount(() => {
|
clearTimeout(uploadQueueTimer.value)
|
})
|
</script>
|
|
<template>
|
<div class="attachment-upload-image">
|
<el-upload
|
ref="uploadRef"
|
:auto-upload="false"
|
:multiple="true"
|
:show-file-list="true"
|
:file-list="displayFileList"
|
list-type="picture-card"
|
accept="image/*"
|
:disabled="disabled || uploading"
|
:limit="limit"
|
:on-change="handleChange"
|
:on-remove="handleRemove"
|
:on-preview="handlePreview"
|
:on-exceed="handleExceed"
|
>
|
<div class="upload-trigger">
|
<el-icon>
|
<Plus/>
|
</el-icon>
|
<span>{{ buttonText }}</span>
|
</div>
|
</el-upload>
|
|
<div class="upload-tip">
|
{{ uploadTip }}
|
</div>
|
|
<el-dialog v-model="previewVisible" title="图片预览" width="720px" append-to-body>
|
<img class="preview-image" :src="previewUrl" alt="preview"/>
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped lang="scss">
|
.attachment-upload-image {
|
width: 100%;
|
}
|
|
.upload-trigger {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
gap: 6px;
|
color: var(--el-text-color-secondary);
|
font-size: 12px;
|
line-height: 1.2;
|
}
|
|
.upload-tip {
|
margin-top: 8px;
|
color: var(--el-text-color-secondary);
|
font-size: 12px;
|
}
|
|
.preview-image {
|
display: block;
|
max-width: 100%;
|
margin: 0 auto;
|
}
|
|
:deep(.el-upload-list--picture-card) {
|
margin: 0;
|
}
|
|
:deep(.el-upload--picture-card) {
|
width: 132px;
|
height: 132px;
|
}
|
|
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
width: 132px;
|
height: 132px;
|
}
|
</style>
|