forked from zguiy/utils
工具完成
This commit is contained in:
402
src/components/tools/Base64ToImage.vue
Normal file
402
src/components/tools/Base64ToImage.vue
Normal file
@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 工具栏 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="convertBase64ToImage"
|
||||
:disabled="!base64Input.trim() || isConverting"
|
||||
class="btn-primary"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="isConverting ? ['fas', 'spinner'] : ['fas', 'image']"
|
||||
:class="['mr-2', isConverting && 'animate-spin']"
|
||||
/>
|
||||
{{ t('tools.base64_to_image.base64_to_image') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="downloadImage"
|
||||
:disabled="!imageDataUrl"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
|
||||
{{ t('tools.base64_to_image.download_image') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearAll"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
|
||||
{{ t('tools.base64_to_image.clear') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Base64 输入区域 -->
|
||||
<div class="space-y-4">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.base64_to_image.base64_input') }}</h3>
|
||||
<textarea
|
||||
v-model="base64Input"
|
||||
:placeholder="t('tools.base64_to_image.base64_placeholder')"
|
||||
class="textarea-field h-40"
|
||||
@input="handleBase64Change"
|
||||
/>
|
||||
<div class="text-sm text-secondary mt-2">
|
||||
字符数: {{ base64Input.length }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传区域 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.base64_to_image.image_to_base64') }}</h3>
|
||||
<div
|
||||
@click="triggerFileUpload"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleFileDrop"
|
||||
:class="[
|
||||
'border-2 border-dashed rounded-lg p-8 cursor-pointer transition-colors text-center',
|
||||
isDragging ? 'border-primary bg-primary bg-opacity-5' : 'border-tertiary hover:border-primary-light'
|
||||
]"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'cloud-upload-alt']" class="text-4xl text-tertiary mb-4" />
|
||||
<div class="text-secondary">
|
||||
<p>{{ t('tools.base64_to_image.click_or_drag') }}</p>
|
||||
<p class="text-sm text-tertiary mt-1">支持 JPG, PNG, GIF, WebP 格式</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览和结果区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- 图片预览 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.base64_to_image.image_preview') }}</h3>
|
||||
|
||||
<div class="flex justify-center items-center min-h-[200px] bg-block rounded-lg">
|
||||
<div v-if="imageDataUrl" class="text-center max-w-full">
|
||||
<img
|
||||
:src="imageDataUrl"
|
||||
:alt="t('tools.base64_to_image.preview_image')"
|
||||
class="max-w-full max-h-64 mx-auto rounded border border-primary border-opacity-20"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<div v-if="imageInfo" class="text-sm text-secondary mt-2">
|
||||
<div>尺寸: {{ imageInfo.width }} × {{ imageInfo.height }}px</div>
|
||||
<div>大小: {{ imageInfo.size }}</div>
|
||||
<div>格式: {{ imageInfo.format }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isConverting" class="text-center">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="text-4xl text-primary animate-spin mb-4" />
|
||||
<div class="text-secondary">{{ t('tools.base64_to_image.converting') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center">
|
||||
<FontAwesomeIcon :icon="['fas', 'image']" class="text-6xl text-tertiary mb-4" />
|
||||
<div class="text-secondary">{{ t('tools.base64_to_image.no_preview') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base64 输出 -->
|
||||
<div v-if="base64Output" class="card p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-primary">{{ t('tools.base64_to_image.base64_output') }}</h3>
|
||||
<button
|
||||
@click="copyBase64ToClipboard"
|
||||
class="p-2 rounded text-secondary hover:text-primary transition-colors"
|
||||
title="复制"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="base64Copied ? ['fas', 'check'] : ['fas', 'copy']"
|
||||
:class="base64Copied && 'text-success'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="base64Output"
|
||||
class="textarea-field h-32"
|
||||
readonly
|
||||
/>
|
||||
<div class="text-sm text-secondary mt-2">
|
||||
字符数: {{ base64Output.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态消息 -->
|
||||
<div v-if="statusMessage" class="card p-4">
|
||||
<div :class="[
|
||||
'flex items-center space-x-2',
|
||||
statusType === 'success' ? 'text-success' : 'text-error'
|
||||
]">
|
||||
<FontAwesomeIcon
|
||||
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
|
||||
/>
|
||||
<span>{{ statusMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
|
||||
const { t } = useLanguage()
|
||||
|
||||
// 响应式状态
|
||||
const base64Input = ref('')
|
||||
const base64Output = ref('')
|
||||
const imageDataUrl = ref('')
|
||||
const isDragging = ref(false)
|
||||
const isConverting = ref(false)
|
||||
const base64Copied = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusType = ref<'success' | 'error'>('success')
|
||||
|
||||
// 文件输入引用
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
// 图片信息
|
||||
const imageInfo = ref<{
|
||||
width: number
|
||||
height: number
|
||||
size: string
|
||||
format: string
|
||||
} | null>(null)
|
||||
|
||||
// 将Base64转换为图片
|
||||
const convertBase64ToImage = async () => {
|
||||
if (!base64Input.value.trim()) {
|
||||
showStatus('请输入Base64编码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
isConverting.value = true
|
||||
statusMessage.value = ''
|
||||
|
||||
await nextTick()
|
||||
|
||||
try {
|
||||
let base64Data = base64Input.value.trim()
|
||||
|
||||
// 如果没有data URI前缀,尝试添加
|
||||
if (!base64Data.startsWith('data:')) {
|
||||
// 检测图片格式
|
||||
const firstChar = base64Data.charAt(0)
|
||||
let mimeType = 'image/png' // 默认
|
||||
|
||||
if (firstChar === '/') {
|
||||
mimeType = 'image/jpeg'
|
||||
} else if (firstChar === 'R') {
|
||||
mimeType = 'image/gif'
|
||||
} else if (firstChar === 'U') {
|
||||
mimeType = 'image/webp'
|
||||
}
|
||||
|
||||
base64Data = `data:${mimeType};base64,${base64Data}`
|
||||
}
|
||||
|
||||
// 验证Base64格式
|
||||
const base64Pattern = /^data:image\/(png|jpe?g|gif|webp);base64,/
|
||||
if (!base64Pattern.test(base64Data)) {
|
||||
throw new Error('无效的Base64图片格式')
|
||||
}
|
||||
|
||||
imageDataUrl.value = base64Data
|
||||
showStatus('Base64转换成功', 'success')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Base64转换失败:', error)
|
||||
showStatus('转换失败: ' + (error instanceof Error ? error.message : '未知错误'), 'error')
|
||||
imageDataUrl.value = ''
|
||||
imageInfo.value = null
|
||||
} finally {
|
||||
isConverting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (file) {
|
||||
convertFileToBase64(file)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件拖拽
|
||||
const handleFileDrop = (event: DragEvent) => {
|
||||
isDragging.value = false
|
||||
const files = event.dataTransfer?.files
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0]
|
||||
if (file.type.startsWith('image/')) {
|
||||
convertFileToBase64(file)
|
||||
} else {
|
||||
showStatus('请选择图片文件', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = () => {
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 将文件转换为Base64
|
||||
const convertFileToBase64 = (file: File) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showStatus('请选择图片文件', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
isConverting.value = true
|
||||
statusMessage.value = ''
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const result = e.target?.result as string
|
||||
base64Output.value = result
|
||||
imageDataUrl.value = result
|
||||
|
||||
// 更新图片信息
|
||||
imageInfo.value = {
|
||||
width: 0, // 将在图片加载后更新
|
||||
height: 0,
|
||||
size: formatFileSize(file.size),
|
||||
format: file.type.split('/')[1].toUpperCase()
|
||||
}
|
||||
|
||||
showStatus('图片转Base64成功', 'success')
|
||||
} catch (error) {
|
||||
console.error('文件转换失败:', error)
|
||||
showStatus('文件转换失败', 'error')
|
||||
} finally {
|
||||
isConverting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
showStatus('文件读取失败', 'error')
|
||||
isConverting.value = false
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 触发文件上传
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
const downloadImage = () => {
|
||||
if (!imageDataUrl.value) return
|
||||
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.download = `base64-image-${Date.now()}.png`
|
||||
link.href = imageDataUrl.value
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
showStatus('图片下载成功', 'success')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
showStatus('下载失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 复制Base64到剪贴板
|
||||
const copyBase64ToClipboard = async () => {
|
||||
if (!base64Output.value) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(base64Output.value)
|
||||
base64Copied.value = true
|
||||
showStatus('Base64已复制到剪贴板', 'success')
|
||||
setTimeout(() => {
|
||||
base64Copied.value = false
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
showStatus('复制失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有内容
|
||||
const clearAll = () => {
|
||||
base64Input.value = ''
|
||||
base64Output.value = ''
|
||||
imageDataUrl.value = ''
|
||||
imageInfo.value = null
|
||||
statusMessage.value = ''
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Base64输入变化
|
||||
const handleBase64Change = () => {
|
||||
if (statusMessage.value) {
|
||||
statusMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载完成
|
||||
const handleImageLoad = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
if (imageInfo.value) {
|
||||
imageInfo.value.width = img.naturalWidth
|
||||
imageInfo.value.height = img.naturalHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
const handleImageError = () => {
|
||||
showStatus('图片加载失败,请检查Base64编码是否正确', 'error')
|
||||
imageDataUrl.value = ''
|
||||
imageInfo.value = null
|
||||
}
|
||||
|
||||
// 显示状态消息
|
||||
const showStatus = (message: string, type: 'success' | 'error') => {
|
||||
statusMessage.value = message
|
||||
statusType.value = type
|
||||
setTimeout(() => {
|
||||
statusMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user