Skip to content

Latest commit

 

History

History
700 lines (606 loc) · 14.1 KB

File metadata and controls

700 lines (606 loc) · 14.1 KB

Watermark.vue 水印组件文档

概述

Watermark.vue是水印组件,用于在页面或指定区域添加水印效果,提供安全防护和版权保护功能。

文件位置: src/components/common/Watermark.vue

组件功能

1. 水印功能

  • 文字水印
  • 图片水印
  • 自定义样式
  • 防删除保护
  • 响应式布局
  • 多种排列方式

2. 组件结构

<template>
  <div class="watermark-container" ref="containerRef">
    <!-- 内容插槽 -->
    <slot></slot>
    
    <!-- 水印层 -->
    <div
      ref="watermarkRef"
      class="watermark-layer"
      :style="watermarkStyle"
      v-show="visible"
    >
      <canvas
        ref="canvasRef"
        :width="canvasWidth"
        :height="canvasHeight"
        class="watermark-canvas"
      ></canvas>
    </div>
  </div>
</template>

3. 脚本逻辑

<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'

// Props定义
const props = defineProps({
  // 水印文字
  text: {
    type: String,
    default: 'Watermark'
  },
  // 水印图片URL
  image: {
    type: String,
    default: ''
  },
  // 水印宽度
  width: {
    type: Number,
    default: 200
  },
  // 水印高度
  height: {
    type: Number,
    default: 100
  },
  // 水印透明度
  opacity: {
    type: Number,
    default: 0.1,
    validator: (value) => value >= 0 && value <= 1
  },
  // 水印旋转角度
  rotate: {
    type: Number,
    default: -20
  },
  // 字体大小
  fontSize: {
    type: Number,
    default: 16
  },
  // 字体颜色
  fontColor: {
    type: String,
    default: '#000000'
  },
  // 字体样式
  fontStyle: {
    type: String,
    default: 'normal'
  },
  // 字体粗细
  fontWeight: {
    type: [String, Number],
    default: 'normal'
  },
  // 字体族
  fontFamily: {
    type: String,
    default: 'Arial, sans-serif'
  },
  // 水印间距X
  gapX: {
    type: Number,
    default: 100
  },
  // 水印间距Y
  gapY: {
    type: Number,
    default: 100
  },
  // 水印偏移X
  offsetX: {
    type: Number,
    default: 0
  },
  // 水印偏移Y
  gapY: {
    type: Number,
    default: 0
  },
  // 层级
  zIndex: {
    type: Number,
    default: 1000
  },
  // 是否显示
  visible: {
    type: Boolean,
    default: true
  },
  // 是否启用防删除保护
  monitor: {
    type: Boolean,
    default: true
  }
})

// 响应式数据
const containerRef = ref(null)
const watermarkRef = ref(null)
const canvasRef = ref(null)
const canvasWidth = ref(0)
const canvasHeight = ref(0)
const observer = ref(null)

// 计算属性
const watermarkStyle = computed(() => ({
  position: 'absolute',
  top: '0',
  left: '0',
  width: '100%',
  height: '100%',
  pointerEvents: 'none',
  zIndex: props.zIndex,
  opacity: props.opacity,
  backgroundImage: `url(${getWatermarkDataUrl()})`,
  backgroundRepeat: 'repeat',
  backgroundPosition: `${props.offsetX}px ${props.offsetY}px`
}))

// 字体样式
const fontStyle = computed(() => {
  return `${props.fontStyle} ${props.fontWeight} ${props.fontSize}px ${props.fontFamily}`
})
</script>

核心功能实现

1. 水印生成

// 生成水印DataURL
const getWatermarkDataUrl = () => {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  
  // 设置画布尺寸
  const canvasWidth = props.width + props.gapX
  const canvasHeight = props.height + props.gapY
  canvas.width = canvasWidth
  canvas.height = canvasHeight
  
  // 设置画布样式
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  ctx.font = fontStyle.value
  ctx.fillStyle = props.fontColor
  
  // 保存当前状态
  ctx.save()
  
  // 移动到画布中心
  ctx.translate(canvasWidth / 2, canvasHeight / 2)
  
  // 旋转画布
  ctx.rotate((props.rotate * Math.PI) / 180)
  
  if (props.image) {
    // 绘制图片水印
    drawImageWatermark(ctx)
  } else {
    // 绘制文字水印
    drawTextWatermark(ctx)
  }
  
  // 恢复状态
  ctx.restore()
  
  return canvas.toDataURL()
}

// 绘制文字水印
const drawTextWatermark = (ctx) => {
  const lines = props.text.split('\n')
  const lineHeight = props.fontSize * 1.2
  const totalHeight = lines.length * lineHeight
  const startY = -totalHeight / 2 + lineHeight / 2
  
  lines.forEach((line, index) => {
    const y = startY + index * lineHeight
    ctx.fillText(line, 0, y)
  })
}

