基于Cropper与element plus图片裁剪插件封装,支持图片回显,图片重传等 电脑版发表于:2025/7/7 16:38 [TOC] ### 封装的组件如下 ``` <template> <div class="image-cropper-container"> <!-- 虚线框上传区域 --> <div class="upload-area" @click="triggerFileInput" :class="{ 'uploaded': croppedImage || imageUrl }" > <img v-if="croppedImage || imageUrl" :src="croppedImage || getImgUrl(imageUrl)" class="preview-image" alt="预览图" /> <div v-else class="upload-placeholder"> <el-icon class="upload-icon"><Plus /></el-icon> <div class="upload-text">点击上传图片</div> </div> </div> <input type="file" ref="fileInputRef" accept="image/*" class="hidden-input" @change="handleFileUpload" /> <!-- Element Plus 弹窗 --> <el-dialog v-model="showModal" title="图片裁剪" width="80%" :before-close="closeModal" append-to-body destroy-on-close > <div class="dialog-content"> <div class="fixed-height-cropper"> <img ref="cropperImageRef" :src="imageSrc" alt="裁剪图片"> </div> </div> <div class="dialog-footer"> <div class="controls"> <el-button-group> <el-button @click="rotate(-90)">左转</el-button> <el-button @click="rotate(90)">右转</el-button> </el-button-group> <el-button-group> <el-button @click="zoom(0.1)">放大</el-button> <el-button @click="zoom(-0.1)">缩小</el-button> </el-button-group> <el-button @click="resetCrop">重置</el-button> <el-button @click="reselectImage">重新选择</el-button> <el-button type="primary" @click="confirmCrop"> 确定裁剪 </el-button> <el-button @click="closeModal">取消</el-button> </div> </div> </el-dialog> </div> </template> <script setup> import { ref, nextTick, onMounted, defineProps, defineEmits, watch } from 'vue'; import Cropper from 'cropperjs'; import 'cropperjs/dist/cropper.min.css'; import { ElMessageBox, ElMessage } from 'element-plus'; import { Plus } from '@element-plus/icons-vue'; import { getImgUrl } from '/@/utils/toolsFunctions'; // 响应式变量声明 const imageSrc = ref(''); const showModal = ref(false); const croppedImage = ref(''); const fileInputRef = ref(null); const cropperImageRef = ref(null); const cropperInstance = ref(null); const uploadInProgress = ref(false); const initialCropData = ref(null); const isReselecting = ref(false); // 新增:重新选择状态标记 const props = defineProps({ value: { type: [String, Number], default: '' }, config: { type: Object, default: () => ({ aspectRatio: 1, viewMode: 1, dragMode: 'move', autoCropArea: 0.8, responsive: true, restore: false }) }, uploadUrl: { type: String, required: true }, uploadHeaders: { type: Object, default: () => ({}) }, uploadParams: { type: Object, default: () => ({}) }, // 固定裁剪框高度 cropperHeight: { type: String, default: '400px' } }); const emit = defineEmits(['update:value', 'upload-success', 'upload-error']); // 初始化处理 onMounted(() => { if (props.value) { imageUrl.value = props.value; } }); const imageUrl = ref(props.value); // 监听外部值变化 watch(() => props.value, (newVal) => { if (newVal && newVal !== imageUrl.value) { imageUrl.value = newVal; croppedImage.value = ''; } }); // 初始化Cropper const initializeCropper = () => { if (!imageSrc.value || !cropperImageRef.value) return; nextTick(() => { // 销毁现有实例 if (cropperInstance.value) { cropperInstance.value.destroy(); } cropperInstance.value = new Cropper(cropperImageRef.value, { ...props.config, ready() { console.log('Cropper initialized'); // 保存初始裁剪状态,用于重置 initialCropData.value = cropperInstance.value.getData(); // 隐藏重新选择标记 isReselecting.value = false; }, cropend() { console.log('Crop action ended'); } }); }); }; // 文件上传处理 const handleFileUpload = (e) => { const file = e.target.files[0]; if (!file) { // 用户取消选择,恢复状态 if (isReselecting.value) { isReselecting.value = false; } return; } // 检查文件类型和大小 const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'; if (!isJPG) { ElMessage.error('上传图片只能是 JPG/PNG 格式!'); return; } const isLt2M = file.size / 1024 / 1024 < 2; if (!isLt2M) { ElMessage.error('上传图片大小不能超过 2MB!'); return; } // 显示重新选择状态 isReselecting.value = true; const reader = new FileReader(); reader.onload = (event) => { // 保存当前图片源 const prevImageSrc = imageSrc.value; // 更新图片源 imageSrc.value = event.target.result; // 如果弹窗未打开,则打开 if (!showModal.value) { showModal.value = true; } // 销毁旧的Cropper实例 if (cropperInstance.value) { cropperInstance.value.destroy(); cropperInstance.value = null; } // 初始化新的Cropper实例 nextTick(() => { initializeCropper(); }); }; reader.readAsDataURL(file); }; // 触发文件输入 const triggerFileInput = () => { if (uploadInProgress.value) return; fileInputRef.value?.click(); }; // 旋转图片 const rotate = (degrees) => { cropperInstance.value?.rotate(degrees); }; // 缩放图片 const zoom = (ratio) => { cropperInstance.value?.zoom(ratio); }; // 重置裁剪 const resetCrop = () => { if (cropperInstance.value && initialCropData.value) { cropperInstance.value.setData(initialCropData.value); } }; // 重新选择图片 - 优化版 const reselectImage = () => { // 标记为重新选择状态 isReselecting.value = true; // 清空文件输入 fileInputRef.value.value = ''; // 触发文件选择 triggerFileInput(); }; // 关闭弹窗 const closeModal = () => { showModal.value = false; // 清理资源 if (cropperInstance.value) { cropperInstance.value.destroy(); cropperInstance.value = null; } // 重置状态 imageSrc.value = ''; isReselecting.value = false; }; // 确认裁剪并上传 const confirmCrop = async () => { if (!cropperInstance.value) { ElMessage.error('请先选择图片'); return; } try { uploadInProgress.value = true; const canvas = cropperInstance.value.getCroppedCanvas({ width: props.config.cropWidth || 800, height: props.config.cropHeight || 800 }); canvas.toBlob(async (blob) => { const formData = new FormData(); formData.append('file', blob, `image_${Date.now()}.png`); // 添加额外的上传参数 Object.entries(props.uploadParams).forEach(([key, value]) => { formData.append(key, value); }); // 执行真实上传 const result = await uploadFile(formData); if (result.success) { croppedImage.value = canvas.toDataURL('image/png'); const imageId = result.data?.id || result.id; if (imageId) { imageUrl.value = imageId; emit('update:value', imageId); emit('upload-success', result); ElMessage.success('图片上传成功'); } else { ElMessage.error('上传成功但未获取到图片ID'); } } else { emit('upload-error', result); ElMessage.error(result.message || '上传失败'); } closeModal(); }, 'image/png'); } catch (error) { emit('upload-error', error); ElMessage.error('裁剪失败,请重试'); console.error('裁剪失败:', error); } finally { uploadInProgress.value = false; } }; // 执行真实的文件上传 const uploadFile = async (formData) => { try { const response = await fetch(props.uploadUrl, { method: 'POST', headers: props.uploadHeaders, body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('上传失败:', error); return { success: false, message: error.message }; } }; </script> <style scoped> .hidden-input { display: none; } .upload-area { border: 1px dashed #dcdfe6; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; width: 200px; height: 150px; display: flex; justify-content: center; align-items: center; transition: border-color 0.3s; } .upload-area:hover { border-color: #409eff; } .uploaded { border-color: #409eff; } .preview-image { max-width: 100%; max-height: 100%; object-fit: contain; } .upload-placeholder { display: flex; flex-direction: column; align-items: center; color: #8c939d; } .upload-icon { font-size: 28px; margin-bottom: 8px; } .dialog-content { padding: 20px; } .dialog-footer { padding: 15px 20px; border-top: 1px solid #ebeef5; } .controls { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } .el-button-group { margin-right: 15px; } /* 固定高度的裁剪容器 */ .fixed-height-cropper { height: var(--cropper-height, 400px); display: flex; justify-content: center; align-items: center; overflow: hidden; border: 1px solid #eee; } .fixed-height-cropper img { max-height: 100%; object-fit: contain; } </style> ``` ### 使用方式如下 ``` <el-form-item label="封面" prop="CoverImg"> <image-cropper v-model="editFromData.coverImg" :config="cropperConfig" :upload-headers="getHeaderToken()" upload-url="/oss/api/SmartFiles/UpLoadFormFile" :cropper-height="'366px'" :upload-params="{ bucketName: 'teacher-certification', // admin存储后台的文件数据,client 存储客户端的数据 filePath: 'admin/', FileType: 1 }" /> </el-form-item> <script setup lang="ts"> const cropperConfig = { autoCropArea: 1, aspectRatio: 83 / 46, cropWidth: 800, cropHeight: 250 } </script> ```