vue 水印组件 水印在最上层,不影响按钮操作

需求

最近项目需要加水印,但是加入的水印必须在最上层,不能影响水印下层的功能正常使用,通过查询找到了一个比较好的水印组件,并按需求优化,记录一下。

效果

在这里插入图片描述

解决方法

因为水印要在最上层,不影响下次功能的正常使用,所有需要使用css样式 pointer-events: none;。

.watermark-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

代码

index.vue

<template>
	<div>
    	<Watermark v-if="WatermarkDisplay" :content="WatermarkContent"></Watermark>
	</div>
</template>
<script>
import Watermark from './watermark.vue'
export default {
	components: {
		Watermark
	},
	data() {
		return {
			WatermarkDisplay: true, // 水印状态
      		WatermarkContent: '管理员', // 水印内容
		}
	}
}
</script>

watermark.vue

<template>
  <div class="watermark-container"/>
</template>

<script>
const rate = 350
let lastClick = Date.now() - rate
const BaseSize = 2
const FontGap = 3
const getPixelRatio = () => window.devicePixelRatio || 1
const toLowercaseSeparator = (key) => key.replace(/([A-Z])/g, '-$1').toLowerCase()
const getStyleStr = (style) =>
  Object.keys(style)
    .map((key) => `${toLowercaseSeparator(key)}: ${style[key]};`)
    .join(' ')

