1
0
forked from zguiy/utils

工具完成

This commit is contained in:
2025-06-28 22:38:49 +08:00
parent 2c668fedd0
commit 8400dbfab9
60 changed files with 23197 additions and 144 deletions

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