当前位置: 首页 > article >正文

基于canvas实现的图片加水印功能

开篇

本文内容基于工具小站-图片模块-图片加水印功能,对于此功能,简单做一个总结。

图片加水印功能实现逻辑

图片加水印功能分为两种:文字水印和图片水印功能。其中如果水印为文字类型,则使用ctx.fillText()方; 如果水印为图片类型,则使用ctx.drawImage()方法。

代码实现

<template>
  <div class="app-container">
    <header class="app-header">
      <h1>图片水印</h1>
      <p class="subtitle">为你的图片添加文字或图片水印</p>
    </header>

    <main class="main-content">
      <el-tabs v-model="activeTab" class="watermark-tabs">
        <el-tab-pane label="单张水印" name="single">
          <!-- 上传区域 -->
          <div class="upload-section" v-if="!currentImage">
            <el-upload
              class="upload-drop-zone"
              drag
              :auto-upload="false"
              accept="image/*"
              :show-file-list="false"
              @change="handleFileChange"
            >
              <el-icon class="upload-icon"><upload-filled /></el-icon>
              <div class="upload-text">
                <h3>将图片拖到此处,或点击上传</h3>
                <p>支持 PNG、JPG、WebP 等格式</p>
              </div>
            </el-upload>
          </div>

          <!-- 编辑区域 -->
          <div v-else class="edit-section">
            <div class="preview-area">
              <div class="canvas-container" ref="canvasContainer">
                <canvas ref="cacheCanvas" style="display: none"></canvas>
                <canvas
                  ref="canvas"
                  @mousedown="handleDrag"
                  @mousemove="handleDrag"
                  @mouseup="handleDrag"
                ></canvas>
              </div>
            </div>

            <div class="control-panel">
              <el-form :model="watermarkSettings" label-position="top">
                <!-- 水印类型选择 -->
                <el-form-item label="水印类型">
                  <el-radio-group v-model="watermarkSettings.type">
                    <el-radio label="text">文字水印</el-radio>
                    <el-radio label="image">图片水印</el-radio>
                  </el-radio-group>
                </el-form-item>

                <!-- 文字水印设置 -->
                <template v-if="watermarkSettings.type === 'text'">
                  <el-form-item label="水印文字">
                    <el-input
                      v-model="watermarkSettings.text"
                      placeholder="请输入水印文字"
                    />
                  </el-form-item>

                  <el-form-item label="字体">
                    <el-select v-model="watermarkSettings.fontFamily">
                      <el-option label="苹方" value="PingFang SC" />
                      <el-option label="黑体" value="SimHei" />
                      <el-option label="宋体" value="SimSun" />
                    </el-select>
                  </el-form-item>

                  <el-form-item label="字号">
                    <el-slider
                      v-model="watermarkSettings.fontSize"
                      :min="12"
                      :max="72"
                    />
                  </el-form-item>

                  <el-form-item label="颜色">
                    <el-color-picker v-model="watermarkSettings.color" />
                  </el-form-item>
                </template>

                <!-- 图片水印设置 -->
                <template v-if="watermarkSettings.type === 'image'">
                  <el-form-item label="水印图片">
                    <el-upload
                      class="watermark-image-upload"
                      :auto-upload="false"
                      accept="image/*"
                      :show-file-list="false"
                      @change="handleWatermarkImageChange"
                    >
                      <el-button type="primary">选择水印图片</el-button>
                    </el-upload>
                  </el-form-item>
                </template>

                <!-- 通用设置 -->
                <el-form-item label="透明度">
                  <el-slider
                    v-model="watermarkSettings.opacity"
                    :min="0"
                    :max="100"
                  />
                </el-form-item>

                <el-form-item label="位置">
                  <el-radio-group v-model="watermarkSettings.position">
                    <el-radio-button label="topLeft">左上</el-radio-button>
                    <el-radio-button label="topRight">右上</el-radio-button>
                    <el-radio-button label="bottomLeft">左下</el-radio-button>
                    <el-radio-button label="bottomRight">右下</el-radio-button>
                    <el-radio-button label="center">居中</el-radio-button>
                    <el-radio-button label="custom">自定义</el-radio-button>
                  </el-radio-group>
                </el-form-item>

                <div class="action-buttons">
                  <el-button
                    type="primary"
                    @click="applyWatermark"
                    :loading="processing"
                  >
                    应用水印
                  </el-button>
                  <el-button @click="resetImage">重新选择</el-button>
                  <el-button
                    type="success"
                    @click="downloadImage"
                    :disabled="!hasWatermark"
                  >
                    下载图片
                  </el-button>
                </div>
              </el-form>
            </div>
          </div>
        </el-tab-pane>

        <el-tab-pane label="批量水印" name="batch">
          <div class="batch-section">
            <div class="upload-section" v-if="!batchFiles.length">
              <el-upload
                class="upload-drop-zone"
                drag
                multiple
                :auto-upload="false"
                accept="image/*"
                :show-file-list="false"
                @change="handleBatchFilesChange"
              >
                <el-icon class="upload-icon"><upload-filled /></el-icon>
                <div class="upload-text">
                  <h3>将多张图片拖到此处,或点击上传</h3>
                  <p>支持 PNG、JPG、WebP 等格式</p>
                </div>
              </el-upload>
            </div>

            <div v-else class="batch-content">
              <div class="batch-main">
                <div class="preview-area" v-if="batchFiles.length">
                  <div
                    class="canvas-container"
                    ref="batchCanvasContainer"
                    style="min-height: 200px"
                  >
                    <canvas
                      ref="batchCacheCanvas"
                      style="display: none"
                    ></canvas>
                    <canvas ref="batchCanvas"></canvas>
                  </div>
                </div>

                <div class="control-panel">
                  <el-form :model="batchWatermarkSettings" label-position="top">
                    <el-form-item label="水印类型">
                      <el-radio-group v-model="batchWatermarkSettings.type">
                        <el-radio label="text">文字水印</el-radio>
                        <el-radio label="image">图片水印</el-radio>
                      </el-radio-group>
                    </el-form-item>

                    <template v-if="batchWatermarkSettings.type === 'text'">
                      <el-form-item label="水印文字">
                        <el-input
                          v-model="batchWatermarkSettings.text"
                          placeholder="请输入水印文字"
                        />
                      </el-form-item>

                      <el-form-item label="字体">
                        <el-select v-model="batchWatermarkSettings.fontFamily">
                          <el-option label="苹方" value="PingFang SC" />
                          <el-option label="黑体" value="SimHei" />
                          <el-option label="宋体" value="SimSun" />
                        </el-select>
                      </el-form-item>

                      <el-form-item label="字号">
                        <el-slider
                          v-model="batchWatermarkSettings.fontSize"
                          :min="12"
                          :max="72"
                        />
                      </el-form-item>

                      <el-form-item label="颜色">
                        <el-color-picker
                          v-model="batchWatermarkSettings.color"
                        />
                      </el-form-item>
                    </template>

                    <template v-if="batchWatermarkSettings.type === 'image'">
                      <el-form-item label="水印图片">
                        <el-upload
                          class="watermark-image-upload"
                          :auto-upload="false"
                          accept="image/*"
                          :show-file-list="false"
                          @change="handleBatchWatermarkImageChange"
                        >
                          <el-button type="primary">选择水印图片</el-button>
                        </el-upload>
                      </el-form-item>
                    </template>

                    <el-form-item label="透明度">
                      <el-slider
                        v-model="batchWatermarkSettings.opacity"
                        :min="0"
                        :max="100"
                      />
                    </el-form-item>

                    <el-form-item label="位置">
                      <el-radio-group v-model="batchWatermarkSettings.position">
                        <el-radio-button label="topLeft">左上</el-radio-button>
                        <el-radio-button label="topRight">右上</el-radio-button>
                        <el-radio-button label="bottomLeft"
                          >左下</el-radio-button
                        >
                        <el-radio-button label="bottomRight"
                          >右下</el-radio-button
                        >
                        <el-radio-button label="center">居中</el-radio-button>
                      </el-radio-group>
                    </el-form-item>
                  </el-form>
                </div>
              </div>

              <div class="batch-footer">
                <div class="files-list">
                  <el-table :data="batchFiles" style="width: 100%">
                    <el-table-column label="文件名" prop="name" />
                    <el-table-column label="预览" width="100">
                      <template #default="{ row }">
                        <el-button
                          link
                          type="primary"
                          @click="previewBatchImage(row)"
                        >
                          预览
                        </el-button>
                      </template>
                    </el-table-column>
                    <el-table-column label="状态" width="120">
                      <template #default="{ row }">
                        <el-tag :type="getStatusType(row.status)">
                          {{ getStatusText(row.status) }}
                        </el-tag>
                      </template>
                    </el-table-column>
                    <el-table-column label="操作" width="120">
                      <template #default="{ row }">
                        <el-button
                          link
                          type="primary"
                          @click="downloadSingleBatchImage(row)"
                          :disabled="row.status !== 'done'"
                        >
                          下载
                        </el-button>
                      </template>
                    </el-table-column>
                  </el-table>
                </div>

                <div class="batch-actions">
                  <el-button
                    type="primary"
                    @click="applyBatchWatermark"
                    :loading="processing"
                  >
                    批量添加水印
                  </el-button>
                  <el-button @click="resetBatchFiles">重新选择</el-button>
                  <el-button
                    type="success"
                    @click="downloadBatchImages"
                    :disabled="!hasBatchWatermark"
                  >
                    下载全部
                  </el-button>
                </div>
              </div>
            </div>
          </div>
        </el-tab-pane>
      </el-tabs>
    </main>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";

