Vue 项目全局水印功能完整实现指南

这个水印功能支持:

  • 内容:动态"姓名 - 工号"(从用户登录信息获取)。
  • 样式:可自定义开关、字体大小、颜色(默认浅灰 rgba(0,0,0,0.1))、旋转角度、间距。
  • 显示:固定全屏(position: fixed),覆盖所有页面(路由切换/滚动不动),包括文件预览(叠加层)。
  • 管理:系统管理模块页面自定义配置,持久化到 sessionStorage。
  • 实现:纯 Canvas(无外部库依赖,避免 npm 冲突),响应 Vuex/Pinia 变化实时重绘。
  • 兼容:文件预览(iframe/img)中复用逻辑。

下面分别提供 Vue 2 (Element UI + Vuex)和 Vue 3 (Element Plus + Pinia)版本的完整代码。假设项目已安装 Vuex/Pinia 和 Element UI/Plus。测试时,确保登录后 userInfo 有 namejobNumber


Vue 2 版本(Element UI + Vuex)

1. Vuex 配置(store/modules/app.js)

合并 state 和 mutations(基于您的原始代码)。

state.js

javascript 复制代码
import { initUserInfo } from './utils';
import { getObjectFromSessionStorage, treeDataTranslate } from '@/utils';

export default {
  // ... 您的原始 state(如 isCollapse 等)
  userInfo: initUserInfo(),
  // 水印配置(新增)
  watermark: getObjectFromSessionStorage('watermarkConfig', {
    enabled: true,
    text: '',  // 登录时动态填充
    fontSize: 20,
    color: 'rgba(0, 0, 0, 0.1)',  // WeCom 浅灰
    rotate: -45,
    gap: 100
  }),
  // ... 其他 state
}

mutations.js

javascript 复制代码
import { SysMenuBindType } from '@/staticDict/index.js';
import { initUserInfo, findMenuItem } from './utils';
import { setObjectToSessionStorage, findItemFromList, treeDataTranslate } from '@/utils';

export default {
  // ... 您的原始 mutations
  setUserInfo: (state, info) => {
    setObjectToSessionStorage('userInfo', info);
    state.userInfo = initUserInfo(info);
    // 更新水印文本
    if (state.watermark) {
      const userName = info.name || info.userName || '';
      const jobNum = info.jobNumber || info.employeeId || '';
      state.watermark.text = `${userName} - ${jobNum}`.trim();
      setObjectToSessionStorage('watermarkConfig', state.watermark);
    }
  },
  // 新增:更新水印配置
  updateWatermark: (state, config) => {
    state.watermark = { ...state.watermark, ...config };
    setObjectToSessionStorage('watermarkConfig', state.watermark);
  },
  // ... 其他 mutations
}

2. Watermark 组件(src/components/Watermark.vue)

纯 Canvas 实现,响应 Vuex。

vue 复制代码
<template>
  <!-- 无需模板内容,Canvas 动态附加到 body -->
</template>

<script>
export default {
  name: 'Watermark',
  data() {
    return {
      canvasRef: null
    };
  },
  computed: {
    config() {
      return this.$store.state.watermark;
    }
  },
  watch: {
    config: {
      handler(newConfig) {
        if (newConfig.enabled && newConfig.text) {
          this.$nextTick(() => this.updateWatermark(newConfig));
        } else {
          this.destroyWatermark();
        }
      },
      deep: true,
      immediate: true
    }
  },
  mounted() {
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize);
    this.destroyWatermark();
  },
  methods: {
    updateWatermark(config) {
      this.destroyWatermark();
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      canvas.style.cssText = `
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        width: 100vw !important;
        height: 100vh !important;
        pointer-events: none !important;
        z-index: 99999 !important;
      `;

      ctx.save();
      ctx.translate(canvas.width / 2, canvas.height / 2);
      ctx.rotate((config.rotate * Math.PI) / 180);
      ctx.font = `bold ${config.fontSize}px Arial, sans-serif`;
      ctx.fillStyle = config.color;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';

      const maxOffset = Math.max(canvas.width, canvas.height) * 1.5;
      for (let x = -maxOffset; x < maxOffset; x += config.gap) {
        for (let y = -maxOffset; y < maxOffset; y += config.gap) {
          ctx.fillText(config.text, x, y);
        }
      }
      ctx.restore();

      document.body.appendChild(canvas);
      this.canvasRef = canvas;
    },
    destroyWatermark() {
      if (this.canvasRef) {
        this.canvasRef.remove();
        this.canvasRef = null;
      }
    },
    handleResize() {
      if (this.canvasRef) {
        this.updateWatermark(this.config);
      }
    }
  }
};
</script>

3. App.vue 全局集成

vue 复制代码
<template>
  <div id="app">
    <router-view />
    <watermark />
  </div>
</template>

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

export default {
  name: 'App',
  components: { Watermark }
};
</script>

4. 水印管理页面(src/views/system/WatermarkManage.vue)

