forked from zguiy/utils
		
	
		
			
				
	
	
		
			773 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			773 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						||
  <div class="space-y-6">
 | 
						||
    <!-- 工具栏 -->
 | 
						||
    <div class="card p-4">
 | 
						||
      <div class="flex flex-wrap gap-2">
 | 
						||
        <button
 | 
						||
          @click="triggerFileInput"
 | 
						||
          class="btn-primary"
 | 
						||
        >
 | 
						||
          <FontAwesomeIcon :icon="['fas', 'upload']" class="mr-2" />
 | 
						||
          上传图片
 | 
						||
        </button>
 | 
						||
        
 | 
						||
        <button
 | 
						||
          @click="addTextWatermark"
 | 
						||
          :disabled="!originalImage"
 | 
						||
          class="btn-secondary"
 | 
						||
        >
 | 
						||
          <FontAwesomeIcon :icon="['fas', 'font']" class="mr-2" />
 | 
						||
          文字水印
 | 
						||
        </button>
 | 
						||
        
 | 
						||
        <button
 | 
						||
          @click="triggerWatermarkInput"
 | 
						||
          :disabled="!originalImage"
 | 
						||
          class="btn-secondary"
 | 
						||
        >
 | 
						||
          <FontAwesomeIcon :icon="['fas', 'image']" class="mr-2" />
 | 
						||
          图片水印
 | 
						||
        </button>
 | 
						||
        
 | 
						||
        <button
 | 
						||
          @click="downloadImage"
 | 
						||
          :disabled="!processedImage"
 | 
						||
          class="btn-secondary"
 | 
						||
        >
 | 
						||
          <FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
 | 
						||
          下载图片
 | 
						||
        </button>
 | 
						||
        
 | 
						||
        <button
 | 
						||
          @click="resetImage"
 | 
						||
          :disabled="!originalImage"
 | 
						||
          class="btn-secondary"
 | 
						||
        >
 | 
						||
          <FontAwesomeIcon :icon="['fas', 'undo']" class="mr-2" />
 | 
						||
          重置
 | 
						||
        </button>
 | 
						||
      </div>
 | 
						||
      
 | 
						||
      <!-- 隐藏的文件输入 -->
 | 
						||
      <input
 | 
						||
        ref="fileInput"
 | 
						||
        type="file"
 | 
						||
        accept="image/*"
 | 
						||
        class="hidden"
 | 
						||
        @change="handleImageUpload"
 | 
						||
      >
 | 
						||
      
 | 
						||
      <input
 | 
						||
        ref="watermarkInput"
 | 
						||
        type="file"
 | 
						||
        accept="image/*"
 | 
						||
        class="hidden"
 | 
						||
        @change="handleWatermarkUpload"
 | 
						||
      >
 | 
						||
    </div>
 | 
						||
 | 
						||
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
 | 
						||
      <!-- 设置区域 -->
 | 
						||
      <div class="space-y-4">
 | 
						||
        <!-- 文字水印设置 -->
 | 
						||
        <div v-if="watermarkType === 'text'" class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">文字水印设置</h3>
 | 
						||
          
 | 
						||
          <div class="space-y-4">
 | 
						||
            <!-- 水印文字 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">水印文字</label>
 | 
						||
              <input
 | 
						||
                v-model="textWatermark.text"
 | 
						||
                type="text"
 | 
						||
                placeholder="请输入水印文字"
 | 
						||
                class="input-field"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 字体大小 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                字体大小: {{ textWatermark.fontSize }}px
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="textWatermark.fontSize"
 | 
						||
                type="range"
 | 
						||
                min="12"
 | 
						||
                max="120"
 | 
						||
                step="2"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 字体颜色 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">字体颜色</label>
 | 
						||
              <div class="flex space-x-2">
 | 
						||
                <input
 | 
						||
                  v-model="textWatermark.color"
 | 
						||
                  type="color"
 | 
						||
                  class="w-12 h-8 rounded border border-primary border-opacity-20"
 | 
						||
                  @input="updateWatermark"
 | 
						||
                >
 | 
						||
                <input
 | 
						||
                  v-model="textWatermark.color"
 | 
						||
                  type="text"
 | 
						||
                  class="input-field flex-1"
 | 
						||
                  @input="updateWatermark"
 | 
						||
                >
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 透明度 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                透明度: {{ Math.round(textWatermark.opacity * 100) }}%
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="textWatermark.opacity"
 | 
						||
                type="range"
 | 
						||
                min="0.1"
 | 
						||
                max="1"
 | 
						||
                step="0.1"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 字体样式 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">字体样式</label>
 | 
						||
              <div class="flex space-x-2">
 | 
						||
                <button
 | 
						||
                  @click="toggleFontStyle('bold')"
 | 
						||
                  :class="['btn-sm', textWatermark.fontWeight === 'bold' ? 'btn-primary' : 'btn-secondary']"
 | 
						||
                >
 | 
						||
                  <FontAwesomeIcon :icon="['fas', 'bold']" />
 | 
						||
                </button>
 | 
						||
                <button
 | 
						||
                  @click="toggleFontStyle('italic')"
 | 
						||
                  :class="['btn-sm', textWatermark.fontStyle === 'italic' ? 'btn-primary' : 'btn-secondary']"
 | 
						||
                >
 | 
						||
                  <FontAwesomeIcon :icon="['fas', 'italic']" />
 | 
						||
                </button>
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 旋转角度 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                旋转角度: {{ textWatermark.rotation }}°
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="textWatermark.rotation"
 | 
						||
                type="range"
 | 
						||
                min="-45"
 | 
						||
                max="45"
 | 
						||
                step="5"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
 | 
						||
        <!-- 图片水印设置 -->
 | 
						||
        <div v-if="watermarkType === 'image'" class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">图片水印设置</h3>
 | 
						||
          
 | 
						||
          <div class="space-y-4">
 | 
						||
            <!-- 水印图片预览 -->
 | 
						||
            <div v-if="watermarkImage">
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">水印图片</label>
 | 
						||
              <div class="bg-block rounded-lg p-4">
 | 
						||
                <img :src="watermarkImage" alt="水印图片" class="max-w-full h-20 object-contain mx-auto">
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 缩放比例 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                缩放比例: {{ Math.round(imageWatermark.scale * 100) }}%
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="imageWatermark.scale"
 | 
						||
                type="range"
 | 
						||
                min="0.1"
 | 
						||
                max="2"
 | 
						||
                step="0.1"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 透明度 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                透明度: {{ Math.round(imageWatermark.opacity * 100) }}%
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="imageWatermark.opacity"
 | 
						||
                type="range"
 | 
						||
                min="0.1"
 | 
						||
                max="1"
 | 
						||
                step="0.1"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 旋转角度 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                旋转角度: {{ imageWatermark.rotation }}°
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="imageWatermark.rotation"
 | 
						||
                type="range"
 | 
						||
                min="-180"
 | 
						||
                max="180"
 | 
						||
                step="15"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
 | 
						||
        <!-- 位置设置 -->
 | 
						||
        <div v-if="originalImage" class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">位置设置</h3>
 | 
						||
          
 | 
						||
          <div class="space-y-4">
 | 
						||
            <!-- 预设位置 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">快速定位</label>
 | 
						||
              <div class="grid grid-cols-3 gap-2">
 | 
						||
                <button
 | 
						||
                  v-for="(pos, key) in positions"
 | 
						||
                  :key="key"
 | 
						||
                  @click="setPosition(key)"
 | 
						||
                  :class="['btn-sm text-xs', currentPosition.key === key ? 'btn-primary' : 'btn-secondary']"
 | 
						||
                >
 | 
						||
                  {{ pos.name }}
 | 
						||
                </button>
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <!-- 自定义位置 -->
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                X坐标: {{ currentPosition.x }}px
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="currentPosition.x"
 | 
						||
                type="range"
 | 
						||
                :min="0"
 | 
						||
                :max="canvasWidth"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
            
 | 
						||
            <div>
 | 
						||
              <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                Y坐标: {{ currentPosition.y }}px
 | 
						||
              </label>
 | 
						||
              <input
 | 
						||
                v-model.number="currentPosition.y"
 | 
						||
                type="range"
 | 
						||
                :min="0"
 | 
						||
                :max="canvasHeight"
 | 
						||
                class="w-full"
 | 
						||
                @input="updateWatermark"
 | 
						||
              >
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
 | 
						||
        <!-- 批量水印设置 -->
 | 
						||
        <div v-if="originalImage" class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">批量水印</h3>
 | 
						||
          
 | 
						||
          <div class="space-y-3">
 | 
						||
            <label class="flex items-center space-x-2">
 | 
						||
              <input
 | 
						||
                v-model="batchSettings.enabled"
 | 
						||
                type="checkbox"
 | 
						||
                class="form-checkbox"
 | 
						||
                @change="updateWatermark"
 | 
						||
              >
 | 
						||
              <span class="text-secondary">启用平铺水印</span>
 | 
						||
            </label>
 | 
						||
            
 | 
						||
            <div v-if="batchSettings.enabled" class="space-y-3">
 | 
						||
              <div>
 | 
						||
                <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                  水平间距: {{ batchSettings.spacingX }}px
 | 
						||
                </label>
 | 
						||
                <input
 | 
						||
                  v-model.number="batchSettings.spacingX"
 | 
						||
                  type="range"
 | 
						||
                  min="50"
 | 
						||
                  max="300"
 | 
						||
                  step="10"
 | 
						||
                  class="w-full"
 | 
						||
                  @input="updateWatermark"
 | 
						||
                >
 | 
						||
              </div>
 | 
						||
              
 | 
						||
              <div>
 | 
						||
                <label class="block text-sm font-medium text-secondary mb-2">
 | 
						||
                  垂直间距: {{ batchSettings.spacingY }}px
 | 
						||
                </label>
 | 
						||
                <input
 | 
						||
                  v-model.number="batchSettings.spacingY"
 | 
						||
                  type="range"
 | 
						||
                  min="50"
 | 
						||
                  max="300"
 | 
						||
                  step="10"
 | 
						||
                  class="w-full"
 | 
						||
                  @input="updateWatermark"
 | 
						||
                >
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
      </div>
 | 
						||
 | 
						||
      <!-- 预览区域 -->
 | 
						||
      <div class="lg:col-span-2 space-y-4">
 | 
						||
        <!-- 原图预览 -->
 | 
						||
        <div class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">图片预览</h3>
 | 
						||
          
 | 
						||
          <div v-if="!originalImage" class="bg-block rounded-lg p-8 text-center">
 | 
						||
            <FontAwesomeIcon :icon="['fas', 'cloud-upload-alt']" class="text-4xl text-tertiary mb-4" />
 | 
						||
            <p class="text-secondary mb-4">请上传图片或拖拽图片到此处</p>
 | 
						||
            <button @click="triggerFileInput" class="btn-primary">
 | 
						||
              选择图片
 | 
						||
            </button>
 | 
						||
          </div>
 | 
						||
          
 | 
						||
          <div v-else class="relative">
 | 
						||
            <canvas
 | 
						||
              ref="previewCanvas"
 | 
						||
              class="max-w-full border border-primary border-opacity-20 rounded-lg cursor-crosshair"
 | 
						||
              @click="handleCanvasClick"
 | 
						||
            />
 | 
						||
            
 | 
						||
            <!-- 图片信息 -->
 | 
						||
            <div class="mt-4 flex justify-between items-center text-sm text-secondary">
 | 
						||
              <span>{{ imageInfo.width }} × {{ imageInfo.height }}</span>
 | 
						||
              <span>{{ imageInfo.size }}</span>
 | 
						||
              <span>{{ imageInfo.format }}</span>
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
 | 
						||
        <!-- 处理结果 -->
 | 
						||
        <div v-if="processedImage" class="card p-4">
 | 
						||
          <h3 class="text-lg font-semibold text-primary mb-3">处理结果</h3>
 | 
						||
          
 | 
						||
          <div class="bg-block rounded-lg p-4">
 | 
						||
            <img :src="processedImage" alt="处理后的图片" class="max-w-full mx-auto">
 | 
						||
          </div>
 | 
						||
          
 | 
						||
          <div class="mt-4 flex justify-between items-center text-sm text-secondary">
 | 
						||
            <span>质量: {{ outputQuality }}%</span>
 | 
						||
            <span>大小: {{ outputSize }}</span>
 | 
						||
          </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, reactive, computed, nextTick } from 'vue'
 | 
						||