// 状态变量
const currentImage = ref(null);
const canvas = ref(null);
const canvasContainer = ref(null);
const processing = ref(false);
const hasWatermark = ref(false);
const activeTab = ref("single");
const cacheCanvas = ref(null);
const batchCanvas = ref(null);
const batchCanvasContainer = ref(null);
const batchFiles = ref([]);
const hasBatchWatermark = ref(false);

// 水印设置
const watermarkSettings = reactive({
  type: "text",
  text: "水印文字",
  fontFamily: "PingFang SC",
  fontSize: 24,
  color: "#000000",
  opacity: 50,
  position: "bottomRight",
  watermarkImage: null,
  x: 0,
  y: 0,
});

// 添加批量水印的设置
const batchWatermarkSettings = reactive({
  type: "text",
  text: "水印文字",
  fontFamily: "PingFang SC",
  fontSize: 24,
  color: "#000000",
  opacity: 50,
  position: "bottomRight",
  watermarkImage: null,
});

// 处理图片上传
const handleFileChange = (file) => {
  const fileObj = file.raw;
  if (!fileObj) return;

  if (!fileObj.type.startsWith("image/")) {
    ElMessage.error("请上传图片文件");
    return;
  }

  currentImage.value = fileObj;
  loadImage();
};

// 加载图片到画布
const loadImage = () => {
  if (!canvas.value || !cacheCanvas.value) {
    setTimeout(loadImage, 50);
    return;
  }

  const img = new Image();

  img.onload = () => {
    // 设置缓存画布尺寸并绘制原始图片
    cacheCanvas.value.width = img.width;
    cacheCanvas.value.height = img.height;
    const cacheCtx = cacheCanvas.value.getContext("2d");
    cacheCtx.drawImage(img, 0, 0);

    // 设置显示画布尺寸
    canvas.value.width = img.width;
    canvas.value.height = img.height;

    // 调整容器大小(只在首次加载时调整)
    if (!canvasContainer.value.style.width) {
      const container = canvasContainer.value;
      const maxWidth = container.clientWidth;
      const scale = maxWidth / img.width;

      if (scale < 1) {
        container.style.width = maxWidth + "px";
        container.style.height = img.height * scale + "px";
      } else {
        container.style.width = img.width + "px";
        container.style.height = img.height + "px";
      }
    }

    // 重新绘制图片
    refreshCanvas();
  };

  img.src = URL.createObjectURL(currentImage.value);
};

