vue 水印组件
Watermark.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';interface Props {text?: string;fontSize?: number;color?: string;rotate?: number;zIndex?: number;gap?: number;
}const props = withDefaults(defineProps<Props>(), {text: 'Watermark',fontSize: 16,color: 'rgba(0, 0, 0, 0.1)',rotate: -45,zIndex: 1000,gap: 100
});const watermarkRef = ref<HTMLDivElement>();
const containerRef = ref<HTMLDivElement>();
let observer: MutationObserver | null = null;
let styleObserver: MutationObserver | null = null;
let securityInterval: number | null = null;const createWatermark = () => {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');if (!ctx) return '';ctx.font = `${props.fontSize}px Arial`;const textWidth = ctx.measureText(props.text).width;const width = textWidth + props.gap;const height = props.fontSize + props.gap;canvas.width = width;canvas.height = height;ctx.translate(width / 2, height / 2);ctx.rotate((props.rotate * Math.PI) / 180);ctx.font = `${props.fontSize}px Arial`;ctx.fillStyle = props.color;ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText(props.text, 0, 0);return canvas.toDataURL();
};const applyWatermark = () => {if (!watermarkRef.value) return;const url = createWatermark();const originalStyles = {position: 'absolute',top: '0',left: '0',width: '100%',height: '100%',pointerEvents: 'none',backgroundImage: `url(${url})`,backgroundRepeat: 'repeat',userSelect: 'none',webkitUserSelect: 'none',zIndex: props.zIndex.toString(),opacity: '1',display: 'block',visibility: 'visible'};Object.assign(watermarkRef.value.style, originalStyles);
};const observeContainer = () => {if (!containerRef.value || !watermarkRef.value) return;observer = new MutationObserver((mutations) => {mutations.forEach((mutation) => {if (mutation.type === 'childList') {const removedNodes = Array.from(mutation.removedNodes);if (removedNodes.includes(watermarkRef.value!)) {containerRef.value?.appendChild(watermarkRef.value!);applyWatermark();}}});});observer.observe(containerRef.value, {childList: true,subtree: true,attributes: true});
};const observeWatermark = () => {if (!watermarkRef.value) return;styleObserver = new MutationObserver(() => {applyWatermark();});styleObserver.observe(watermarkRef.value, {attributes: true,attributeFilter: ['style', 'class']});
};const startSecurityCheck = () => {securityInterval = window.setInterval(() => {if (!watermarkRef.value?.isConnected) {containerRef.value?.appendChild(watermarkRef.value!);}applyWatermark();}, 100) as unknown as number;
};watch(() => [props.text, props.fontSize, props.color, props.rotate, props.gap], () => {applyWatermark();
});onMounted(() => {applyWatermark();observeContainer();observeWatermark();startSecurityCheck();
});onUnmounted(() => {observer?.disconnect();styleObserver?.disconnect();if (securityInterval) {clearInterval(securityInterval);}
});
</script><template><div ref="containerRef" class="watermark-container"><div ref="watermarkRef" class="watermark"></div><div class="content"><slot></slot></div></div>
</template><style scoped>
.watermark-container {position: relative;width: 100%;height: 100%;
}.watermark {position: absolute;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;background-repeat: repeat;user-select: none;-webkit-user-select: none;
}.content {position: relative;width: 100%;height: 100%;
}
</style>