// 绘制图片水印
const drawImageWatermark = (ctx) => {
  const img = new Image()
  img.crossOrigin = 'anonymous'
  
  img.onload = () => {
    const imgWidth = Math.min(img.width, props.width)
    const imgHeight = Math.min(img.height, props.height)
    
    ctx.drawImage(
      img,
      -imgWidth / 2,
      -imgHeight / 2,
      imgWidth,
      imgHeight
    )
    
    // 更新水印
    updateWatermark()
  }
  
  img.onerror = () => {
    console.error('水印图片加载失败:', props.image)
    // 降级到文字水印
    drawTextWatermark(ctx)
  }
  
  img.src = props.image
}

// 更新水印
const updateWatermark = () => {
  if (!watermarkRef.value) return
  
  const dataUrl = getWatermarkDataUrl()
  watermarkRef.value.style.backgroundImage = `url(${dataUrl})`
}

2. 容器尺寸管理

// 更新容器尺寸
const updateContainerSize = () => {
  if (!containerRef.value) return
  
  const rect = containerRef.value.getBoundingClientRect()
  canvasWidth.value = rect.width
  canvasHeight.value = rect.height
  
  // 更新水印
  nextTick(() => {
    updateWatermark()
  })
}

// 监听容器尺寸变化
const observeContainer = () => {
  if (!containerRef.value || !window.ResizeObserver) return
  
  observer.value = new ResizeObserver((entries) => {
    for (const entry of entries) {
      updateContainerSize()
    }
  })
  
  observer.value.observe(containerRef.value)
}

// 停止监听
const unobserveContainer = () => {
  if (observer.value && containerRef.value) {
    observer.value.unobserve(containerRef.value)
    observer.value.disconnect()
    observer.value = null
  }
}

3. 防删除保护

// 防删除监听器
const mutationObserver = ref(null)

// 启用防删除保护
const enableMonitor = () => {
  if (!props.monitor || !watermarkRef.value) return
  
  mutationObserver.value = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      // 检查水印元素是否被删除或修改
      if (mutation.type === 'childList') {
        const removedNodes = Array.from(mutation.removedNodes)
        const hasWatermarkRemoved = removedNodes.some(node => 
          node === watermarkRef.value || 
          (node.nodeType === Node.ELEMENT_NODE && node.contains(watermarkRef.value))
        )
        
        if (hasWatermarkRemoved) {
          console.warn('水印被删除,正在恢复...')
          restoreWatermark()
        }
      }
      
      // 检查水印样式是否被修改
      if (mutation.type === 'attributes' && mutation.target === watermarkRef.value) {
        if (mutation.attributeName === 'style') {
          console.warn('水印样式被修改,正在恢复...')
          restoreWatermark()
        }
      }
    })
  })
  
  // 监听容器和水印元素
  if (containerRef.value) {
    mutationObserver.value.observe(containerRef.value, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['style', 'class']
    })
  }
}

// 禁用防删除保护
const disableMonitor = () => {
  if (mutationObserver.value) {
    mutationObserver.value.disconnect()
    mutationObserver.value = null
  }
}

// 恢复水印
const restoreWatermark = () => {
  nextTick(() => {
    if (containerRef.value && !containerRef.value.contains(watermarkRef.value)) {
      // 重新添加水印元素
      containerRef.value.appendChild(watermarkRef.value)
    }
    
    // 恢复水印样式
    if (watermarkRef.value) {
      Object.assign(watermarkRef.value.style, watermarkStyle.value)
    }
    
    updateWatermark()
  })
}

4. 监听器和生命周期

// 监听props变化
watch([
  () => props.text,
  () => props.image,
  () => props.width,
  () => props.height,
  () => props.opacity,
  () => props.rotate,
  () => props.fontSize,
  () => props.fontColor,
  () => props.fontStyle,
  () => props.fontWeight,
  () => props.fontFamily,
  () => props.gapX,
  () => props.gapY,
  () => props.offsetX,
  () => props.offsetY
], () => {
  updateWatermark()
}, { deep: true })

// 监听可见性变化
watch(() => props.visible, (newValue) => {
  if (watermarkRef.value) {
    watermarkRef.value.style.display = newValue ? 'block' : 'none'
  }
})

// 监听防删除保护开关
watch(() => props.monitor, (newValue) => {
  if (newValue) {
    enableMonitor()
  } else {
    disableMonitor()
  }
})

// 组件挂载
onMounted(() => {
  nextTick(() => {
    updateContainerSize()
    observeContainer()
    
    if (props.monitor) {
      enableMonitor()
    }
  })
})

// 组件卸载
onBeforeUnmount(() => {
  unobserveContainer()
  disableMonitor()
})