// 添加刷新画布函数
const refreshCanvas = () => {
  const ctx = canvas.value.getContext("2d");
  ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
  ctx.drawImage(cacheCanvas.value, 0, 0);
};

// 应用水印
const applyWatermark = async () => {
  if (!currentImage.value) return;

  processing.value = true;
  const ctx = canvas.value.getContext("2d");

  try {
    // 重新绘制原始图片
    refreshCanvas();

    // 应用水印
    if (watermarkSettings.type === "text") {
      // 应用文字水印
      ctx.globalAlpha = watermarkSettings.opacity / 100;
      ctx.fillStyle = watermarkSettings.color;
      ctx.font = `${watermarkSettings.fontSize}px ${watermarkSettings.fontFamily}`;

      const position = calculatePosition(
        ctx.measureText(watermarkSettings.text).width,
        watermarkSettings.fontSize
      );

      ctx.fillText(watermarkSettings.text, position.x, position.y);
    } else {
      // 应用图片水印
      if (watermarkSettings.watermarkImage) {
        const watermarkImg = new Image();
        watermarkImg.src = URL.createObjectURL(
          watermarkSettings.watermarkImage
        );

        await new Promise((resolve) => {
          watermarkImg.onload = () => {
            ctx.globalAlpha = watermarkSettings.opacity / 100;
            const position = calculatePosition(
              watermarkImg.width,
              watermarkImg.height
            );
            ctx.drawImage(watermarkImg, position.x, position.y);
            resolve();
          };
        });
      }
    }

    hasWatermark.value = true;
    ElMessage.success("水印添加成功");
  } catch (error) {
    ElMessage.error("水印添加失败");
    console.error(error);
  } finally {
    processing.value = false;
    ctx.globalAlpha = 1;
  }
};