vue 复制代码
<template>
  <div class="watermark-manage">
    <el-form :model="form" label-width="120px">
      <el-form-item label="水印开关">
        <el-switch v-model="form.enabled" @change="handleSwitchChange"></el-switch>
      </el-form-item>
      <el-form-item label="水印内容">
        <el-input v-model="form.text" placeholder="自动:姓名 + 工号"></el-input>
      </el-form-item>
      <el-form-item label="字体大小">
        <el-input-number v-model="form.fontSize" :min="10" :max="50"></el-input-number>
      </el-form-item>
      <el-form-item label="水印颜色">
        <el-color-picker v-model="form.color" show-alpha></el-color-picker>
      </el-form-item>
      <el-form-item label="旋转角度">
        <el-input-number v-model="form.rotate" :min="-90" :max="90"></el-input-number>°
      </el-form-item>
      <el-form-item label="水印间距">
        <el-input-number v-model="form.gap" :min="50" :max="300"></el-input-number>px
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="saveConfig">保存设置</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        enabled: true,
        text: '',
        fontSize: 20,
        color: 'rgba(0, 0, 0, 0.1)',
        rotate: -45,
        gap: 100
      }
    };
  },
  mounted() {
    const config = this.$store.state.watermark;
    this.form = { ...this.form, ...config };
    this.form.text = `${this.$store.state.userInfo.name || ''} - ${this.$store.state.userInfo.jobNumber || ''}`;
  },
  methods: {
    handleSwitchChange(val) {
      this.form.enabled = val;
      this.saveConfig();
    },
    saveConfig() {
      this.$store.commit('updateWatermark', this.form);
      this.$message.success('保存成功');
    }
  }
};
</script>

<style scoped>
.watermark-manage { padding: 20px; }
</style>

5. 文件预览支持(示例:src/components/FilePreview.vue)

vue 复制代码
<template>
  <div class="file-preview-wrapper">
    <iframe v-if="fileType === 'pdf'" :src="fileUrl" class="preview-content"></iframe>
    <img v-else :src="fileUrl" class="preview-content" />
    <!-- 叠加水印 -->
    <canvas ref="previewCanvas" class="preview-watermark"></canvas>
  </div>
</template>

<script>
export default {
  props: { fileUrl: String, fileType: String },
  mounted() {
    this.addPreviewWatermark();
    window.addEventListener('resize', this.addPreviewWatermark);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.addPreviewWatermark);
  },
  methods: {
    addPreviewWatermark() {
      const canvas = this.$refs.previewCanvas;
      const config = this.$store.state.watermark;
      if (!config.enabled || !config.text) return;
      // 复用 Watermark 的 updateWatermark 逻辑,但容器为 wrapper
      // ... (复制 Canvas 绘制代码,附加到 canvas)
      canvas.width = canvas.offsetWidth;
      canvas.height = canvas.offsetHeight;
      // 绘制... (同上)
    }
  }
};
</script>

<style scoped>
.file-preview-wrapper { position: relative; width: 100%; height: 100vh; }
.preview-content { width: 100%; height: 100%; }
.preview-watermark {
  position: absolute !important;
  top: 0 !important;
  left: 0 !important;
  width: 100% !important;
  height: 100% !important;
  pointer-events: none !important;
  z-index: 10 !important;
}
</style>

6. 路由添加(router/index.js)

javascript 复制代码
{
  path: '/system/watermark',
  name: 'WatermarkManage',
  component: () => import('@/views/system/WatermarkManage.vue'),
  meta: { requiresAuth: true }
}

Vue 3 版本(Element Plus + Pinia)

Vue 3 使用 Composition API 和 Pinia(替换 Vuex)。其他逻辑类似。

1. Pinia 配置(stores/app.js)

javascript 复制代码
import { defineStore } from 'pinia';
import { getObjectFromSessionStorage } from '@/utils';  // 假设有 utils

export const useAppStore = defineStore('app', {
  state: () => ({
    // ... 您的原始 state
    userInfo: { name: '', jobNumber: '' },  // 默认
    watermark: getObjectFromSessionStorage('watermarkConfig', {
      enabled: true,
      text: '',
      fontSize: 20,
      color: 'rgba(0, 0, 0, 0.1)',
      rotate: -45,
      gap: 100
    }),
    // ... 其他
  }),
  actions: {
    setUserInfo(info) {
      this.userInfo = info;
      // 更新水印
      const userName = info.name || info.userName || '';
      const jobNum = info.jobNumber || info.employeeId || '';
      this.watermark.text = `${userName} - ${jobNum}`.trim();
      sessionStorage.setItem('watermarkConfig', JSON.stringify(this.watermark));
    },
    updateWatermark(config) {
      this.watermark = { ...this.watermark, ...config };
      sessionStorage.setItem('watermarkConfig', JSON.stringify(this.watermark));
    }
  }
});

2. Watermark 组件(src/components/Watermark.vue)