// 暴露方法
defineExpose({
  updateWatermark,
  updateContainerSize
})

样式定义

1. 容器样式

.watermark-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.watermark-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  user-select: none;
  background-repeat: repeat;
}

.watermark-canvas {
  display: none;
}

2. 防选择样式

.watermark-layer {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  
  -webkit-user-drag: none;
  -moz-user-drag: none;
  -ms-user-drag: none;
  user-drag: none;
  
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

使用示例

1. 基础文字水印

<template>
  <Watermark text="机密文档" :opacity="0.1">
    <div class="content">
      <h1>文档标题</h1>
      <p>这里是文档内容...</p>
    </div>
  </Watermark>
</template>

<script setup>
import Watermark from '@/components/common/Watermark.vue'
</script>

2. 多行文字水印

<template>
  <Watermark 
    text="公司名称\n版权所有\n2025"
    :font-size="14"
    font-color="#cccccc"
    :rotate="-30"
    :gap-x="150"
    :gap-y="120"
  >
    <div class="document">
      <!-- 文档内容 -->
    </div>
  </Watermark>
</template>

3. 图片水印

<template>
  <Watermark 
    image="/logo.png"
    :width="100"
    :height="50"
    :opacity="0.15"
    :gap-x="200"
    :gap-y="150"
  >
    <div class="content">
      <!-- 内容区域 -->
    </div>
  </Watermark>
</template>

4. 动态水印

<template>
  <div>
    <div class="controls">
      <el-input v-model="watermarkText" placeholder="水印文字" />
      <el-slider v-model="opacity" :min="0" :max="1" :step="0.1" />
      <el-slider v-model="rotate" :min="-90" :max="90" />
    </div>
    
    <Watermark 
      :text="watermarkText"
      :opacity="opacity"
      :rotate="rotate"
      :monitor="true"
    >
      <div class="content">
        <h2>受保护的内容</h2>
        <p>这个内容受到水印保护</p>
      </div>
    </Watermark>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Watermark from '@/components/common/Watermark.vue'

const watermarkText = ref('保密文档')
const opacity = ref(0.1)
const rotate = ref(-20)
</script>

5. 用户信息水印

<template>
  <Watermark 
    :text="userWatermark"
    :font-size="12"
    font-color="#999999"
    :opacity="0.08"
    :gap-x="180"
    :gap-y="100"
    :monitor="true"
  >
    <div class="sensitive-content">
      <!-- 敏感内容 -->
    </div>
  </Watermark>
</template>

<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import Watermark from '@/components/common/Watermark.vue'

const userStore = useUserStore()

const userWatermark = computed(() => {
  const user = userStore.userInfo
  const now = new Date()
  return `${user.username}\n${user.realName}\n${now.toLocaleString()}`
})
</script>

6. 条件显示水印

<template>
  <Watermark 
    text="内部文档"
    :visible="showWatermark"
    :monitor="isProduction"
  >
    <div class="document">
      <!-- 文档内容 -->
    </div>
  </Watermark>
</template>

<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const showWatermark = computed(() => {
  // 根据用户角色决定是否显示水印
  return userStore.hasRole('guest') || userStore.hasRole('viewer')
})

const isProduction = computed(() => {
  return process.env.NODE_ENV === 'production'
})
</script>

高级功能

1. 自定义水印样式

// 创建渐变水印
const createGradientWatermark = (ctx) => {
  const gradient = ctx.createLinearGradient(0, 0, props.width, 0)
  gradient.addColorStop(0, 'rgba(255, 0, 0, 0.3)')
  gradient.addColorStop(0.5, 'rgba(0, 255, 0, 0.3)')
  gradient.addColorStop(1, 'rgba(0, 0, 255, 0.3)')
  
  ctx.fillStyle = gradient
  ctx.fillText(props.text, 0, 0)
}

// 创建阴影水印
const createShadowWatermark = (ctx) => {
  ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'
  ctx.shadowBlur = 4
  ctx.shadowOffsetX = 2
  ctx.shadowOffsetY = 2
  
  ctx.fillStyle = props.fontColor
  ctx.fillText(props.text, 0, 0)
}

2. 水印加密

// 简单的文字编码
const encodeText = (text) => {
  return btoa(encodeURIComponent(text))
}

const decodeText = (encodedText) => {
  try {
    return decodeURIComponent(atob(encodedText))
  } catch (e) {
    return encodedText
  }
}

注意事项

  1. 性能优化: 大量水印时注意Canvas性能
  2. 浏览器兼容: 某些老版本浏览器可能不支持部分API
  3. 安全性: 水印只是视觉保护,不能完全防止内容被复制
  4. 响应式: 确保水印在不同屏幕尺寸下正常显示
  5. 可访问性: 水印不应影响内容的可读性

相关文档


最后更新时间:2025-09-19