// 计算水印位置
const calculatePosition = (width, height) => {
  const canvasWidth = canvas.value.width;
  const canvasHeight = canvas.value.height;
  const padding = 20;

  if (watermarkSettings.position === "custom") {
    // 如果是首次设置自定义位置,将水印放在中心
    if (watermarkSettings.x === 0 && watermarkSettings.y === 0) {
      watermarkSettings.x = (canvasWidth - width) / 2;
      watermarkSettings.y = (canvasHeight + height) / 2;
    }
    return {
      x: Math.min(Math.max(watermarkSettings.x, 0), canvasWidth - width),
      y: Math.min(Math.max(watermarkSettings.y, 0), canvasHeight - height),
    };
  }

  const positions = {
    topLeft: { x: padding, y: padding + height },
    topRight: { x: canvasWidth - width - padding, y: padding + height },
    bottomLeft: { x: padding, y: canvasHeight - padding },
    bottomRight: {
      x: canvasWidth - width - padding,
      y: canvasHeight - padding,
    },
    center: { x: (canvasWidth - width) / 2, y: (canvasHeight + height) / 2 },
  };

  return positions[watermarkSettings.position];
};

// 处理水印图片上传
const handleWatermarkImageChange = (file) => {
  watermarkSettings.watermarkImage = file.raw;
};

// 下载处理后的图片
const downloadImage = () => {
  const link = document.createElement("a");
  link.download = `watermarked_${currentImage.value.name}`;
  link.href = canvas.value.toDataURL();
  link.click();
};

// 重置
const resetImage = () => {
  currentImage.value = null;
  hasWatermark.value = false;
  watermarkSettings.watermarkImage = null;
};

// 修改拖拽功能实现
let isDragging = false;
let startX = 0;
let startY = 0;
let dragDebounceTimer = null;
let isFirstDrag = true;

const handleDrag = (event) => {
  if (watermarkSettings.position !== "custom") return;

  const rect = canvas.value.getBoundingClientRect();
  const scale = canvas.value.width / rect.width;

  switch (event.type) {
    case "mousedown":
      isDragging = true;
      isFirstDrag = true;
      startX = event.clientX;
      startY = event.clientY;
      break;

    case "mousemove":
      if (!isDragging) return;

      const deltaX = (event.clientX - startX) * scale;
      const deltaY = (event.clientY - startY) * scale;

      watermarkSettings.x += deltaX;
      watermarkSettings.y += deltaY;

      startX = event.clientX;
      startY = event.clientY;

      // 使用防抖处理水印应用
      if (dragDebounceTimer) clearTimeout(dragDebounceTimer);
      dragDebounceTimer = setTimeout(() => {
        refreshCanvas();
        updateWatermark();
      }, 16);
      break;

    case "mouseup":
      isDragging = false;
      if (dragDebounceTimer) {
        clearTimeout(dragDebounceTimer);
        if (!isFirstDrag) {
          applyWatermark();
        }
      }
      isFirstDrag = false;
      break;
  }
};

