这个水印功能支持:
- 内容:动态"姓名 - 工号"(从用户登录信息获取)。
- 样式:可自定义开关、字体大小、颜色(默认浅灰 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 有 name 和 jobNumber。
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。