使用 Composition API。

vue 复制代码
<template>
  <!-- 无需模板 -->
</template>

<script setup>
import { onMounted, onUnmounted, watch, computed } from 'vue';
import { useAppStore } from '@/stores/app';

const store = useAppStore();
const config = computed(() => store.watermark);
let canvasRef = null;

const updateWatermark = (cfg) => {
  if (canvasRef) canvasRef.remove();
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  canvas.style.cssText = `
    position: fixed !important;
    top: 0 !important;
    left: 0 !important;
    width: 100vw !important;
    height: 100vh !important;
    pointer-events: none !important;
    z-index: 99999 !important;
  `;

  ctx.save();
  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate((cfg.rotate * Math.PI) / 180);
  ctx.font = `bold ${cfg.fontSize}px Arial, sans-serif`;
  ctx.fillStyle = cfg.color;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';

  const maxOffset = Math.max(canvas.width, canvas.height) * 1.5;
  for (let x = -maxOffset; x < maxOffset; x += cfg.gap) {
    for (let y = -maxOffset; y < maxOffset; y += cfg.gap) {
      ctx.fillText(cfg.text, x, y);
    }
  }
  ctx.restore();

  document.body.appendChild(canvas);
  canvasRef = canvas;
};

const destroyWatermark = () => {
  if (canvasRef) {
    canvasRef.remove();
    canvasRef = null;
  }
};

watch(() => config.value, (newConfig) => {
  if (newConfig.enabled && newConfig.text) {
    updateWatermark(newConfig);
  } else {
    destroyWatermark();
  }
}, { deep: true, immediate: true });

const handleResize = () => {
  if (canvasRef) updateWatermark(config.value);
};

onMounted(() => {
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', handleResize);
  destroyWatermark();
});
</script>

3. App.vue 全局集成

vue 复制代码
<template>
  <div id="app">
    <router-view />
    <watermark />
  </div>
</template>

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

4. 水印管理页面(src/views/system/WatermarkManage.vue)

vue 复制代码
<template>
  <div class="watermark-manage">
    <el-form :model="form" label-width="120px">
      <!-- 同 Vue 2,替换 el- 为 Element Plus -->
      <el-form-item label="水印开关">
        <el-switch v-model="form.enabled" @change="handleSwitchChange"></el-switch>
      </el-form-item>
      <!-- ... 其他 form-item 同 Vue 2 -->
      <el-form-item>
        <el-button type="primary" @click="saveConfig">保存设置</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { useAppStore } from '@/stores/app';

const store = useAppStore();
const form = ref({
  enabled: true,
  text: '',
  fontSize: 20,
  color: 'rgba(0, 0, 0, 0.1)',
  rotate: -45,
  gap: 100
});

onMounted(() => {
  const config = store.watermark;
  Object.assign(form.value, config);
  form.value.text = `${store.userInfo.name || ''} - ${store.userInfo.jobNumber || ''}`;
});

const handleSwitchChange = (val) => {
  form.value.enabled = val;
  saveConfig();
};

const saveConfig = () => {
  store.updateWatermark(form.value);
  ElMessage.success('保存成功');
};
</script>

<style scoped>
.watermark-manage { padding: 20px; }
</style>

5. 文件预览支持

类似 Vue 2,使用 <script setup> 和 ref。

6. 路由添加

同 Vue 2。


通用测试与注意

  • 登录 :调用 store.setUserInfo({ name: '张三', jobNumber: '001' }),text 更新。
  • 管理:保存后实时重绘。
  • 文件预览 :在预览组件 mounted 中调用 addPreviewWatermark(复用绘制逻辑)。
  • 性能:resize 时重绘;大屏 gap 增大。
  • 迁移 :Vue 3 从 Vue 2 升级,用 vue add vuex → Pinia。
相关推荐
银河系的一束光4 小时前
tomcat问题
1024程序员节
黄毛火烧雪下4 小时前
【居中】相对定位 + 绝对定位 或 Flexbox 居中
1024程序员节
Samuel-Gyx4 小时前
数据结构--顺序表与链表
数据结构·算法·链表·1024程序员节
板鸭〈小号〉4 小时前
应用层自定义协议与序列化
运维·服务器·网络·1024程序员节
柳鲲鹏4 小时前
多种方法:OpenCV中修改像素RGB值
前端·javascript·opencv·1024程序员节
wanhengidc4 小时前
传奇手游可以使用云手机挂机搬砖吗
服务器·arm开发·智能手机·玩游戏·1024程序员节
吴禅染4 小时前
爱思唯尔期刊投稿经验
1024程序员节
MATLAB代码顾问4 小时前
MATLAB 实现基于短时傅里叶变换 (STFT) 的音频信号时频分析与可视化
1024程序员节
拓端研究室4 小时前
专题:2025AI+直播+私域电商行业洞察报告|附200+份报告PDF、数据仪表盘汇总下载
1024程序员节