forked from zguiy/utils
		
	
		
			
				
	
	
		
			289 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						|
  <div class="space-y-6">
 | 
						|
    <!-- 工具栏 -->
 | 
						|
    <div class="card p-4">
 | 
						|
      <div class="flex flex-wrap gap-2">
 | 
						|
        <button
 | 
						|
          @click="generateQRCode"
 | 
						|
          :disabled="!qrText.trim() || isGenerating"
 | 
						|
          class="btn-primary"
 | 
						|
        >
 | 
						|
          <FontAwesomeIcon 
 | 
						|
            :icon="isGenerating ? ['fas', 'spinner'] : ['fas', 'qrcode']" 
 | 
						|
            :class="['mr-2', isGenerating && 'animate-spin']"
 | 
						|
          />
 | 
						|
          {{ t('tools.qrcode_generator.generate') }}
 | 
						|
        </button>
 | 
						|
        
 | 
						|
        <button
 | 
						|
          @click="downloadQRCode"
 | 
						|
          :disabled="!qrCodeDataUrl"
 | 
						|
          class="btn-secondary"
 | 
						|
        >
 | 
						|
          <FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
 | 
						|
          {{ t('tools.qrcode_generator.download') }}
 | 
						|
        </button>
 | 
						|
        
 | 
						|
        <button
 | 
						|
          @click="clearAll"
 | 
						|
          class="btn-secondary"
 | 
						|
        >
 | 
						|
          <FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
 | 
						|
          {{ t('tools.qrcode_generator.clear') }}
 | 
						|
        </button>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
 | 
						|
      <!-- 输入配置区域 -->
 | 
						|
      <div class="space-y-6">
 | 
						|
        <!-- 文本输入 -->
 | 
						|
        <div class="card p-4">
 | 
						|
          <h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.qrcode_generator.text_input') }}</h3>
 | 
						|
          <textarea
 | 
						|
            v-model="qrText"
 | 
						|
            :placeholder="t('tools.qrcode_generator.placeholder')"
 | 
						|
            class="textarea-field h-32"
 | 
						|
            @input="handleTextChange"
 | 
						|
          />
 | 
						|
          <div class="text-sm text-secondary mt-2">
 | 
						|
            字符数: {{ qrText.length }}
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <!-- 配置选项 -->
 | 
						|
        <div class="card p-4">
 | 
						|
          <h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.qrcode_generator.settings') }}</h3>
 | 
						|
          
 | 
						|
          <div class="space-y-4">
 | 
						|
            <!-- 尺寸 -->
 | 
						|
            <div>
 | 
						|
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						|
                {{ t('tools.qrcode_generator.size') }}: {{ qrSize }}px
 | 
						|
              </label>
 | 
						|
              <input
 | 
						|
                v-model="qrSize"
 | 
						|
                type="range"
 | 
						|
                min="100"
 | 
						|
                max="800"
 | 
						|
                step="50"
 | 
						|
                class="w-full"
 | 
						|
              >
 | 
						|
              <div class="flex justify-between text-xs text-tertiary mt-1">
 | 
						|
                <span>100px</span>
 | 
						|
                <span>800px</span>
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <!-- 容错级别 -->
 | 
						|
            <div>
 | 
						|
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						|
                {{ t('tools.qrcode_generator.error_level') }}
 | 
						|
              </label>
 | 
						|
              <select v-model="errorLevel" class="select-field">
 | 
						|
                <option value="L">低 (L) - 7%</option>
 | 
						|
                <option value="M">中 (M) - 15%</option>
 | 
						|
                <option value="Q">中高 (Q) - 25%</option>
 | 
						|
                <option value="H">高 (H) - 30%</option>
 | 
						|
              </select>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <!-- 前景色 -->
 | 
						|
            <div>
 | 
						|
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						|
                {{ t('tools.qrcode_generator.foreground_color') }}
 | 
						|
              </label>
 | 
						|
              <div class="flex space-x-2">
 | 
						|
                <input
 | 
						|
                  v-model="foregroundColor"
 | 
						|
                  type="color"
 | 
						|
                  class="w-12 h-10 rounded border border-primary border-opacity-20"
 | 
						|
                >
 | 
						|
                <input
 | 
						|
                  v-model="foregroundColor"
 | 
						|
                  type="text"
 | 
						|
                  class="input-field flex-1"
 | 
						|
                  placeholder="#000000"
 | 
						|
                >
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <!-- 背景色 -->
 | 
						|
            <div>
 | 
						|
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						|
                {{ t('tools.qrcode_generator.background_color') }}
 | 
						|
              </label>
 | 
						|
              <div class="flex space-x-2">
 | 
						|
                <input
 | 
						|
                  v-model="backgroundColor"
 | 
						|
                  type="color"
 | 
						|
                  class="w-12 h-10 rounded border border-primary border-opacity-20"
 | 
						|
                >
 | 
						|
                <input
 | 
						|
                  v-model="backgroundColor"
 | 
						|
                  type="text"
 | 
						|
                  class="input-field flex-1"
 | 
						|
                  placeholder="#FFFFFF"
 | 
						|
                >
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
 | 
						|
      <!-- 预览区域 -->
 | 
						|
      <div class="card p-4">
 | 
						|
        <h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.qrcode_generator.preview') }}</h3>
 | 
						|
        
 | 
						|
        <div class="flex justify-center items-center min-h-[300px]">
 | 
						|
          <div v-if="qrCodeDataUrl" class="text-center">
 | 
						|
            <img 
 | 
						|
              :src="qrCodeDataUrl" 
 | 
						|
              :alt="t('tools.qrcode_generator.qr_code')"
 | 
						|
              class="mx-auto mb-4 rounded border border-primary border-opacity-20"
 | 
						|
              :style="{ maxWidth: '100%', height: 'auto' }"
 | 
						|
            >
 | 
						|
            <div class="text-sm text-secondary">
 | 
						|
              {{ qrSize }}x{{ qrSize }}px
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
          
 | 
						|
          <div v-else-if="isGenerating" class="text-center">
 | 
						|
            <FontAwesomeIcon :icon="['fas', 'spinner']" class="text-4xl text-primary animate-spin mb-4" />
 | 
						|
            <div class="text-secondary">{{ t('tools.qrcode_generator.generating') }}</div>
 | 
						|
          </div>
 | 
						|
          
 | 
						|
          <div v-else class="text-center">
 | 
						|
            <FontAwesomeIcon :icon="['fas', 'qrcode']" class="text-6xl text-tertiary mb-4" />
 | 
						|
            <div class="text-secondary">{{ t('tools.qrcode_generator.no_preview') }}</div>
 | 
						|
          </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, watch } from 'vue'
 | 
						|