// 添加更新水印函数(不显示提示)
const updateWatermark = async () => {
  if (!currentImage.value) return;

  const ctx = canvas.value.getContext("2d");

  try {
    // 重新绘制原始图片
    refreshCanvas();

    // 应用水印
    if (watermarkSettings.type === "text") {
      ctx.globalAlpha = watermarkSettings.opacity / 100;
      ctx.fillStyle = watermarkSettings.color;
      ctx.font = `${watermarkSettings.fontSize}px ${watermarkSettings.fontFamily}`;

      const position = calculatePosition(
        ctx.measureText(watermarkSettings.text).width,
        watermarkSettings.fontSize
      );

      ctx.fillText(watermarkSettings.text, position.x, position.y);
    } else if (watermarkSettings.watermarkImage) {
      const watermarkImg = new Image();
      watermarkImg.src = URL.createObjectURL(watermarkSettings.watermarkImage);

      await new Promise((resolve) => {
        watermarkImg.onload = () => {
          ctx.globalAlpha = watermarkSettings.opacity / 100;
          const position = calculatePosition(
            watermarkImg.width,
            watermarkImg.height
          );
          ctx.drawImage(watermarkImg, position.x, position.y);
          resolve();
        };
      });
    }
  } finally {
    ctx.globalAlpha = 1;
  }
};

// 修改水印设置监听
watch(
  watermarkSettings,
  (newVal, oldVal) => {
    if (currentImage.value) {
      // 当切换到自定义位置时,重置坐标
      if (newVal.position === "custom" && oldVal.position !== "custom") {
        watermarkSettings.x = 0;
        watermarkSettings.y = 0;
      }
      nextTick(() => {
        loadImage();
        if (hasWatermark.value) {
          updateWatermark();
        }
      });
    }
  },
  { deep: true }
);

onMounted(() => {
  // 确保组件完全挂载后再初始化
  nextTick(() => {
    if (currentImage.value) {
      loadImage();
    }
  });
});

// 添加批量处理相关函数
const handleBatchFilesChange = (file) => {
  const fileObj = file.raw;
  if (!fileObj) return;

  if (!fileObj.type.startsWith("image/")) {
    ElMessage.error("请上传图片文件");
    return;
  }

  batchFiles.value.push({
    id: Date.now() + Math.random(),
    name: fileObj.name,
    file: fileObj,
    status: "pending",
    processedBlob: null,
  });

  // 如果是第一个文件,自动加载预览
  if (batchFiles.value.length === 1) {
    nextTick(() => {
      previewBatchImage(batchFiles.value[0]);
    });
  }
};

const previewBatchImage = (file) => {
  if (!batchCanvas.value) return;

  const ctx = batchCanvas.value.getContext("2d");
  const img = new Image();

  img.onload = () => {
    batchCanvas.value.width = img.width;
    batchCanvas.value.height = img.height;
    ctx.clearRect(0, 0, img.width, img.height);
    ctx.drawImage(img, 0, 0);

    // 立即应用水印预览
    if (batchWatermarkSettings.type === "text") {
      ctx.globalAlpha = batchWatermarkSettings.opacity / 100;
      ctx.fillStyle = batchWatermarkSettings.color;
      ctx.font = `${batchWatermarkSettings.fontSize}px ${batchWatermarkSettings.fontFamily}`;

      const position = calculatePositionForBatch(
        ctx.measureText(batchWatermarkSettings.text).width,
        batchWatermarkSettings.fontSize,
        batchCanvas.value
      );

      ctx.fillText(batchWatermarkSettings.text, position.x, position.y);
      ctx.globalAlpha = 1;
    } else if (batchWatermarkSettings.watermarkImage) {
      const watermarkImg = new Image();
      watermarkImg.onload = () => {
        ctx.globalAlpha = batchWatermarkSettings.opacity / 100;
        const position = calculatePositionForBatch(
          watermarkImg.width,
          watermarkImg.height,
          batchCanvas.value
        );
        ctx.drawImage(watermarkImg, position.x, position.y);
        ctx.globalAlpha = 1;
      };
      watermarkImg.src = URL.createObjectURL(
        batchWatermarkSettings.watermarkImage
      );
    }

    // 调整容器大小
    const container = batchCanvasContainer.value;
    const maxWidth = container.clientWidth;
    const scale = maxWidth / img.width;

    if (scale < 1) {
      container.style.width = maxWidth + "px";
      container.style.height = img.height * scale + "px";
    } else {
      container.style.width = img.width + "px";
      container.style.height = img.height + "px";
    }
  };

  img.src = URL.createObjectURL(file.file);
};