import { useLanguage } from '@/composables/useLanguage'
 | 
						||
 | 
						||
const { t } = useLanguage()
 | 
						||
 | 
						||
// DOM引用
 | 
						||
const fileInput = ref<HTMLInputElement>()
 | 
						||
const watermarkInput = ref<HTMLInputElement>()
 | 
						||
const previewCanvas = ref<HTMLCanvasElement>()
 | 
						||
 | 
						||
// 响应式状态
 | 
						||
const originalImage = ref('')
 | 
						||
const processedImage = ref('')
 | 
						||
const watermarkImage = ref('')
 | 
						||
const watermarkType = ref<'text' | 'image' | null>(null)
 | 
						||
const statusMessage = ref('')
 | 
						||
const statusType = ref<'success' | 'error'>('success')
 | 
						||
 | 
						||
// 图片信息
 | 
						||
const imageInfo = reactive({
 | 
						||
  width: 0,
 | 
						||
  height: 0,
 | 
						||
  size: '',
 | 
						||
  format: ''
 | 
						||
})
 | 
						||
 | 
						||
// Canvas尺寸
 | 
						||
const canvasWidth = ref(0)
 | 
						||
const canvasHeight = ref(0)
 | 
						||
 | 
						||
// 文字水印设置
 | 
						||
