TS+Vue3 如何实现全屏动态水印功能?

138 阅读2分钟

需求描述:

   给整个屏幕添加水印,并且水印中的时间动态更新。

实现效果:

实现步骤:

    1. 创建一个水印容器,覆盖整个屏幕,fixed定位,top:0, left:0, width和height 100%,pointer-events: none;,z-index高。

    2. 在这个容器内动态生成多个水印元素,比如文本节点,或者使用canvas绘制的水印图片作为背景。

    3. 使用MutationObserver来保护水印容器,防止被删除或修改。

    4. 动态更新水印,比如每隔一段时间重新生成水印图片。

    5.监听resize事件,在窗口大小变化时重新生成。

完整代码:

// Watermark.vue
<template>
  <div ref="watermarkContainer" class="watermark-container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue"

interface WatermarkOptions {
  updateInterval?: number // 更新间隔
  opacity?: number // 透明度
  fontSize?: number // 字体大小
  zIndex?: number // 层级
  angle?: number // 旋转角度
  colSpacing?: number //列间距(px)
  rowSpacing?: number //行间距(px)
}

const props = withDefaults(defineProps<WatermarkOptions>(), {
  updateInterval: 3000,
  opacity: 0.2,
  fontSize: 20,
  zIndex: 9999,
  angle: -15,
  rowSpacing: 160,
  colSpacing: 240
})

const watermarkContainer = ref<HTMLElement | null>(null)
let observer: MutationObserver | null = null
let updateTimer: number | null = null

// 生成水印文本
const generateText = (): string => {
  const timestamp = new Date().toLocaleString()
  return `百昌科技  ${timestamp} `
}
// 计算水印矩阵布局
const calculateLayout = () => {
  if (!watermarkContainer.value) return []

  const { clientWidth: width, clientHeight: height } = document.documentElement
  const colCount = Math.ceil(width / props.colSpacing) + 1
  const rowCount = Math.ceil(height / props.rowSpacing) + 1

  return Array.from({ length: rowCount }, (_, row) =>
    Array.from({ length: colCount }, (_, col) => ({
      x: col * props.colSpacing,
      y: row * props.rowSpacing
    }))
  ).flat()
}

// 创建单个水印元素
const createWatermark = (position: { x: number; y: number }): HTMLElement => {
  const watermark = document.createElement("div")
  watermark.className = "watermark"

  Object.assign(watermark.style, {
    left: `${position.x}px`,
    top: `${position.y}px`,
    transform: `translate(-50%, -50%) rotate(${props.angle}deg)`,
    opacity: props.opacity.toString(),
    fontSize: `${props.fontSize}px`,
    position: "absolute",
    whiteSpace: "nowrap",
    userSelect: "none",
    pointerEvents: "none",
    color: "#666"
  })

  watermark.textContent = generateText()

  return watermark
}

// 生成水印群组
const generateWatermarks = (): void => {
  if (!watermarkContainer.value) return

  watermarkContainer.value.innerHTML = ""
  const positions = calculateLayout()
  positions.forEach((position) => {
    watermarkContainer.value?.appendChild(createWatermark(position))
  })
}

// 动态更新水印
const updateWatermarks = (): void => {
  if (!watermarkContainer.value) return

  const watermarks = watermarkContainer.value.querySelectorAll(".watermark")
  watermarks.forEach((w) => {
    w.textContent = generateText()
  })
}

// 初始化防删除观察者
const initObserver = (): void => {
  observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (!document.body.contains(watermarkContainer.value)) {
        document.body.appendChild(watermarkContainer.value!)
        generateWatermarks()
      }
      Array.from(mutation.removedNodes).forEach((node) => {
        if (node === watermarkContainer.value) {
          document.body.appendChild(watermarkContainer.value!)
          generateWatermarks()
        }
      })
    })
  })

  observer.observe(document.body, {
    childList: true,
    subtree: true
  })
}

// 初始化水印
const initWatermark = (): void => {
  if (!watermarkContainer.value) return

  // 设置容器样式
  Object.assign(watermarkContainer.value.style, {
    position: "fixed",
    top: "0",
    left: "0",
    width: "100vw",
    height: "100vh",
    pointerEvents: "none",
    zIndex: props.zIndex.toString(),
    overflow: "hidden"
  })

  // 防止控制台删除
  Object.defineProperty(window, "watermarkContainer", {
    configurable: false,
    writable: false,
    value: watermarkContainer.value
  })

  generateWatermarks()
  initObserver()
}

onMounted(() => {
  initWatermark()
  updateTimer = setInterval(updateWatermarks, props.updateInterval)
  window.addEventListener("resize", generateWatermarks)
})

onBeforeUnmount(() => {
  if (observer) observer.disconnect()
  if (updateTimer) clearInterval(updateTimer)
  window.removeEventListener("resize", generateWatermarks)
})
</script>

<style scoped>
.watermark-container :deep(.watermark) {
  position: absolute;
  opacity: v-bind("props.opacity");
  color: #666;
  font-size: v-bind('props.fontSize + "px"');
  user-select: none;
  white-space: nowrap;
  animation: move 20s linear infinite;
}

/* @keyframes move {
  0% {
    transform: translate(-50%, -50%);
  }
  100% {
    transform: translate(150%, 150%);
  }
} */
</style>
import Watermark from "../components/Watermark.vue

<Watermark :angle="-15" :row-spacing="180" :col-spacing="300" :opacity="0.18" :update-interval="5000" />