更新
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:custom-request="customUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:accept="accept"
|
||||
:max-count="maxCount"
|
||||
:disabled="disabled"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: !disabled }"
|
||||
@preview="handlePreview"
|
||||
@change="handleChange"
|
||||
@drop="handleDrop"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
class="custom-upload"
|
||||
:class="{ 'drag-over': isDragOver }"
|
||||
>
|
||||
<div v-if="fileList.length < maxCount && !disabled" class="upload-area">
|
||||
<loading-outlined v-if="uploading" class="upload-icon" />
|
||||
<plus-outlined v-else class="upload-icon" />
|
||||
<div class="ant-upload-text">{{ uploading ? '上传中...' : uploadText }}</div>
|
||||
<div v-if="tip" class="ant-upload-tip">{{ tip }}</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<a-modal
|
||||
:open="previewVisible"
|
||||
:title="previewTitle"
|
||||
:footer="null"
|
||||
:width="800"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<img alt="图片预览" style="width: 100%; max-height: 600px; object-fit: contain;" :src="previewImage" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import uploadConfig from '@/config/upload'
|
||||
|
||||
const props = defineProps({
|
||||
// 图片列表
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
default: () => []
|
||||
},
|
||||
// 最大上传数量,默认1为单图上传
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 接受的文件类型
|
||||
accept: {
|
||||
type: String,
|
||||
default: 'image/*'
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否返回URL字符串(单图)或URL数组(多图)
|
||||
returnUrl: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 上传按钮文字
|
||||
uploadText: {
|
||||
type: String,
|
||||
default: '上传图片'
|
||||
},
|
||||
// 提示文字
|
||||
tip: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 最小宽度(像素)
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大宽度(像素)
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最小高度(像素)
|
||||
minHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大高度(像素)
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 是否删除前确认
|
||||
confirmBeforeRemove: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 自定义上传按钮内容
|
||||
customUploadBtn: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'preview', 'remove', 'uploadSuccess', 'uploadError'])
|
||||
|
||||
// 文件列表
|
||||
const fileList = ref([])
|
||||
|
||||
// 预览相关
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
const previewTitle = computed(() => {
|
||||
return previewImage.value ? '图片预览' : ''
|
||||
})
|
||||
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
|
||||
// 拖拽状态
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// 初始化文件列表
|
||||
const initFileList = () => {
|
||||
if (props.modelValue) {
|
||||
if (typeof props.modelValue === 'string') {
|
||||
// 单图上传,字符串格式
|
||||
fileList.value = props.modelValue
|
||||
? [
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'image.png',
|
||||
status: 'done',
|
||||
url: props.modelValue
|
||||
}
|
||||
]
|
||||
: []
|
||||
} else if (Array.isArray(props.modelValue)) {
|
||||
// 多图上传,数组格式
|
||||
fileList.value = props.modelValue.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: `image${index}.png`,
|
||||
status: 'done',
|
||||
url: url
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
fileList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
initFileList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 自定义上传
|
||||
const customUpload = (options) => {
|
||||
const { file, onProgress, onSuccess, onError } = options
|
||||
const formData = new FormData()
|
||||
formData.append(uploadConfig.filename || 'file', file)
|
||||
|
||||
uploading.value = true
|
||||
|
||||
uploadConfig.apiObj(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
onProgress({ percent }, file)
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
const data = uploadConfig.parseData(res)
|
||||
if (data.code === uploadConfig.successCode) {
|
||||
onSuccess(data, file)
|
||||
message.success('上传成功')
|
||||
emit('uploadSuccess', data, file)
|
||||
} else {
|
||||
onError(new Error(data.msg || '上传失败'))
|
||||
message.error(data.msg || '上传失败')
|
||||
emit('uploadError', data.msg || '上传失败', file)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
onError(error)
|
||||
message.error('上传失败:' + error.message)
|
||||
emit('uploadError', error.message, file)
|
||||
})
|
||||
.finally(() => {
|
||||
uploading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = async (file) => {
|
||||
// 文件大小校验
|
||||
const maxSizeMB = uploadConfig.maxSize || 10
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024
|
||||
|
||||
if (file.size > maxSizeBytes) {
|
||||
message.error(`图片大小不能超过 ${maxSizeMB}MB`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 图片尺寸校验
|
||||
if (props.minWidth || props.maxWidth || props.minHeight || props.maxHeight) {
|
||||
try {
|
||||
const dimensions = await getImageDimensions(file)
|
||||
const { width, height } = dimensions
|
||||
|
||||
if (props.minWidth && width < props.minWidth) {
|
||||
message.error(`图片宽度不能小于 ${props.minWidth}px`)
|
||||
return false
|
||||
}
|
||||
if (props.maxWidth && width > props.maxWidth) {
|
||||
message.error(`图片宽度不能大于 ${props.maxWidth}px`)
|
||||
return false
|
||||
}
|
||||
if (props.minHeight && height < props.minHeight) {
|
||||
message.error(`图片高度不能小于 ${props.minHeight}px`)
|
||||
return false
|
||||
}
|
||||
if (props.maxHeight && height > props.maxHeight) {
|
||||
message.error(`图片高度不能大于 ${props.maxHeight}px`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('图片尺寸校验失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
const getImageDimensions = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.src = e.target.result
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height })
|
||||
}
|
||||
img.onerror = reject
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理预览
|
||||
const handlePreview = async (file) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj)
|
||||
}
|
||||
previewImage.value = file.url || file.preview
|
||||
previewVisible.value = true
|
||||
emit('preview', file)
|
||||
}
|
||||
|
||||
// 获取Base64
|
||||
const getBase64 = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理文件列表变化
|
||||
const handleChange = ({ fileList: newFileList }) => {
|
||||
// 更新文件列表,确保上传成功的文件有正确的 url
|
||||
const updatedFileList = newFileList.map((file) => {
|
||||
// 如果文件上传成功且有响应数据但没有 url,则设置 url
|
||||
if (file.status === 'done' && file.response?.src && !file.url) {
|
||||
return {
|
||||
...file,
|
||||
url: file.response.src
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
|
||||
fileList.value = updatedFileList
|
||||
|
||||
// 过滤掉失败的文件
|
||||
const validFileList = updatedFileList.filter((file) => file.status !== 'error')
|
||||
|
||||
// 提取成功的文件URL
|
||||
const successFiles = validFileList
|
||||
.filter((file) => file.status === 'done' && (file.url || file.response?.src))
|
||||
.map((file) => file.url || file.response?.src)
|
||||
|
||||
// 触发更新事件
|
||||
if (props.returnUrl) {
|
||||
// 返回URL字符串或数组
|
||||
const value = props.maxCount === 1 ? successFiles[0] || '' : successFiles
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value, validFileList)
|
||||
} else {
|
||||
// 返回完整文件列表
|
||||
emit('update:modelValue', validFileList)
|
||||
emit('change', validFileList)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽相关
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
// 取消预览
|
||||
const handleCancel = () => {
|
||||
previewVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 24px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.ant-upload-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
border: 2px dashed #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
|
||||
.drag-over .upload-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user