const applyBatchWatermark = async () => {
  if (!batchFiles.value.length) return;

  processing.value = true;

  try {
    for (const file of batchFiles.value) {
      file.status = "processing";
      await processImage(file);
      file.status = "done";
    }

    hasBatchWatermark.value = true;
    ElMessage.success("批量处理完成");
  } catch (error) {
    ElMessage.error("批量处理失败");
    console.error(error);
  } finally {
    processing.value = false;
  }
};

const processImage = async (file) => {
  return new Promise((resolve, reject) => {
    const tempCanvas = document.createElement("canvas");
    const tempCtx = tempCanvas.getContext("2d");
    const img = new Image();

    img.onload = async () => {
      tempCanvas.width = img.width;
      tempCanvas.height = img.height;
      tempCtx.drawImage(img, 0, 0);

      try {
        // 应用水印
        if (batchWatermarkSettings.type === "text") {
          tempCtx.globalAlpha = batchWatermarkSettings.opacity / 100;
          tempCtx.fillStyle = batchWatermarkSettings.color;
          tempCtx.font = `${batchWatermarkSettings.fontSize}px ${batchWatermarkSettings.fontFamily}`;

          const position = calculatePositionForBatch(
            tempCtx.measureText(batchWatermarkSettings.text).width,
            batchWatermarkSettings.fontSize,
            tempCanvas
          );

          tempCtx.fillText(batchWatermarkSettings.text, position.x, position.y);
        } else if (batchWatermarkSettings.watermarkImage) {
          // 处理图片水印
          // ... 类似的图片水印处理逻辑 ...
        }

        // 转换为 Blob
        tempCanvas.toBlob((blob) => {
          if (blob) {
            file.processedBlob = blob;
            resolve(blob);
          } else {
            reject(new Error("转换失败"));
          }
        }, file.file.type);
      } catch (error) {
        reject(error);
      }
    };

    img.onerror = () => reject(new Error("图片加载失败"));
    img.src = URL.createObjectURL(file.file);
  });
};

// 添加批量水印位置计算函数
const calculatePositionForBatch = (width, height, canvas) => {
  const canvasWidth = canvas.width;
  const canvasHeight = canvas.height;
  const padding = 20;

  const positions = {
    topLeft: { x: padding, y: padding + height },
    topRight: { x: canvasWidth - width - padding, y: padding + height },
    bottomLeft: { x: padding, y: canvasHeight - padding },
    bottomRight: {
      x: canvasWidth - width - padding,
      y: canvasHeight - padding,
    },
    center: { x: (canvasWidth - width) / 2, y: (canvasHeight + height) / 2 },
  };

  return positions[batchWatermarkSettings.position];
};

// 修改下载批量图片函数
const downloadBatchImages = () => {
  batchFiles.value.forEach((file) => {
    if (file.status === "done") {
      const link = document.createElement("a");
      link.download = `watermarked_${file.name}`;
      const blob = file.processedBlob || file.file;
      link.href = URL.createObjectURL(blob);
      link.click();
      setTimeout(() => URL.revokeObjectURL(link.href), 100);
    }
  });
};

const resetBatchFiles = () => {
  batchFiles.value = [];
  hasBatchWatermark.value = false;
};

// 状态显示辅助函数
const getStatusText = (status) => {
  const statusMap = {
    pending: "待处理",
    processing: "处理中",
    done: "已完成",
    error: "失败",
  };
  return statusMap[status];
};

const getStatusType = (status) => {
  const typeMap = {
    pending: "info",
    processing: "warning",
    done: "success",
    error: "danger",
  };
  return typeMap[status];
};

// 添加批量水印图片处理函数
const handleBatchWatermarkImageChange = (file) => {
  batchWatermarkSettings.watermarkImage = file.raw;
};

// 添加单个批量图片下载功能
const downloadSingleBatchImage = (file) => {
  if (file.status !== "done") return;

  const link = document.createElement("a");
  link.download = `watermarked_${file.name}`;
  const blob = file.processedBlob || file.file;
  link.href = URL.createObjectURL(blob);
  link.click();
  setTimeout(() => URL.revokeObjectURL(link.href), 100);
};

// 添加批量水印设置变化监听
watch(
  batchWatermarkSettings,
  () => {
    if (batchFiles.value.length > 0) {
      // 重新预览当前显示的图片
      previewBatchImage(batchFiles.value[0]);
    }
  },
  { deep: true }
);
</script>

