Appearance
实现 图片处理工具
分类:developer | 标签:图片、格式转换、裁剪、旋转、工具 在线图片处理,支持格式转换、裁剪、旋转、翻转、缩放等操作
2.1 功能说明
在线图片处理,支持格式转换、裁剪、旋转、翻转、缩放等操作
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
本工具目录 ImageProcessor/ 下除 index.vue 外,还包含以下源文件:
BgColorTool.vue(226 行)CropTool.vue(312 行)FormatConvert.vue(88 行)HelpTab.vue(149 行)ImageCanvas.vue(655 行)MosaicTool.vue(431 行)ResizeTool.vue(90 行)RotateFlip.vue(90 行)WatermarkTool.vue(135 行)bgSegmenter.js(224 行)
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
onCompressFiles()loadImageEl()compressOne()canvasToBlob()compressAll()downloadOne()downloadZip()clearCompress()makeZip()crcTable()crc32()formatFileSize()triggerUpload()handleFileSelect()handleDrop()loadImage()renderCanvas()updateOutputSize()resetTransformState()drawWatermark()drawSingle()onFormatChange()onRotateFlipChange()onResizeChange()onWatermarkChange()onWatermarkDragToggle()onWatermarkDrag()onMosaicChange()onMosaicSelectToggle()onMosaicRegionAdd()onMosaicRegionUpdate()onMosaicRegionRemove()onMosaicRegionsChange()onApplyMosaic()applyMosaicToCanvas()applyMosaicEffect()applyBlurEffect()applySolidEffect()onCropStart()onCropCancel()onCropApplied()onCropRatioChange()onApplyBgColor()onCropMouseDown()onCropMouseMove()onCropMouseUp()downloadImage()resetAll()removeImage()goBack()handleKey()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, reactive, nextTick, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowLeft,
Upload,
View,
Delete,
RefreshRight,
Download,
Tools
} from '@element-plus/icons-vue'
import FeedbackForm from '@/components/FeedbackForm.vue'
import HelpTab from './HelpTab.vue'
import FormatConvert from './FormatConvert.vue'
import CropTool from './CropTool.vue'
import RotateFlip from './RotateFlip.vue'
import ResizeTool from './ResizeTool.vue'
import ImageCanvas from './ImageCanvas.vue'
import WatermarkTool from './WatermarkTool.vue'
import MosaicTool from './MosaicTool.vue'
import BgColorTool from './BgColorTool.vue'
const router = useRouter()
const fileInputRef = ref(null)
const imageCanvasRef = ref(null)
const formatConvertRef = ref(null)
const cropToolRef = ref(null)
const rotateFlipRef = ref(null)
const resizeToolRef = ref(null)
const watermarkToolRef = ref(null)
const mosaicToolRef = ref(null)
const bgColorToolRef = ref(null)
const activeTab = ref('editor')
const activeFunc = ref('format')
const previewUrl = ref(null)
const originalImage = ref(null)
const isWatermarkDragging = ref(false)
const watermarkPos = reactive({ x: 50, y: 50 })
const isMosaicSelecting = ref(false)
const mosaicRegions = ref([])
const originalInfo = reactive({ width: 0, height: 0, format: '', size: '' })
const outputInfo = reactive({ format: '', size: '' })
const functions = [
{ key: 'format', icon: '🔄', label: '格式' },
{ key: 'crop', icon: '✂️', label: '裁剪' },
{ key: 'rotate', icon: '🔃', label: '旋转' },
{ key: 'resize', icon: '📐', label: '缩放' },
{ key: 'watermark', icon: '💧', label: '水印' },
{ key: 'mosaic', icon: '🔒', label: '打码' },
{ key: 'bgColor', icon: '🎨', label: '换底' },
]
// ---- 批量压缩(独立 Tab,不依赖编辑器画布管线)----
const cmpFmt = ref('image/webp')
const cmpQuality = ref(0.8)
const cmpMax = ref(0)
const cmpTarget = ref(0)
const cmpFiles = ref([])
const cmpResults = ref([])
const cmpErr = ref('')
function onCompressFiles(e) {
const list = Array.from(e.target.files || [])
if (!list.length) return
cmpFiles.value = list.map(f => ({ name: f.name, file: f, size: f.size }))
cmpResults.value = []
cmpErr.value = ''
}
function loadImageEl(file) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file)
const img = new Image()
img.onload = () => resolve({ img, url })
img.onerror = reject
img.src = url
})
}
async function compressOne(item) {
const { img, url } = await loadImageEl(item.file)
let w = img.width, h = img.height
if (cmpMax.value > 0 && Math.max(w, h) > cmpMax.value) {
const s = cmpMax.value / Math.max(w, h)
w = Math.max(1, Math.round(w * s)); h = Math.max(1, Math.round(h * s))
}
const canvas = document.createElement('canvas')
canvas.width = w; canvas.height = h
canvas.getContext('2d').drawImage(img, 0, 0, w, h)
URL.revokeObjectURL(url)
let quality = cmpQuality.value
if (cmpTarget.value > 0 && cmpFmt.value !== 'image/png') {
// 二分搜索质量以逼近目标体积
let lo = 0.1, hi = 1
for (let i = 0; i < 8; i++) {
quality = (lo + hi) / 2
const blob = await canvasToBlob(canvas, cmpFmt.value, quality)
if (!blob) break
if (blob.size <= cmpTarget.value * 1024) lo = quality
else hi = quality
}
}
const blob = await canvasToBlob(canvas, cmpFmt.value, quality)
if (!blob) throw new Error(item.name + ':当前浏览器不支持该格式导出')
const compSize = blob.size
const ratio = Math.round((1 - compSize / item.size) * 100)
return {
name: item.name,
orig: formatFileSize(item.size),
cmp: formatFileSize(compSize),
ratio,
url: URL.createObjectURL(blob),
blob,
compSize
}
}
function canvasToBlob(canvas, type, quality) {
return new Promise(resolve => {
try { canvas.toBlob(b => resolve(b), type, quality) }
catch { resolve(null) }
})
}
async function compressAll() {
cmpErr.value = ''
const results = []
let failed = []
for (const item of cmpFiles.value) {
try { results.push(await compressOne(item)) }
catch (e) { failed.push(e.message) }
}
cmpResults.value = results
if (failed.length) cmpErr.value = '部分失败:' + failed.join(';')
else if (results.length) ElMessage.success(`压缩完成,共 ${results.length} 张`)
}
function downloadOne(row) {
const a = document.createElement('a')
a.href = row.url; a.download = 'compressed_' + row.name; a.click()
}
function downloadZip() {
if (!cmpResults.value.length) return
Promise.all(cmpResults.value.map(async r => ({
name: 'compressed_' + r.name,
bytes: new Uint8Array(await r.blob.arrayBuffer())
}))).then(arr => {
const zip = makeZip(arr)
const blob = new Blob([zip], { type: 'application/zip' })
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'compressed.zip'; a.click()
URL.revokeObjectURL(a.href)
})
}
function clearCompress() {
cmpResults.value.forEach(r => URL.revokeObjectURL(r.url))
cmpFiles.value = []; cmpResults.value = []; cmpErr.value = ''
}
// 极简 store 模式 ZIP(无压缩),仅用于本地打包下载
function makeZip(files) {
const crcTable = (() => {
const t = new Uint32Array(256)
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)
t[n] = c >>> 0
}
return t
})()
const crc32 = (u8) => {
let c = 0xFFFFFFFF
for (let i = 0; i < u8.length; i++) c = crcTable[(c ^ u8[i]) & 0xFF] ^ (c >>> 8)
return (c ^ 0xFFFFFFFF) >>> 0
}
const enc = new TextEncoder()
const chunks = []
let offset = 0
const localOffsets = []
for (const f of files) {
const nameBytes = enc.encode(f.name)
const data = f.bytes
const crc = crc32(data)
localOffsets.push(offset)
const header = new DataView(new ArrayBuffer(30))
header.setUint32(0, 0x04034b50, true)
header.setUint16(4, 20, true)
header.setUint16(6, 0, true)
header.setUint16(8, 0, true)
header.setUint16(10, 0, true)
header.setUint16(12, 0, true)
header.setUint32(14, crc, true)
header.setUint32(18, data.length, true)
header.setUint32(22, data.length, true)
header.setUint16(26, nameBytes.length, true)
header.setUint16(28, 0, true)
chunks.push(new Uint8Array(header.buffer), nameBytes, data)
offset += 30 + nameBytes.length + data.length
}
const centralStart = offset
let centralSize = 0
for (let i = 0; i < files.length; i++) {
const f = files[i]
const nameBytes = enc.encode(f.name)
const data = f.bytes
const crc = crc32(data)
const rec = new DataView(new ArrayBuffer(46))
rec.setUint32(0, 0x02014b50, true)
rec.setUint16(4, 20, true)
rec.setUint16(6, 20, true)
rec.setUint16(8, 0, true)
rec.setUint16(10, 0, true)
rec.setUint16(12, 0, true)
rec.setUint32(14, crc, true)
rec.setUint32(18, data.length, true)
rec.setUint32(22, data.length, true)
rec.setUint16(26, nameBytes.length, true)
rec.setUint16(28, 0, true)
rec.setUint16(30, 0, true)
rec.setUint16(32, 0, true)
rec.setUint16(34, 0, true)
rec.setUint32(36, 0, true)
rec.setUint32(40, localOffsets[i], true)
chunks.push(new Uint8Array(rec.buffer), nameBytes)
centralSize += 46 + nameBytes.length
offset += 46 + nameBytes.length
}
const end = new DataView(new ArrayBuffer(22))
end.setUint32(0, 0x06054b50, true)
end.setUint16(4, 0, true)
end.setUint16(6, 0, true)
end.setUint16(8, files.length, true)
end.setUint16(10, files.length, true)
end.setUint32(12, centralSize, true)
end.setUint32(16, centralStart, true)
end.setUint16(20, 0, true)
chunks.push(new Uint8Array(end.buffer))
let total = 0
for (const c of chunks) total += c.length
const out = new Uint8Array(total)
let pos = 0
for (const c of chunks) { out.set(c, pos); pos += c.length }
return out
}
const formatFileSize = (bytes) => {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
}
const triggerUpload = () => {
fileInputRef.value?.click()
}
const handleFileSelect = (event) => {
const file = event.target.files?.[0]
if (file) loadImage(file)
}
const handleDrop = (event) => {
const file = event.dataTransfer?.files?.[0]
if (file) loadImage(file)
}
const loadImage = (file) => {
if (!file.type.startsWith('image/')) {
ElMessage.error('请选择图片文件')
return
}
if (file.size > 20 * 1024 * 1024) {
ElMessage.error('图片大小不能超过20MB')
return
}
const formatMap = {
'image/png': 'PNG',
'image/jpeg': 'JPEG',
'image/webp': 'WebP',
'image/bmp': 'BMP',
'image/gif': 'GIF'
}
originalInfo.format = formatMap[file.type] || file.type
originalInfo.size = formatFileSize(file.size)
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
originalImage.value = img
originalInfo.width = img.width
originalInfo.height = img.height
previewUrl.value = e.target.result
resetTransformState()
resizeToolRef.value?.initSize(img.width, img.height)
nextTick(() => renderCanvas())
ElMessage.success('图片加载成功')
}
img.src = e.target.result
}
reader.readAsDataURL(file)
}
const renderCanvas = () => {
const canvas = imageCanvasRef.value?.canvasRef
if (!canvas || !originalImage.value) return
const ctx = canvas.getContext('2d')
const img = originalImage.value
const rotation = rotateFlipRef.value?.rotation ?? 0
const flipH = rotateFlipRef.value?.flipH ?? false
const flipV = rotateFlipRef.value?.flipV ?? false
const rad = (rotation * Math.PI) / 180
const isRotated90 = rotation % 180 !== 0
const srcW = img.width
const srcH = img.height
const rotW = isRotated90 ? srcH : srcW
const rotH = isRotated90 ? srcW : srcH
const offscreen = document.createElement('canvas')
offscreen.width = rotW
offscreen.height = rotH
const offCtx = offscreen.getContext('2d')
offCtx.translate(rotW / 2, rotH / 2)
offCtx.rotate(rad)
if (flipH) offCtx.scale(-1, 1)
if (flipV) offCtx.scale(1, -1)
offCtx.drawImage(img, -srcW / 2, -srcH / 2, srcW, srcH)
const outW = resizeToolRef.value?.resizeWidth || rotW
const outH = resizeToolRef.value?.resizeHeight || rotH
canvas.width = outW
canvas.height = outH
ctx.clearRect(0, 0, outW, outH)
ctx.drawImage(offscreen, 0, 0, outW, outH)
const wmConfig = watermarkToolRef.value?.getWatermarkConfig()
if (wmConfig?.enabled) {
drawWatermark(ctx, outW, outH, wmConfig)
}
updateOutputSize()
}
const updateOutputSize = () => {
const canvas = imageCanvasRef.value?.canvasRef
if (!canvas || !originalImage.value) {
outputInfo.format = ''
outputInfo.size = ''
return
}
outputInfo.format = formatConvertRef.value?.getFormatLabel() ?? ''
const mimeType = formatConvertRef.value?.getMimeType() ?? 'image/png'
const quality = formatConvertRef.value?.getQuality()
try {
canvas.toBlob((blob) => {
if (blob) {
outputInfo.size = formatFileSize(blob.size)
}
}, mimeType, quality)
} catch {
outputInfo.size = '计算失败'
}
}
const resetTransformState = () => {
formatConvertRef.value?.reset()
cropToolRef.value?.reset()
rotateFlipRef.value?.reset()
watermarkToolRef.value?.reset()
mosaicToolRef.value?.reset()
bgColorToolRef.value?.reset()
isWatermarkDragging.value = false
watermarkPos.x = 50
watermarkPos.y = 50
isMosaicSelecting.value = false
mosaicRegions.value = []
}
const drawWatermark = (ctx, canvasW, canvasH, config) => {
const { text, position, fontSize, opacity, angle, color, customX, customY } = config
ctx.save()
ctx.globalAlpha = opacity
ctx.font = `${fontSize}px sans-serif`
ctx.fillStyle = color
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const rad = (angle * Math.PI) / 180
const metrics = ctx.measureText(text)
const textW = metrics.width
const textH = fontSize
const drawSingle = (x, y) => {
ctx.save()
ctx.translate(x, y)
ctx.rotate(rad)
ctx.fillText(text, 0, 0)
ctx.restore()
}
if (position === 'tile') {
const gapX = textW + fontSize * 2
const gapY = textH + fontSize * 2
const diagonal = Math.sqrt(canvasW * canvasW + canvasH * canvasH)
const startX = -diagonal / 2
const startY = -diagonal / 2
for (let y = startY; y < diagonal / 2; y += gapY) {
for (let x = startX; x < diagonal / 2; x += gapX) {
drawSingle(canvasW / 2 + x, canvasH / 2 + y)
}
}
} else if (position === 'custom') {
const x = (customX / 100) * canvasW
const y = (customY / 100) * canvasH
drawSingle(x, y)
} else {
const padding = fontSize
let x, y
switch (position) {
case 'center':
x = canvasW / 2
y = canvasH / 2
break
case 'top-left':
x = padding + textW / 2
y = padding + textH / 2
break
case 'top-right':
x = canvasW - padding - textW / 2
y = padding + textH / 2
break
case 'bottom-left':
x = padding + textW / 2
y = canvasH - padding - textH / 2
break
case 'bottom-right':
default:
x = canvasW - padding - textW / 2
y = canvasH - padding - textH / 2
break
}
drawSingle(x, y)
}
ctx.restore()
}
const onFormatChange = () => {
renderCanvas()
}
const onRotateFlipChange = () => {
if (!originalImage.value) return
renderCanvas()
}
const onResizeChange = () => {
renderCanvas()
}
const onWatermarkChange = () => {
renderCanvas()
}
const onWatermarkDragToggle = (enabled) => {
isWatermarkDragging.value = enabled
if (enabled) {
watermarkPos.x = watermarkToolRef.value?.customX ?? 50
watermarkPos.y = watermarkToolRef.value?.customY ?? 50
}
}
const onWatermarkDrag = (pos) => {
watermarkPos.x = pos.x
watermarkPos.y = pos.y
watermarkToolRef.value?.setCustomPosition(pos.x, pos.y)
}
const onMosaicChange = () => {}
const onMosaicSelectToggle = (enabled) => {
isMosaicSelecting.value = enabled
if (enabled) {
activeFunc.value = 'mosaic'
}
}
const onMosaicRegionAdd = (region) => {
mosaicToolRef.value?.addManualRegion(region)
mosaicRegions.value = mosaicToolRef.value?.allRegions || []
}
const onMosaicRegionUpdate = (index, newRegion) => {
mosaicToolRef.value?.updateRegionPosition(index, newRegion)
mosaicRegions.value = mosaicToolRef.value?.allRegions || []
}
const onMosaicRegionRemove = (index) => {
mosaicToolRef.value?.removeRegion(index)
mosaicRegions.value = mosaicToolRef.value?.allRegions || []
}
const onMosaicRegionsChange = (regions) => {
mosaicRegions.value = regions
}
const onApplyMosaic = (config) => {
applyMosaicToCanvas(config)
}
const applyMosaicToCanvas = (config) => {
const canvas = imageCanvasRef.value?.canvasRef
if (!canvas || !originalImage.value) return
const ctx = canvas.getContext('2d')
const { style, regions, mosaicSize, blurRadius, solidColor } = config
if (!regions || regions.length === 0) {
ElMessage.warning('没有需要打码的区域')
return
}
for (const region of regions) {
const rx = Math.max(0, Math.round(region.x))
const ry = Math.max(0, Math.round(region.y))
const rw = Math.min(Math.round(region.w), canvas.width - rx)
const rh = Math.min(Math.round(region.h), canvas.height - ry)
if (rw <= 0 || rh <= 0) continue
if (style === 'mosaic') {
applyMosaicEffect(ctx, rx, ry, rw, rh, mosaicSize)
} else if (style === 'blur') {
applyBlurEffect(ctx, canvas, rx, ry, rw, rh, blurRadius)
} else if (style === 'solid') {
applySolidEffect(ctx, rx, ry, rw, rh, solidColor)
}
}
mosaicRegions.value = []
mosaicToolRef.value?.reset()
isMosaicSelecting.value = false
const croppedCanvas = document.createElement('canvas')
croppedCanvas.width = canvas.width
croppedCanvas.height = canvas.height
const croppedCtx = croppedCanvas.getContext('2d')
croppedCtx.drawImage(canvas, 0, 0)
const newImg = new Image()
newImg.onload = () => {
originalImage.value = newImg
renderCanvas()
ElMessage.success('打码已应用')
}
newImg.src = croppedCanvas.toDataURL()
}
const applyMosaicEffect = (ctx, x, y, w, h, blockSize) => {
const size = Math.max(2, blockSize)
const imageData = ctx.getImageData(x, y, w, h)
const data = imageData.data
for (let by = 0; by < h; by += size) {
for (let bx = 0; bx < w; bx += size) {
let r = 0, g = 0, b = 0, count = 0
const bw = Math.min(size, w - bx)
const bh = Math.min(size, h - by)
for (let dy = 0; dy < bh; dy++) {
for (let dx = 0; dx < bw; dx++) {
const idx = ((by + dy) * w + (bx + dx)) * 4
r += data[idx]
g += data[idx + 1]
b += data[idx + 2]
count++
}
}
r = Math.round(r / count)
g = Math.round(g / count)
b = Math.round(b / count)
for (let dy = 0; dy < bh; dy++) {
for (let dx = 0; dx < bw; dx++) {
const idx = ((by + dy) * w + (bx + dx)) * 4
data[idx] = r
data[idx + 1] = g
data[idx + 2] = b
}
}
}
}
ctx.putImageData(imageData, x, y)
}
const applyBlurEffect = (ctx, canvas, x, y, w, h, radius) => {
const tempCanvas = document.createElement('canvas')
tempCanvas.width = canvas.width
tempCanvas.height = canvas.height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.filter = `blur(${radius}px)`
tempCtx.drawImage(canvas, 0, 0)
ctx.drawImage(
tempCanvas,
x, y, w, h,
x, y, w, h
)
}
const applySolidEffect = (ctx, x, y, w, h, color) => {
ctx.save()
ctx.fillStyle = color
ctx.fillRect(x, y, w, h)
ctx.restore()
}
const onCropStart = () => {
if (!originalImage.value) return
cropToolRef.value.startCropping(originalImage.value.width, originalImage.value.height)
activeFunc.value = 'crop'
}
const onCropCancel = () => {}
const onCropApplied = (rect) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const sw = Math.min(rect.w, originalImage.value.width - rect.x)
const sh = Math.min(rect.h, originalImage.value.height - rect.y)
canvas.width = sw
canvas.height = sh
ctx.drawImage(originalImage.value, rect.x, rect.y, sw, sh, 0, 0, sw, sh)
const croppedImg = new Image()
croppedImg.onload = () => {
originalImage.value = croppedImg
originalInfo.width = sw
originalInfo.height = sh
resizeToolRef.value?.initSize(sw, sh)
cropToolRef.value?.finishCrop()
renderCanvas()
ElMessage.success('裁剪完成')
}
croppedImg.src = canvas.toDataURL()
}
const onCropRatioChange = (ratio) => {
if (!originalImage.value) return
cropToolRef.value.setCropRectFromRatio(originalImage.value.width, originalImage.value.height)
}
const onApplyBgColor = ({ canvas: sourceCanvas, imageData }) => {
const canvas = imageCanvasRef.value?.canvasRef
if (!canvas || !originalImage.value) return
const tempCanvas = document.createElement('canvas')
tempCanvas.width = canvas.width
tempCanvas.height = canvas.height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.putImageData(imageData, 0, 0)
const newImg = new Image()
newImg.onload = () => {
originalImage.value = newImg
renderCanvas()
}
newImg.src = tempCanvas.toDataURL()
}
const onCropMouseDown = (e, ctx) => {
if (!originalImage.value) return
cropToolRef.value.handleMouseDown(e, ctx.getOverlayRect, ctx.displayScale, ctx.getCanvasOffset)
}
const onCropMouseMove = (e, ctx) => {
if (!originalImage.value) return
cropToolRef.value.handleMouseMove(e, ctx.getOverlayRect, ctx.displayScale, ctx.getCanvasOffset, originalImage.value.width, originalImage.value.height)
}
const onCropMouseUp = () => {
cropToolRef.value?.handleMouseUp()
}
const downloadImage = () => {
const canvas = imageCanvasRef.value?.canvasRef
if (!canvas) return
renderCanvas()
const mimeType = formatConvertRef.value?.getMimeType() ?? 'image/png'
const quality = formatConvertRef.value?.getQuality()
const ext = formatConvertRef.value?.getExt() ?? 'png'
let dataUrl
try {
dataUrl = canvas.toDataURL(mimeType, quality)
} catch {
ElMessage.error('当前浏览器不支持该格式的导出')
return
}
const link = document.createElement('a')
link.download = `processed_${Date.now()}.${ext}`
link.href = dataUrl
link.click()
ElMessage.success('图片已下载')
}
const resetAll = () => {
if (!originalImage.value) return
resetTransformState()
resizeToolRef.value?.initSize(originalImage.value.width, originalImage.value.height)
renderCanvas()
ElMessage.success('已重置所有操作')
}
const removeImage = () => {
previewUrl.value = null
originalImage.value = null
originalInfo.width = 0
originalInfo.height = 0
originalInfo.format = ''
originalInfo.size = ''
outputInfo.format = ''
outputInfo.size = ''
resetTransformState()
}
const goBack = () => {
router.push('/developer')
}
onMounted(() => {
const handleKey = (e) => {
if (e.key === 'Escape' && cropToolRef.value?.isCropping) {
cropToolRef.value.cancelCrop()
}
}
window.addEventListener('keydown', handleKey)
onUnmounted(() => window.removeEventListener('keydown', handleKey))
})2.3 效果截图
