Files
vueadmin/src/components/scUpload/index.vue
T
2026-01-20 21:29:33 +08:00

384 lines
8.5 KiB
Vue

<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>