制作一个简单的水印组件

背景

公司的页面功能要给合作的伙伴用 , 产品一拍脑袋: 我们能不能把页面加上自己的水印,更好的推销自己呢? fine, 这么一个需求就下来了

技术栈: vue2 + canvas

目的: 目标是实现一个动态生成的水印效果,支持自定义水印文字、字体、颜色、尺寸、旋转角度等属性,并确保水印覆盖整个页面且不会影响用户的正常操作

思路

  1. 利用 canvas 动态生成水印图案,灵活性高,支持自定义样式。
  2. canvas 转换为 base64 格式的图片 URL,方便直接应用于 CSS 背景。
  3. 通过监听 resize 事件重新生成水印,确保在不同屏幕尺寸下的显示效果。
  4. 水印层设置为不可交互(pointer-events: none),避免干扰用户操作。

代码实现

  • 使用
vue 复制代码
<watermark text="测试水印"/>
  • 视图容器
vue 复制代码
<template>
    <!-- 水印容器 -->
    <div class="watermark-container">
        <!-- 水印层,用于显示生成的水印 -->
        <div class="watermark" ref="watermark"></div>
        <!-- 内容区域,使用插槽允许父组件插入自定义内容 -->
        <div class="content">
            <slot></slot>
        </div>
    </div>
</template>
  • 逻辑代码
vue 复制代码
<script>
export default {
    name: 'Watermark', // 组件名称
    props: {
        // 水印文字内容
        text: {
            type: String,
            default: ''
        },
        // 水印字体样式,默认为 "16px Microsoft JhengHei"
        font: {
            type: String,
            default: '16px Microsoft JhengHei'
        },
        // 水印文字颜色,默认为半透明黑色
        textColor: {
            type: String,
            default: 'rgba(0, 0, 0, 0.1)'
        },
        // 水印单元宽度,默认 200px
        width: {
            type: Number,
            default: 200
        },
        // 水印单元高度,默认 200px
        height: {
            type: Number,
            default: 200
        },
        // 水印旋转角度,默认 -30 度
        rotate: {
            type: Number,
            default: -30
        }
    },
    mounted() {
        // 在组件挂载完成后渲染水印
        this.renderWatermark()
        // 监听窗口大小变化,重新渲染水印
        window.addEventListener('resize', this.renderWatermark)
    },
    beforeDestroy() {
        // 在组件销毁前移除事件监听器,避免内存泄漏
        window.removeEventListener('resize', this.renderWatermark)
    },
    methods: {
        renderWatermark() {
            // 创建一个 canvas 元素用于绘制水印
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d') // 获取 canvas 的 2D 上下文
            const ratio = window.devicePixelRatio || 1 // 获取设备像素比,确保高清显示

            // 设置 canvas 的宽高(考虑设备像素比)
            canvas.width = this.width * ratio
            canvas.height = this.height * ratio

            // 缩放上下文以适应设备像素比
            ctx.scale(ratio, ratio)

            // 设置水印文字样式
            ctx.font = this.font // 字体样式
            ctx.fillStyle = this.textColor // 文字颜色
            ctx.textAlign = 'center' // 文字水平居中对齐
            ctx.textBaseline = 'middle' // 文字垂直居中对齐

            // 平移和旋转画布
            ctx.translate(this.width / 2, this.height / 2) // 将画布原点移动到中心
            ctx.rotate((this.rotate * Math.PI) / 180) // 根据旋转角度旋转画布
            ctx.translate(-this.width / 2, -this.height / 2) // 将画布原点移回左上角

            // 绘制水印文字
            ctx.fillText(this.text, this.width / 2, this.height / 2) // 在画布中心绘制文字

            // 将 canvas 转换为 base64 格式的图片 URL
            const base64Url = canvas.toDataURL()

            // 将生成的图片设置为水印层的背景图
            const watermark = this.$refs.watermark
            watermark.style.backgroundImage = `url(${base64Url})`
        }
    }
}
</script>
  • 样式
vue 复制代码
<style scoped>
/* 水印容器样式 */
.watermark-container {
    position: relative; /* 相对定位 */
    width: 100%; /* 宽度占满父容器 */
    height: 100%; /* 高度占满父容器 */
}

/* 水印层样式 */
.watermark {
    position: fixed; /* 固定定位,覆盖整个页面 */
    top: 0;
    left: 0;
    width: 100%; /* 宽度占满页面 */
    height: 100%; /* 高度占满页面 */
    pointer-events: none; /* 禁用鼠标事件,不影响用户交互 */
    background-repeat: repeat; /* 背景图片重复平铺 */
    z-index: 9999; /* 确保水印层在最上层 */
}

/* 内容区域样式 */
.content {
    position: relative; /* 相对定位 */
    width: 100%; /* 宽度占满父容器 */
    height: 100%; /* 高度占满父容器 */
}
</style>

优化点

  1. 可以缓存生成的水印 base64 图片
  2. 目前只支持简单的文字水印,还不支持图片水印
  • 优化后代码实现:
vue 复制代码
<template>
    <div class="watermark-container">
        <!-- 水印层 -->
        <div class="watermark" ref="watermark"></div>
        <!-- 内容区域 -->
        <div class="content">
            <slot></slot>
        </div>
    </div>
</template>

