|
| 1 | +<template> |
| 2 | + <el-upload |
| 3 | + ref="uploadRef" |
| 4 | + :action="`${getBaseUrl()}/fileUploadAndDownload/upload`" |
| 5 | + accept="image/*" |
| 6 | + :show-file-list="false" |
| 7 | + :auto-upload="false" |
| 8 | + :data="{'classId': props.classId}" |
| 9 | + :on-success="handleImageSuccess" |
| 10 | + :on-change="handleFileChange" |
| 11 | + > |
| 12 | + <el-button type="primary" icon="crop"> 裁剪上传</el-button> |
| 13 | + </el-upload> |
| 14 | + |
| 15 | + <el-dialog v-model="dialogVisible" title="图片裁剪" width="1200px" append-to-body @close="dialogVisible = false" :close-on-click-modal="false" draggable> |
| 16 | + <div class="flex gap-[30px] h-[600px]"> |
| 17 | + <!-- 左侧编辑区 --> |
| 18 | + <div class="flex flex-col flex-1"> |
| 19 | + <div class="flex-1 bg-[#f8f8f8] rounded-lg overflow-hidden"> |
| 20 | + <VueCropper |
| 21 | + ref="cropperRef" |
| 22 | + :img="imgSrc" |
| 23 | + outputType="jpeg" |
| 24 | + :autoCrop="true" |
| 25 | + :autoCropWidth="cropWidth" |
| 26 | + :autoCropHeight="cropHeight" |
| 27 | + :fixedBox="false" |
| 28 | + :fixed="fixedRatio" |
| 29 | + :fixedNumber="fixedNumber" |
| 30 | + :centerBox="true" |
| 31 | + :canMoveBox="true" |
| 32 | + :full="false" |
| 33 | + :maxImgSize="1200" |
| 34 | + :original="true" |
| 35 | + @realTime="handleRealTime" |
| 36 | + ></VueCropper> |
| 37 | + </div> |
| 38 | + |
| 39 | + <!-- 工具栏 --> |
| 40 | + <div class="mt-[20px] flex items-center p-[10px] bg-white rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]"> |
| 41 | + <el-button-group> |
| 42 | + <el-tooltip content="向左旋转"> |
| 43 | + <el-button @click="rotate(-90)" :icon="RefreshLeft" /> |
| 44 | + </el-tooltip> |
| 45 | + <el-tooltip content="向右旋转"> |
| 46 | + <el-button @click="rotate(90)" :icon="RefreshRight" /> |
| 47 | + </el-tooltip> |
| 48 | + <el-button :icon="Plus" @click="changeScale(1)"></el-button> |
| 49 | + <el-button :icon="Minus" @click="changeScale(-1)"></el-button> |
| 50 | + </el-button-group> |
| 51 | + |
| 52 | + |
| 53 | + <el-select v-model="currentRatio" placeholder="选择比例" class="w-32 ml-4" @change="onCurrentRatio"> |
| 54 | + <el-option v-for="(item, index) in ratioOptions" :key="index" :label="item.label" :value="index" /> |
| 55 | + </el-select> |
| 56 | + </div> |
| 57 | + </div> |
| 58 | + |
| 59 | + <!-- 右侧预览区 --> |
| 60 | + <div class="w-[340px]"> |
| 61 | + <div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]"> |
| 62 | + <div class="mb-[15px] text-gray-600">裁剪预览</div> |
| 63 | + <div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]" |
| 64 | + :style="{'width': previews.w + 'px', 'height': previews.h + 'px'}" |
| 65 | + > |
| 66 | + <div class="w-full h-full relative overflow-hidden"> |
| 67 | + <img :src="previews.url" :style="previews.img" alt="" class="max-w-none absolute transition-all duration-300 ease-in-out image-render-pixelated origin-[0_0]" /> |
| 68 | + </div> |
| 69 | + </div> |
| 70 | + </div> |
| 71 | + </div> |
| 72 | + </div> |
| 73 | + <template #footer> |
| 74 | + <div class="dialog-footer"> |
| 75 | + <el-button @click="dialogVisible = false">取 消</el-button> |
| 76 | + <el-button type="primary" @click="handleUpload" :loading="uploading"> {{ uploading ? '上传中...' : '上 传' }} |
| 77 | + </el-button> |
| 78 | + </div> |
| 79 | + </template> |
| 80 | + </el-dialog> |
| 81 | +</template> |
| 82 | + |
| 83 | +<script setup> |
| 84 | +import { ref, getCurrentInstance } from 'vue' |
| 85 | +import { ElMessage } from 'element-plus' |
| 86 | +import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue' |
| 87 | +import 'vue-cropper/dist/index.css' |
| 88 | +import { VueCropper } from 'vue-cropper' |
| 89 | +import { getBaseUrl } from '@/utils/format' |
| 90 | +
|
| 91 | +defineOptions({ |
| 92 | + name: 'CropperImage' |
| 93 | +}) |
| 94 | +
|
| 95 | +const emit = defineEmits(['on-success']) |
| 96 | +
|
| 97 | +const props = defineProps({ |
| 98 | + classId: { |
| 99 | + type: Number, |
| 100 | + default: 0 |
| 101 | + } |
| 102 | +}) |
| 103 | +
|
| 104 | +const uploadRef = ref(null) |
| 105 | +// 响应式数据 |
| 106 | +const dialogVisible = ref(false) |
| 107 | +const imgSrc = ref('') |
| 108 | +const cropperRef = ref(null) |
| 109 | +const { proxy } = getCurrentInstance() |
| 110 | +const previews = ref({}) |
| 111 | +const uploading = ref(false) |
| 112 | +
|
| 113 | +// 缩放控制 |
| 114 | +const changeScale = (value) => { |
| 115 | + proxy.$refs.cropperRef.changeScale(value) |
| 116 | +} |
| 117 | +
|
| 118 | +// 比例预设 |
| 119 | +const ratioOptions = ref([ |
| 120 | + { label: '1:1', value: [1, 1] }, |
| 121 | + { label: '16:9', value: [16, 9] }, |
| 122 | + { label: '9:16', value: [9, 16] }, |
| 123 | + { label: '4:3', value: [4, 3] }, |
| 124 | + { label: '自由比例', value: [] } |
| 125 | +]) |
| 126 | +
|
| 127 | +const fixedNumber = ref([1, 1]) |
| 128 | +const cropWidth = ref(300) |
| 129 | +const cropHeight = ref(300) |
| 130 | +
|
| 131 | +const fixedRatio = ref(false) |
| 132 | +const currentRatio = ref(4) |
| 133 | +const onCurrentRatio = () => { |
| 134 | + fixedNumber.value = ratioOptions.value[currentRatio.value].value |
| 135 | + switch (currentRatio.value) { |
| 136 | + case 0: |
| 137 | + cropWidth.value = 300 |
| 138 | + cropHeight.value = 300 |
| 139 | + fixedRatio.value = true |
| 140 | + break |
| 141 | + case 1: |
| 142 | + cropWidth.value = 300 |
| 143 | + cropHeight.value = 300 * 9 / 16 |
| 144 | + fixedRatio.value = true |
| 145 | + break |
| 146 | + case 2: |
| 147 | + cropWidth.value = 300 * 9 / 16 |
| 148 | + cropHeight.value = 300 |
| 149 | + fixedRatio.value = true |
| 150 | + break |
| 151 | + case 3: |
| 152 | + cropWidth.value = 300 |
| 153 | + cropHeight.value = 300 * 3 / 4 |
| 154 | + fixedRatio.value = true |
| 155 | + break |
| 156 | + default: |
| 157 | + cropWidth.value = 300 |
| 158 | + cropHeight.value = 300 |
| 159 | + fixedRatio.value = false |
| 160 | + } |
| 161 | +} |
| 162 | +
|
| 163 | +// 文件处理 |
| 164 | +const handleFileChange = (file) => { |
| 165 | + const isImage = file.raw.type.includes('image') |
| 166 | + if (!isImage) { |
| 167 | + ElMessage.error('请选择图片文件') |
| 168 | + return |
| 169 | + } |
| 170 | +
|
| 171 | + if (file.raw.size / 1024 / 1024 > 8) { |
| 172 | + ElMessage.error('文件大小不能超过8MB!') |
| 173 | + return false |
| 174 | + } |
| 175 | +
|
| 176 | + const reader = new FileReader() |
| 177 | + reader.onload = (e) => { |
| 178 | + imgSrc.value = e.target.result |
| 179 | + dialogVisible.value = true |
| 180 | + } |
| 181 | + reader.readAsDataURL(file.raw) |
| 182 | +} |
| 183 | +
|
| 184 | +// 旋转控制 |
| 185 | +const rotate = (degree) => { |
| 186 | + if (degree === -90) { |
| 187 | + proxy.$refs.cropperRef.rotateLeft() |
| 188 | + } else { |
| 189 | + proxy.$refs.cropperRef.rotateRight() |
| 190 | + } |
| 191 | +} |
| 192 | +
|
| 193 | +// 实时预览 |
| 194 | +const handleRealTime = (data) => { |
| 195 | + previews.value = data |
| 196 | + //console.log(data) |
| 197 | +} |
| 198 | +
|
| 199 | +// 上传处理 |
| 200 | +const handleUpload = () => { |
| 201 | + uploading.value = true |
| 202 | + proxy.$refs.cropperRef.getCropBlob((blob) => { |
| 203 | + try { |
| 204 | + const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' }) |
| 205 | + uploadRef.value.clearFiles() |
| 206 | + uploadRef.value.handleStart(file) |
| 207 | + uploadRef.value.submit() |
| 208 | +
|
| 209 | + } catch (error) { |
| 210 | + uploading.value = false |
| 211 | + ElMessage.error('上传失败: ' + error.message) |
| 212 | + } |
| 213 | + }) |
| 214 | +} |
| 215 | +
|
| 216 | +const handleImageSuccess = (res) => { |
| 217 | + const { data } = res |
| 218 | + if (data) { |
| 219 | + setTimeout(() => { |
| 220 | + uploading.value = false |
| 221 | + dialogVisible.value = false |
| 222 | + previews.value = {} |
| 223 | + ElMessage.success('上传成功') |
| 224 | + emit('on-success', data.url) |
| 225 | + }, 1000) |
| 226 | + } |
| 227 | +} |
| 228 | +
|
| 229 | +</script> |
| 230 | + |
| 231 | +<style scoped> |
| 232 | +:deep(.vue-cropper) { |
| 233 | + background: transparent; |
| 234 | +} |
| 235 | +</style> |
0 commit comments