1
0
forked from zguiy/utils
Files
utils/src/components/tools/Base64ToImage.vue
2025-06-28 22:38:49 +08:00

402 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>