function reRendering(mutation, watermarkElement) {
  let flag = false
  // 是否删除水印节点
  if (mutation.removedNodes.length) {
    flag = Array.from(mutation.removedNodes).some((node) => node === watermarkElement)
  }
  // 是否修改过水印dom属性值
  if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
    flag = true
  }

  return flag
}
export default {
  name: 'Watermark',
  data() {
    return {
      watermarkRef: null,
      stopObservation: false,
      observe: null
    }
  },
  props: {
    zIndex: { type: Number, default: 9 },
    rotate: { type: Number, default: 45 },
    width: { type: [String, Number], default: 120 },
    height: { type: [String, Number], default: 64 },
    image: { type: String, default: '' },
    content: { type: [String, Array], default: '' },
    font: {
      type: Object,
      default: () => ({
        fontSize: 12,
        fontFamily: '思源黑体',
        fontStyle: 'normal',
        fontWeight: 'normal',
        color: 'rgba(0, 0, 0, 0.15)'
      })
    },
    clockwise: { type: Boolean, default: false },
    opacity: { type: Number, default: 1 },
    rootClassName: null,
    gap: { type: Array, default: () => [100, 100] },
    offset: { type: Array, default: () => [100, 50] }
  },
  computed: {
    markStyle() {
      const props = this.$props
      const [gapX, gapY] = props.gap
      const [offsetX, offsetY] = props.offset

      const gapXCenter = gapX / 2
      const gapYCenter = gapY / 2
      const offsetTop = offsetY || gapYCenter
      const offsetLeft = offsetX || gapXCenter

      const markStyle = {
        zIndex: this.zIndex,
        opacity: this.opacity,
        position: 'absolute',
        left: 0,
        top: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none',
        backgroundRepeat: 'repeat'
      }

      let positionLeft = offsetLeft - gapXCenter
      let positionTop = offsetTop - gapYCenter
      if (positionLeft > 0) {
        markStyle.left = `${positionLeft}px`
        markStyle.width = `calc(100% - ${positionLeft}px)`
        positionLeft = 0
      }
      if (positionTop > 0) {
        markStyle.top = `${positionTop}px`
        markStyle.height = `calc(100% - ${positionTop}px)`
        positionTop = 0
      }
      markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`

      return markStyle
    }
  },
  watch: {
    $props: {
      handler() {
        if (Date.now() - lastClick >= rate) {
          this.stopObservation = true
          this.renderWatermark()
          // 延迟执行
          setTimeout(() => {
            this.stopObservation = false
            lastClick = Date.now()
          })
        }
      },
      deep: true
    }
  },
  beforeDestroy() {
    this.destroyWatermark()
    this.observe.disconnect()
    this.observe = null
  },
  mounted() {
    this.renderWatermark()
    this.$nextTick(() => {
      this.observe = this.useMutationObserver(this.$el, this.onMutate, { attributes: true, childList: true, subtree: true })
    })
  },
  methods: {
    onMutate(records) {
      if (this.stopObservation) return

      records.forEach((mutation) => {
        if (!reRendering(mutation, this.watermarkRef)) return
        this.destroyWatermark()
        this.renderWatermark()
      })
    },
    useMutationObserver(target, callback, options) {
      const isSupported = typeof MutationObserver !== 'undefined'
      if (!isSupported) return false
      const observe = new MutationObserver(callback)
      observe.observe(target, options)
      return observe
    },
    getMarkSize(ctx) {
      const props = this.$props
      const { fontSize, fontFamily } = props.font

      let defaultWidth
      let defaultHeight
      const content = props.content
      const image = props.image
      const width = props.width
      const height = props.height

      if (!image && ctx.measureText) {
        ctx.font = `${Number(fontSize)}px ${fontFamily}`
        const contents = Array.isArray(content) ? content : [content]
        const widths = contents.map((item) => ctx.measureText(item).width)
        defaultWidth = Math.ceil(Math.max(...widths))
        defaultHeight = Number(fontSize.value) * contents.length + (contents.length - 1) * FontGap
      }

      return [width ?? defaultWidth, height ?? defaultHeight]
    },
    rotateWatermark(ctx, rotateX, rotateY, rotate) {
      const direction = this.$props.clockwise ? 1 : -1
      ctx.translate(rotateX, rotateY)
      ctx.rotate((Math.PI / 180) * Number(rotate) * direction)
      ctx.translate(-rotateX, -rotateY)
    },
    fillTexts(ctx, drawX, drawY, drawWidth, drawHeight) {
      const props = this.$props
      const { fontSize, fontFamily, fontStyle, fontWeight, color } = props.font

      const ratio = getPixelRatio()
      const content = props.content
      const mergedFontSize = Number(fontSize) * ratio
      ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${drawHeight}px ${fontFamily}`
      ctx.fillStyle = color
      ctx.textAlign = 'center'
      ctx.textBaseline = 'top'
      ctx.translate(drawWidth / 2, 0)
      const contents = Array.isArray(content) ? content : [content]
      contents?.forEach((item, index) => {
        ctx.fillText(item ?? '', drawX, drawY + index * (mergedFontSize + FontGap * ratio))
      })
    },
    appendWatermark(base64Url, markWidth) {
      if (!this.watermarkRef) return
      const props = this.$props
      const [gapX, gapY] = props.gap

      this.stopObservation = true
      const attrs = getStyleStr({ ...this.markStyle, backgroundImage: `url('${base64Url}')`, backgroundSize: `${(gapX + markWidth) * BaseSize}px` })
      this.watermarkRef.setAttribute('style', attrs)
      this.$el.append(this.watermarkRef)
      // 延迟执行
      setTimeout(() => {
        this.stopObservation = false
      })
    },
    renderWatermark() {
      const props = this.$props
      const [gapX, gapY] = props.gap

      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      const image = props.image
      const rotate = props.rotate

      if (!ctx) return false
      if (!this.watermarkRef) {
        this.watermarkRef = document.createElement('div')
      }

      const ratio = getPixelRatio()
      const [markWidth, markHeight] = this.getMarkSize(ctx)
      const canvasWidth = (gapX + markWidth) * ratio
      const canvasHeight = (gapY + markHeight) * ratio
      canvas.setAttribute('width', `${canvasWidth * BaseSize}px`)
      canvas.setAttribute('height', `${canvasHeight * BaseSize}px`)

      const drawX = (gapX * ratio) / 2
      const drawY = (gapY * ratio) / 2
      const drawWidth = markWidth * ratio
      const drawHeight = markHeight * ratio
      const rotateX = (drawWidth + gapX * ratio) / 2
      const rotateY = (drawHeight + gapY * ratio) / 2
      /** 备选绘图参数 */
      const alternateDrawX = drawX + canvasWidth
      const alternateDrawY = drawY + canvasHeight
      const alternateRotateX = rotateX + canvasWidth
      const alternateRotateY = rotateY + canvasHeight

      ctx.save()
      this.rotateWatermark(ctx, rotateX, rotateY, rotate)

      if (image) {
        const img = new Image()
        img.onload = () => {
          ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight)
          /** 旋转后绘制交错图 */
          ctx.restore()
          this.rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
          ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
          this.appendWatermark(canvas.toDataURL(), markWidth)
        }
        img.crossOrigin = 'anonymous'
        img.referrerPolicy = 'no-referrer'
        img.src = image
      } else {
        this.fillTexts(ctx, drawX, drawY, drawWidth, drawHeight)
        /**
         * 旋转后填充交错的文本
         * */
        ctx.restore()
        this.rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
        this.fillTexts(ctx, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
        this.appendWatermark(canvas.toDataURL(), markWidth)
      }
    },
    destroyWatermark() {
      if (!this.watermarkRef) return
      this.watermarkRef.remove()
      this.watermarkRef = undefined
    }
  }
  //  End
}
</script>

<style lang="scss" scoped>
.watermark-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
</style>

参考

借鉴与vue 水印组件
具体参数可以参考
在这里插入图片描述
在这里插入图片描述

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuelong-ming

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值