const textWatermark = reactive({
 | 
						||
  text: 'Sample Watermark',
 | 
						||
  fontSize: 36,
 | 
						||
  color: '#ffffff',
 | 
						||
  opacity: 0.7,
 | 
						||
  fontWeight: 'normal',
 | 
						||
  fontStyle: 'normal',
 | 
						||
  rotation: 0
 | 
						||
})
 | 
						||
 | 
						||
// 图片水印设置
 | 
						||
const imageWatermark = reactive({
 | 
						||
  scale: 0.3,
 | 
						||
  opacity: 0.7,
 | 
						||
  rotation: 0
 | 
						||
})
 | 
						||
 | 
						||
// 位置设置
 | 
						||
const currentPosition = reactive({
 | 
						||
  x: 0,
 | 
						||
  y: 0,
 | 
						||
  key: 'center'
 | 
						||
})
 | 
						||
 | 
						||
// 批量水印设置
 | 
						||
const batchSettings = reactive({
 | 
						||
  enabled: false,
 | 
						||
  spacingX: 150,
 | 
						||
  spacingY: 150
 | 
						||
})
 | 
						||
 | 
						||
// 预设位置
 | 
						||
const positions = {
 | 
						||
  'top-left': { name: '左上', x: 0.1, y: 0.1 },
 | 
						||
  'top-center': { name: '居上', x: 0.5, y: 0.1 },
 | 
						||
  'top-right': { name: '右上', x: 0.9, y: 0.1 },
 | 
						||
  'center-left': { name: '居左', x: 0.1, y: 0.5 },
 | 
						||
  'center': { name: '居中', x: 0.5, y: 0.5 },
 | 
						||
  'center-right': { name: '居右', x: 0.9, y: 0.5 },
 | 
						||
  'bottom-left': { name: '左下', x: 0.1, y: 0.9 },
 | 
						||
  'bottom-center': { name: '居下', x: 0.5, y: 0.9 },
 | 
						||
  'bottom-right': { name: '右下', x: 0.9, y: 0.9 }
 | 
						||
}
 | 
						||
 | 
						||