<script>
export default {
    name: 'Watermark',
    props: {
        text: { // 水印文字内容
            type: String,
            default: ''
        },
        font: { // 水印字体样式
            type: String,
            default: '16px Microsoft JhengHei'
        },
        textColor: { // 水印文字颜色
            type: String,
            default: 'rgba(0, 0, 0, 0.1)'
        },
        imageSrc: { // 水印图片路径(新增)
            type: String,
            default: ''
        },
        width: { // 水印单元宽度
            type: Number,
            default: 200
        },
        height: { // 水印单元高度
            type: Number,
            default: 200
        },
        rotate: { // 水印旋转角度
            type: Number,
            default: -30
        },
        opacity: { // 水印透明度
            type: Number,
            default: 0.1
        },
        density: { // 水印密度
            type: Number,
            default: 1
        }
    },
    data() {
        return {
            cachedBase64Url: null // 缓存生成的水印 base64 图片
        }
    },
    mounted() {
        this.renderWatermark()
        window.addEventListener('resize', this.renderWatermark)
    },
    beforeDestroy() {
        window.removeEventListener('resize', this.renderWatermark)
    },
    watch: {
        // 监听所有影响水印的属性变化,重新生成水印
        text: 'renderWatermark',
        font: 'renderWatermark',
        textColor: 'renderWatermark',
        imageSrc: 'renderWatermark',
        width: 'renderWatermark',
        height: 'renderWatermark',
        rotate: 'renderWatermark',
        opacity: 'renderWatermark',
        density: 'renderWatermark'
    },
    methods: {
        renderWatermark() {
            // 如果缓存中已有 base64 图片且内容未变化,则直接使用缓存
            if (this.cachedBase64Url) {
                this.applyWatermark(this.cachedBase64Url)
                return
            }

            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            const ratio = window.devicePixelRatio || 1

            // 设置 canvas 尺寸
            canvas.width = this.width * ratio
            canvas.height = this.height * ratio
            ctx.scale(ratio, ratio)

            // 判断是文字水印还是图片水印
            if (this.imageSrc) {
                // 图片水印
                const img = new Image()
                img.crossOrigin = 'anonymous' // 避免跨域问题
                img.src = this.imageSrc

                // 等待图片加载完成后再绘制
                img.onload = () => {
                    // 绘制图片
                    ctx.drawImage(img, 0, 0, this.width, this.height)

                    // 生成 base64 图片 URL
                    const base64Url = canvas.toDataURL()

                    // 缓存生成的 base64 图片
                    this.cachedBase64Url = base64Url

                    // 应用水印
                    this.applyWatermark(base64Url)
                }
            } else {
                // 文字水印
                // 设置文字样式
                ctx.font = this.font
                ctx.fillStyle = this.textColor
                ctx.textAlign = 'center'
                ctx.textBaseline = 'middle'

                // 平移和旋转画布
                ctx.translate(this.width / 2, this.height / 2)
                ctx.rotate((this.rotate * Math.PI) / 180)
                ctx.translate(-this.width / 2, -this.height / 2)

                // 绘制文字
                ctx.fillText(this.text, this.width / 2, this.height / 2)

                // 生成 base64 图片 URL
                const base64Url = canvas.toDataURL()

                // 缓存生成的 base64 图片
                this.cachedBase64Url = base64Url

                // 应用水印
                this.applyWatermark(base64Url)
            }
        },
        applyWatermark(base64Url) {
            const watermark = this.$refs.watermark
            watermark.style.backgroundImage = `url(${base64Url})`
            watermark.style.opacity = this.opacity // 设置透明度
            watermark.style.backgroundSize = `${this.width * this.density}px ${this.height * this.density}px` // 设置水印密度
        }
    }
}
</script>

<style scoped>
/* 水印容器样式 */
.watermark-container {
    position: relative;
    width: 100%;
    height: 100%;
}

/* 水印层样式 */
.watermark {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* 禁用鼠标事件 */
    background-repeat: repeat; /* 背景图片重复平铺 */
    z-index: 9999; /* 确保水印层在最上层 */
}

/* 内容区域样式 */
.content {
    position: relative;
    width: 100%;
    height: 100%;
}
</style>
  • 使用
vue 复制代码
<Watermark text="Confidential" font="20px Arial" textColor="rgba(0, 0, 0, 0.2)" />
<Watermark text="Confidential" imageSrc="https://example.com/logo.png" width="200" height="200" />
相关推荐
EndingCoder29 分钟前
React从基础入门到高级实战:React 实战项目 - 项目三:实时聊天应用
前端·react.js·架构·前端框架
阿阳微客2 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏
德育处主任Pro2 小时前
『React』Fragment的用法及简写形式
前端·javascript·react.js
CodeBlossom2 小时前
javaweb -html -CSS
前端·javascript·html
打小就很皮...3 小时前
HBuilder 发行Android(apk包)全流程指南
前端·javascript·微信小程序
集成显卡4 小时前
PlayWright | 初识微软出品的 WEB 应用自动化测试框架
前端·chrome·测试工具·microsoft·自动化·edge浏览器
前端小趴菜055 小时前
React - 组件通信
前端·react.js·前端框架
Amy_cx5 小时前
在表单输入框按回车页面刷新的问题
前端·elementui
dancing9995 小时前
cocos3.X的oops框架oops-plugin-excel-to-json改进兼容多表单导出功能
前端·javascript·typescript·游戏程序
后海 0_o6 小时前
2025前端微服务 - 无界 的实战应用
前端·微服务·架构