初次提交
This commit is contained in:
345
src/components/ImageUpload.vue
Normal file
345
src/components/ImageUpload.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:data="uploadData"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:on-progress="handleProgress"
|
||||
:show-file-list="false"
|
||||
:auto-upload="true"
|
||||
accept="image/*"
|
||||
class="upload-container"
|
||||
>
|
||||
<div class="upload-area" :class="{ 'uploading': uploading, 'has-image': imageUrl }">
|
||||
<div v-if="!imageUrl && !uploading" class="upload-placeholder">
|
||||
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||
<div class="upload-text">{{ placeholder }}</div>
|
||||
<div class="upload-hint">支持 JPG、PNG 格式,大小不超过 5MB</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploading" class="upload-progress">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<div class="progress-text">上传中... {{ uploadProgress }}%</div>
|
||||
<el-progress :percentage="uploadProgress" :show-text="false" />
|
||||
</div>
|
||||
|
||||
<div v-if="imageUrl && !uploading" class="image-preview">
|
||||
<img :src="imageUrl" :alt="placeholder" class="preview-image" />
|
||||
<div class="image-overlay">
|
||||
<el-button type="primary" size="small" @click.stop="previewImage">
|
||||
<el-icon><ZoomIn /></el-icon>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click.stop="removeImage">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-dialog v-model="previewVisible" title="图片预览" width="60%" center>
|
||||
<div class="preview-dialog">
|
||||
<img :src="imageUrl" :alt="placeholder" class="preview-dialog-image" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Loading, ZoomIn, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '点击上传图片'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error'])
|
||||
|
||||
// 响应式数据
|
||||
const uploadRef = ref()
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const imageUrl = ref(props.modelValue)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = '/api/upload/image'
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
})
|
||||
const uploadData = { type: 'document' }
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
imageUrl.value = newValue
|
||||
})
|
||||
|
||||
/**
|
||||
* 上传前验证
|
||||
*/
|
||||
const beforeUpload = (file) => {
|
||||
if (props.disabled) {
|
||||
ElMessage.warning('当前状态下不允许上传')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件大小(5MB)
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传进度
|
||||
*/
|
||||
const handleProgress = (event) => {
|
||||
uploadProgress.value = Math.round(event.percent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传成功
|
||||
*/
|
||||
const handleSuccess = (response) => {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
|
||||
if (response.success) {
|
||||
imageUrl.value = response.data.url
|
||||
emit('update:modelValue', response.data.url)
|
||||
emit('upload-success', response.data)
|
||||
ElMessage.success('图片上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
emit('upload-error', response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传失败
|
||||
*/
|
||||
const handleError = (error) => {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error('图片上传失败,请重试')
|
||||
emit('upload-error', error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
*/
|
||||
const previewImage = () => {
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*/
|
||||
const removeImage = () => {
|
||||
imageUrl.value = ''
|
||||
emit('update:modelValue', '')
|
||||
ElMessage.success('图片已删除')
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发上传
|
||||
*/
|
||||
const triggerUpload = () => {
|
||||
if (!props.disabled) {
|
||||
uploadRef.value?.$el.querySelector('input').click()
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
triggerUpload
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.upload-area.uploading {
|
||||
border-color: #409eff;
|
||||
background-color: #f0f9ff;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.upload-area.has-image {
|
||||
border-color: #409eff;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
text-align: center;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 24px;
|
||||
color: #409eff;
|
||||
margin-bottom: 8px;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.image-preview:hover .image-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.preview-dialog {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-dialog-image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.upload-area {
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user