// 输出设置
 | 
						||
const outputQuality = computed(() => 90)
 | 
						||
const outputSize = computed(() => {
 | 
						||
  if (!processedImage.value) return ''
 | 
						||
  return '估算大小'
 | 
						||
})
 | 
						||
 | 
						||
// 触发文件选择
 | 
						||
const triggerFileInput = () => {
 | 
						||
  fileInput.value?.click()
 | 
						||
}
 | 
						||
 | 
						||
const triggerWatermarkInput = () => {
 | 
						||
  watermarkInput.value?.click()
 | 
						||
}
 | 
						||
 | 
						||
// 处理图片上传
 | 
						||
const handleImageUpload = (event: Event) => {
 | 
						||
  const file = (event.target as HTMLInputElement).files?.[0]
 | 
						||
  if (!file) return
 | 
						||
 | 
						||
  if (!file.type.startsWith('image/')) {
 | 
						||
    showStatus('请选择有效的图片文件', 'error')
 | 
						||
    return
 | 
						||
  }
 | 
						||
 | 
						||
  const reader = new FileReader()
 | 
						||
  reader.onload = (e) => {
 | 
						||
    originalImage.value = e.target?.result as string
 | 
						||
    loadImageInfo(file)
 | 
						||
    nextTick(() => {
 | 
						||
      setupCanvas()
 | 
						||
    })
 | 
						||
  }
 | 
						||
  reader.readAsDataURL(file)
 | 
						||
}
 | 
						||
 | 
						||
