forked from zguiy/utils
		
	
		
			
				
	
	
		
			402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<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>  |