vue 水印组件

该文章展示了如何在Vue2中创建一个动态水印组件,允许用户自定义水印的内容、位置、角度、透明度等属性。组件支持文字和图片两种类型的水印,可以通过滑块进行实时调整,用于在页面上添加个性化且可配置的水印效果。
摘要由CSDN通过智能技术生成
vue2 水印效果
效果图展示

在这里插入图片描述

Watermark

参数说明类型默认值
width水印的宽度,content 的默认值为自身的宽度number120
height水印的高度,content 的默认值为自身的高度number64
rotate水印绘制时,旋转的角度,单位 °number-22
zIndex追加的水印元素的 z-indexnumber9
image图片源,建议导出 2 倍或 3 倍图,优先级高string-
content水印文字内容string | string[]-
font文字样式FontFont
gap水印之间的间距[number, number][100, 100]
clockwise顺时针旋转booleantrue
opacity水印的透明度 0~1number1
offset水印距离容器左上角的偏移量,默认为 gap/2[number, number][gap[0]/2, gap[1]/2]

Font

参数说明类型默认值
color字体颜色stringrgba(0,0,0,.15)
fontSize字体大小number16
fontWeight字体粗细normal | light | weight | numbernormal
fontFamily字体类型stringsans-serif
fontStyle字体样式none | normal | italic | obliquenormal
使用Watermark 组件
<!--
 * Copyright ©
 * #  
 * @author: zw
 * @date: 2023-05-11 
 -->

<template>
  <el-row v-parent-height>
    <el-col :span="8" class="bg-blue-50 h-80%">
      <el-card class="h-full" :body-style="{ margin: 0, padding: 0, height: '100%' }" v-style="{ marginTop: '80px' }">
        <el-form :model="form" ref="queryForm" :size="layoutSize" :inline="false" label-width="90px">
          <br />

          <el-col :span="22">
            <template v-for="(item, index) in form.content">
              <el-form-item :label="index <= 0 ? '水印内容' : '水印内容 ' + (index + 1)">
                <div class="flex items-center">
                  <el-input class="w-full" v-model="form.content[index]" placeholder="请输入文字信息" />

                  <el-button v-if="index === 0" class="ml-10" type="text" :size="layoutSize" icon="el-icon-circle-plus-outline" @click="form.content.splice(index + 1, 0, '')" />

                  <el-button v-if="index > 0" class="ml-10" v-style="{ color: 'red' }" type="text" :size="layoutSize" icon="el-icon-error" @click="form.content.splice(index, 1)" />
                </div>
              </el-form-item>
            </template>
          </el-col>

          <el-col :span="11">
            <el-form-item label="水平间距">
              <el-slider v-model="form.gap[0]" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="垂直间距">
              <el-slider v-model="form.gap[1]" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="水平偏移">
              <el-slider v-model="form.offset[0]" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="垂直偏移">
              <el-slider v-model="form.offset[1]" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="宽度">
              <el-slider v-model="form.width" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="高度">
              <el-slider v-model="form.height" :max="180" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="顺时针">
              <el-switch v-model="form.clockwise" :active-value="true" :inactive-value="false" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="旋转角度">
              <el-slider v-model="form.rotate" :max="90" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="水印层级">
              <el-slider v-model="form.zIndex" />
            </el-form-item>
          </el-col>

          <el-col :span="11">
            <el-form-item label="透明度">
              <el-slider v-model="form.opacity" :format-tooltip="(val) => val / 100" />
            </el-form-item>
          </el-col>

          <el-col :span="22">
            <el-form-item label="上传图片">
              <ImageUpload v-model="upload" @input="inputChange" :limit="1" />
            </el-form-item>
          </el-col>

          <el-col>
            <el-form-item>
              <el-button :size="layoutSize" type="info" @click="reset">重置</el-button>
            </el-form-item>
          </el-col>
        </el-form>
      </el-card>
    </el-col>

    <el-col :span="15" class="bg-blue-50 h-full">
      <Watermark v-parent-height v-bind="{ ...form, opacity: form.opacity / 100 }">
        <el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
        <el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
        <el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
        <el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
        <el-col class="mt-20 bg-yellow-50 text-gray-600" :span="13" :push="3"> Lorem, ipsum dolor sit amet consectetur adipisicing elit. Laudantium aut animi rerum veritatis? Nulla quas libero, rerum deserunt doloremque aspernatur, animi necessitatibus sunt a dicta nobis repellat odit, saepe maiores. </el-col>
      </Watermark>
    </el-col>
  </el-row>
</template>

<script>
import Watermark from '@/components/Watermark'
import ImageUpload from '@/components/ImageUpload'

export default {
  name: 'Watermark-template',
  components: { Watermark, ImageUpload },
  data() {
    return {
      form: {
        content: ['水印内容', '水印内容2', '水印内容3'],
        gap: [60, 60],
        offset: [100, 100],
        width: 120,
        height: 64,
        rotate: 45,
        zIndex: 9,
        image: '',
        clockwise: false,
        opacity: 100,
      },
      upload: [],
    }
  },

  mounted() {},

  methods: {
    reset() {
      Object.assign(this.$data.form, this.$options.data().form)
    },
    inputChange(_, list) {
      const image = list[0]?.base64
      this.form.image = image
    },
  },

  //  End
}
</script>

<style lang="css" scoped></style>

Watermark 组件
<!--
 * Copyright ©
 * #  
 * @author: zw
 * @date: 2023-05-09 
 -->

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

<script>
let 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: -22 },
    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: 16,
        fontFamily: 'sans-serif',
        fontStyle: 'normal',
        fontWeight: 'normal',
        color: 'rgba(0, 0, 0, 0.15)',
      }),
    },
    clockwise: { type: Boolean, default: true },
    opacity: { type: Number, default: 1 },
    rootClassName: '',
    gap: { type: Array, default: () => [20, 20] },
    offset: { type: Array, default: () => [100, 100] },
  },

  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
    },
  },

  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
  },
  //  End
}
</script>

<style lang="scss" scoped>
.watermark-container {
  position: relative;
  .watermark-content {
    position: relative;
    z-index: 1;
  }
}
</style>

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值