// 处理水印图片上传
 | 
						||
const handleWatermarkUpload = (event: Event) => {
 | 
						||
  const file = (event.target as HTMLInputElement).files?.[0]
 | 
						||
  if (!file) return
 | 
						||
 | 
						||
  if (!file.type.startsWith('image/')) {
 | 
						||
    showStatus('请选择有效的图片文件', 'error')
 | 
						||
    return
 | 
						||
  }
 | 
						||
 | 
						||
  const reader = new FileReader()
 | 
						||
  reader.onload = (e) => {
 | 
						||
    watermarkImage.value = e.target?.result as string
 | 
						||
    watermarkType.value = 'image'
 | 
						||
    updateWatermark()
 | 
						||
  }
 | 
						||
  reader.readAsDataURL(file)
 | 
						||
}
 | 
						||
 | 
						||
// 加载图片信息
 | 
						||
const loadImageInfo = (file: File) => {
 | 
						||
  const img = new Image()
 | 
						||
  img.onload = () => {
 | 
						||
    imageInfo.width = img.width
 | 
						||
    imageInfo.height = img.height
 | 
						||
    imageInfo.size = formatFileSize(file.size)
 | 
						||
    imageInfo.format = file.type.split('/')[1].toUpperCase()
 | 
						||
  }
 | 
						||
  img.src = originalImage.value
 | 
						||
}
 | 
						||
 | 
						||
// 设置Canvas
 | 
						||
const setupCanvas = () => {
 | 
						||
  if (!previewCanvas.value || !originalImage.value) return
 | 
						||
 | 
						||
  const img = new Image()
 | 
						||
  img.onload = () => {
 | 
						||
    const canvas = previewCanvas.value!
 | 
						||
    const ctx = canvas.getContext('2d')!
 | 
						||
    
 | 
						||
    // 设置画布尺寸
 | 
						||
    const maxWidth = 600
 | 
						||
    const maxHeight = 400
 | 
						||
    let { width, height } = img
 | 
						||
    
 | 
						||
    if (width > maxWidth) {
 | 
						||
      height = (height * maxWidth) / width
 | 
						||
      width = maxWidth
 | 
						||
    }
 | 
						||
    if (height > maxHeight) {
 | 
						||
      width = (width * maxHeight) / height
 | 
						||
      height = maxHeight
 | 
						||
    }
 | 
						||
    
 | 
						||
    canvas.width = width
 | 
						||
    canvas.height = height
 | 
						||
    canvasWidth.value = width
 | 
						||
    canvasHeight.value = height
 | 
						||
    
 | 
						||
    // 绘制原图
 | 
						||
    ctx.drawImage(img, 0, 0, width, height)
 | 
						||
    
 | 
						||
    // 设置默认水印位置
 | 
						||
    setPosition('bottom-right')
 | 
						||
  }
 | 
						||
  img.src = originalImage.value
 | 
						||
}
 | 
						||
 | 
						||
// 添加文字水印
 | 
						||
const addTextWatermark = () => {
 | 
						||
  watermarkType.value = 'text'
 | 
						||
  updateWatermark()
 | 
						||
}
 | 
						||
 | 
						||
// 更新水印
 | 
						||