<style scoped>
.app-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.app-header {
  text-align: center;
  margin-bottom: 3rem;
}

.app-header h1 {
  font-size: 2.5rem;
  font-weight: 600;
  color: var(--el-text-color-primary);
  margin-bottom: 0.5rem;
}

.subtitle {
  color: var(--el-text-color-secondary);
  font-size: 1.1rem;
}

.main-content {
  background: var(--el-bg-color);
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.upload-section {
  padding: 2rem;
}

.upload-drop-zone {
  border: 2px dashed var(--el-border-color);
  border-radius: 8px;
  padding: 3rem 1rem;
  transition: all 0.3s ease;
}

.upload-drop-zone:hover {
  border-color: var(--el-color-primary);
  background: rgba(var(--el-color-primary-rgb), 0.1);
}

.upload-icon {
  font-size: 3rem;
  color: var(--el-text-color-secondary);
  margin-bottom: 1rem;
}

.edit-section {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
}

.preview-area {
  background: var(--el-bg-color-page);
  border-radius: 8px;
  padding: 1rem;
  overflow: hidden;
}

.canvas-container {
  position: relative;
  overflow: auto;
}

.canvas-container canvas {
  max-width: 100%;
  height: auto;
}

.control-panel {
  background: var(--el-bg-color-page);
  border-radius: 8px;
  padding: 1.5rem;
}

.action-buttons {
  display: flex;
  gap: 1rem;
  margin-top: 2rem;
}

:deep(.el-form-item) {
  margin-bottom: 1.5rem;
}

:deep(.el-radio-group) {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

:deep(.el-radio-button__inner) {
  border-radius: 6px;
}

.watermark-image-upload {
  width: 100%;
}

.watermark-tabs {
  margin-bottom: 2rem;
}

.batch-section {
  min-height: 400px;
}

.batch-content {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.files-list {
  margin: 2rem 0;
}

.batch-actions {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin-top: 2rem;
}

.batch-main {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
  margin-bottom: 2rem;
}

.batch-footer {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.batch-main .preview-area {
  background: var(--el-bg-color-page);
  border-radius: 8px;
  padding: 1rem;
  overflow: hidden;
  min-height: 300px;
}

.batch-main .canvas-container {
  position: relative;
  overflow: auto;
  display: flex;
  justify-content: center;
  align-items: center;
}

.batch-main .canvas-container canvas {
  max-width: 100%;
  height: auto;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>

效果截图

在这里插入图片描述

其实该功能还有很大的优化空间,比如说可以随机生成指定数量的水印,按照某种角度(字体旋转45度等)随机排布在图片上。
也希望本文能对您能起到抛砖引玉的作用,愿我们在计算机之路上一路前行!


http://www.kler.cn/a/463390.html

相关文章:

  • 滴滴数据分析80道面试题及参考答案
  • ES IK分词器插件
  • 等保测评和密评的相关性和区别
  • 默认ip无法访问,利用dhcp功能获取ip进行访问的方法
  • 4. C语言 变量
  • 基于Matlab的变压器仿真模型建模方法(12):单相降压自耦变压器的等效电路和仿真模型
  • 单片机从入门到放弃教程001
  • 代码随想录算法训练营第二十天-二叉树-669. 修剪二叉搜索树
  • 如何使用 JPA 实现分页查询并返回 VO 对象
  • 东部新区文化旅游体育局莅临园区考察入驻企业
  • springboot534售楼管理系统(论文+源码)_kaic
  • 关于HarmonyOS Next中卡片的使用方法
  • ctr方法下载的镜像能用docker save进行保存吗?
  • 【老张的程序人生】一天时间,我成软考高级系统分析师
  • 6.若依数据字典
  • 日本IT|敏捷开发指的到底是什么?
  • 青少年编程与数学 02-005 移动Web编程基础 09课题、地理定位
  • 【分布式缓存中间件Memcached原理与应用】
  • leetcode 1315.祖父结点值为偶数的和
  • 【 Sonarqube】可视化Java项目单元测试覆盖率统计框架搭建
  • MySQL数据库的日志
  • typora+picgo core+minio自动上传图片
  • SqlSession的线程安全问题源码分析
  • 2025.01.01(IO模型分类,超时检测,抓包分析,机械臂客户端)
  • Python软体中在AWS Lambda上部署Python应用:一站式部署指南
  • QT---------QT框架功能概述