import { useLanguage } from '@/composables/useLanguage'
 | 
						|
import QRCode from 'qrcode'
 | 
						|
 | 
						|
const { t } = useLanguage()
 | 
						|
 | 
						|
// 响应式状态
 | 
						|
const qrText = ref('')
 | 
						|
const qrSize = ref(300)
 | 
						|
const errorLevel = ref('M')
 | 
						|
const foregroundColor = ref('#000000')
 | 
						|
const backgroundColor = ref('#FFFFFF')
 | 
						|
const qrCodeDataUrl = ref('')
 | 
						|
const isGenerating = ref(false)
 | 
						|
const statusMessage = ref('')
 | 
						|
const statusType = ref<'success' | 'error'>('success')
 | 
						|
 | 
						|
// 生成二维码
 | 
						|
const generateQRCode = async () => {
 | 
						|
  if (!qrText.value.trim()) {
 | 
						|
    showStatus('请输入要生成二维码的内容', 'error')
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  isGenerating.value = true
 | 
						|
  statusMessage.value = ''
 | 
						|
 | 
						|
  try {
 | 
						|
    const options = {
 | 
						|
      width: qrSize.value,
 | 
						|
      height: qrSize.value,
 | 
						|
      errorCorrectionLevel: errorLevel.value as 'L' | 'M' | 'Q' | 'H',
 | 
						|
      color: {
 | 
						|
        dark: foregroundColor.value,
 | 
						|
        light: backgroundColor.value
 | 
						|
      },
 | 
						|
      margin: 2
 | 
						|
    }
 | 
						|
 | 
						|
    const dataUrl = await QRCode.toDataURL(qrText.value, options)
 | 
						|
    qrCodeDataUrl.value = dataUrl
 | 
						|
    showStatus('二维码生成成功', 'success')
 | 
						|
    
 | 
						|
  } catch (error) {
 | 
						|
    console.error('生成二维码失败:', error)
 | 
						|
    showStatus('生成二维码失败: ' + (error instanceof Error ? error.message : '未知错误'), 'error')
 | 
						|
    qrCodeDataUrl.value = ''
 | 
						|
  } finally {
 | 
						|
    isGenerating.value = false
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// 下载二维码
 | 
						|
const downloadQRCode = () => {
 | 
						|
  if (!qrCodeDataUrl.value) return
 | 
						|
 | 
						|
  try {
 | 
						|
    const link = document.createElement('a')
 | 
						|
    link.download = `qrcode-${Date.now()}.png`
 | 
						|
    link.href = qrCodeDataUrl.value
 | 
						|
    document.body.appendChild(link)
 | 
						|
    link.click()
 | 
						|
    document.body.removeChild(link)
 | 
						|
    showStatus('二维码下载成功', 'success')
 | 
						|
  } catch (error) {
 | 
						|
    console.error('下载失败:', error)
 | 
						|
    showStatus('下载失败', 'error')
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// 清除所有内容
 | 
						|
const clearAll = () => {
 | 
						|
  qrText.value = ''
 | 
						|
  qrCodeDataUrl.value = ''
 | 
						|
  statusMessage.value = ''
 | 
						|
}
 | 
						|
 | 
						|
// 处理文本变化
 | 
						|
const handleTextChange = () => {
 | 
						|
  // 文本变化时清除状态
 | 
						|
  if (statusMessage.value) {
 | 
						|
    statusMessage.value = ''
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// 显示状态消息
 | 
						|
const showStatus = (message: string, type: 'success' | 'error') => {
 | 
						|
  statusMessage.value = message
 | 
						|
  statusType.value = type
 | 
						|
  setTimeout(() => {
 | 
						|
    statusMessage.value = ''
 | 
						|
  }, 3000)
 | 
						|
}
 | 
						|
 | 
						|
// 防抖重新生成
 | 
						|
let regenerateTimer: number | null = null
 | 
						|
const debouncedRegenerate = () => {
 | 
						|
  if (regenerateTimer) {
 | 
						|
    clearTimeout(regenerateTimer)
 | 
						|
  }
 | 
						|
  regenerateTimer = window.setTimeout(() => {
 | 
						|
    if (qrCodeDataUrl.value && qrText.value.trim()) {
 | 
						|
      generateQRCode()
 | 
						|
    }
 | 
						|
  }, 500)
 | 
						|
}
 | 
						|
 | 
						|
// 监听配置变化,自动重新生成
 | 
						|
watch([qrSize, errorLevel, foregroundColor, backgroundColor], debouncedRegenerate)
 | 
						|
</script>  |