const updateWatermark = () => {
 | 
						||
  if (!previewCanvas.value || !originalImage.value || !watermarkType.value) return
 | 
						||
 | 
						||
  const canvas = previewCanvas.value
 | 
						||
  const ctx = canvas.getContext('2d')!
 | 
						||
  
 | 
						||
  // 重新绘制原图
 | 
						||
  const img = new Image()
 | 
						||
  img.onload = () => {
 | 
						||
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
 | 
						||
    
 | 
						||
    // 绘制水印
 | 
						||
    if (watermarkType.value === 'text') {
 | 
						||
      drawTextWatermark(ctx)
 | 
						||
    } else if (watermarkType.value === 'image' && watermarkImage.value) {
 | 
						||
      drawImageWatermark(ctx)
 | 
						||
    }
 | 
						||
    
 | 
						||
    // 生成处理后的图片
 | 
						||
    processedImage.value = canvas.toDataURL('image/jpeg', outputQuality.value / 100)
 | 
						||
  }
 | 
						||
  img.src = originalImage.value
 | 
						||
}
 | 
						||
 | 
						||
// 绘制文字水印
 | 
						||
const drawTextWatermark = (ctx: CanvasRenderingContext2D) => {
 | 
						||
  ctx.save()
 | 
						||
  
 | 
						||
  // 设置字体
 | 
						||
  const fontStyle = textWatermark.fontStyle === 'italic' ? 'italic ' : ''
 | 
						||
  const fontWeight = textWatermark.fontWeight === 'bold' ? 'bold ' : ''
 | 
						||
  ctx.font = `${fontStyle}${fontWeight}${textWatermark.fontSize}px Arial`
 | 
						||
  
 | 
						||
  // 设置颜色和透明度
 | 
						||
  ctx.fillStyle = textWatermark.color
 | 
						||
  ctx.globalAlpha = textWatermark.opacity
 | 
						||
  
 | 
						||
  if (batchSettings.enabled) {
 | 
						||
    // 平铺水印
 | 
						||
    for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
 | 
						||
      for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
 | 
						||
        drawSingleTextWatermark(ctx, x + 50, y + 50)
 | 
						||
      }
 | 
						||
    }
 | 
						||
  } else {
 | 
						||
    // 单个水印
 | 
						||
    drawSingleTextWatermark(ctx, currentPosition.x, currentPosition.y)
 | 
						||
  }
 | 
						||
  
 | 
						||
  ctx.restore()
 | 
						||
}
 | 
						||
 | 
						||
// 绘制单个文字水印
 | 
						||
const drawSingleTextWatermark = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
 | 
						||
  ctx.save()
 | 
						||
  
 | 
						||
  ctx.translate(x, y)
 | 
						||
  ctx.rotate((textWatermark.rotation * Math.PI) / 180)
 | 
						||
  
 | 
						||
  // 添加阴影效果
 | 
						||
  ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
 | 
						||
  ctx.shadowBlur = 3
 | 
						||
  ctx.shadowOffsetX = 1
 | 
						||
  ctx.shadowOffsetY = 1
 | 
						||
  
 | 
						||
  ctx.fillText(textWatermark.text, 0, 0)
 | 
						||
  
 | 
						||
  ctx.restore()
 | 
						||
}
 | 
						||
 | 
						||
// 绘制图片水印
 | 
						||
const drawImageWatermark = (ctx: CanvasRenderingContext2D) => {
 | 
						||
  const watermarkImg = new Image()
 | 
						||
  watermarkImg.onload = () => {
 | 
						||
    ctx.save()
 | 
						||
    ctx.globalAlpha = imageWatermark.opacity
 | 
						||
    
 | 
						||
    const scaledWidth = watermarkImg.width * imageWatermark.scale
 | 
						||
    const scaledHeight = watermarkImg.height * imageWatermark.scale
 | 
						||
    
 | 
						||
    if (batchSettings.enabled) {
 | 
						||
      // 平铺水印
 | 
						||
      for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
 | 
						||
        for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
 | 
						||
          drawSingleImageWatermark(ctx, watermarkImg, x, y, scaledWidth, scaledHeight)
 | 
						||
        }
 | 
						||
      }
 | 
						||
    } else {
 | 
						||
      // 单个水印
 | 
						||
      drawSingleImageWatermark(ctx, watermarkImg, currentPosition.x, currentPosition.y, scaledWidth, scaledHeight)
 | 
						||
    }
 | 
						||
    
 | 
						||
    ctx.restore()
 | 
						||
  }
 | 
						||
  watermarkImg.src = watermarkImage.value
 | 
						||
}
 | 
						||
 | 
						||
// 绘制单个图片水印
 | 
						||
const drawSingleImageWatermark = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, x: number, y: number, width: number, height: number) => {
 | 
						||
  ctx.save()
 | 
						||
  
 | 
						||
  ctx.translate(x, y)
 | 
						||
  ctx.rotate((imageWatermark.rotation * Math.PI) / 180)
 | 
						||
  
 | 
						||
  ctx.drawImage(img, -width / 2, -height / 2, width, height)
 | 
						||
  
 | 
						||
  ctx.restore()
 | 
						||
}
 | 
						||
 | 
						||
// 设置位置
 | 
						||
const setPosition = (key: string) => {
 | 
						||
  const pos = positions[key as keyof typeof positions]
 | 
						||
  if (pos) {
 | 
						||
    currentPosition.x = canvasWidth.value * pos.x
 | 
						||
    currentPosition.y = canvasHeight.value * pos.y
 | 
						||
    currentPosition.key = key
 | 
						||
    updateWatermark()
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
// 处理Canvas点击
 | 
						||
const handleCanvasClick = (event: MouseEvent) => {
 | 
						||
  if (!previewCanvas.value) return
 | 
						||
  
 | 
						||
  const rect = previewCanvas.value.getBoundingClientRect()
 | 
						||
  const scaleX = canvasWidth.value / rect.width
 | 
						||
  const scaleY = canvasHeight.value / rect.height
 | 
						||
  
 | 
						||
  currentPosition.x = (event.clientX - rect.left) * scaleX
 | 
						||
  currentPosition.y = (event.clientY - rect.top) * scaleY
 | 
						||
  currentPosition.key = 'custom'
 | 
						||
  
 | 
						||
  updateWatermark()
 | 
						||
}
 | 
						||
 | 
						||
// 切换字体样式
 | 
						||
const toggleFontStyle = (style: 'bold' | 'italic') => {
 | 
						||
  if (style === 'bold') {
 | 
						||
    textWatermark.fontWeight = textWatermark.fontWeight === 'bold' ? 'normal' : 'bold'
 | 
						||
  } else {
 | 
						||
    textWatermark.fontStyle = textWatermark.fontStyle === 'italic' ? 'normal' : 'italic'
 | 
						||
  }
 | 
						||
  updateWatermark()
 | 
						||
}
 | 
						||
 | 
						||
// 下载图片
 | 
						||
const downloadImage = () => {
 | 
						||
  if (!processedImage.value) return
 | 
						||
  
 | 
						||
  const link = document.createElement('a')
 | 
						||
  link.download = `watermarked-image-${Date.now()}.jpg`
 | 
						||
  link.href = processedImage.value
 | 
						||
  link.click()
 | 
						||
  
 | 
						||
  showStatus('图片下载完成', 'success')
 | 
						||
}
 | 
						||
 | 
						||
// 重置图片
 | 
						||
const resetImage = () => {
 | 
						||
  originalImage.value = ''
 | 
						||
  processedImage.value = ''
 | 
						||
  watermarkImage.value = ''
 | 
						||
  watermarkType.value = null
 | 
						||
  statusMessage.value = ''
 | 
						||
}
 | 
						||
 | 
						||
// 格式化文件大小
 | 
						||
const formatFileSize = (bytes: number): string => {
 | 
						||
  if (bytes === 0) return '0 B'
 | 
						||
  const k = 1024
 | 
						||
  const sizes = ['B', 'KB', 'MB', 'GB']
 | 
						||
  const i = Math.floor(Math.log(bytes) / Math.log(k))
 | 
						||
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
 | 
						||
}
 | 
						||
 | 
						||
// 显示状态消息
 | 
						||
const showStatus = (message: string, type: 'success' | 'error') => {
 | 
						||
  statusMessage.value = message
 | 
						||
  statusType.value = type
 | 
						||
  setTimeout(() => {
 | 
						||
    statusMessage.value = ''
 | 
						||
  }, 3000)
 | 
						||
}
 | 
						||
</script>  |