w-vue3-watermark 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "w-vue3-watermark",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "vue3 水印组件",
5
5
  "main": "./watermark.umd.js",
6
6
  "module": "./watermark.es.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./watermark.es.js",
10
+ "require": "./watermark.umd.js"
11
+ }
12
+ },
7
13
  "scripts": {
8
14
  "test": "echo \"Error: no test specified\" && exit 1"
9
15
  },
package/watermark.es.js CHANGED
@@ -124,7 +124,7 @@ var components = [/* @__PURE__ */ defineComponent({
124
124
  }
125
125
  })], packages_default = function(e) {
126
126
  components.forEach((t) => {
127
- e.component(t.name, t);
127
+ e.component("watermark", t);
128
128
  });
129
129
  };
130
130
  export { packages_default as default };
@@ -1 +1 @@
1
- {"version":3,"file":"watermark.es.js","names":["watermark"],"sources":["../../src/packages/water-mark/useWatermarkBg.ts","../../src/packages/water-mark/index.vue","../../src/packages/water-mark/index.vue","../../src/packages/index.js"],"sourcesContent":["import { computed } from 'vue'\r\n\r\ntype Props = {\r\n waterName: string | string[]\r\n waterFontSize: number\r\n waterFontWeight: number\r\n waterGap: number\r\n waterRotate: number\r\n waterFontFamily: string\r\n waterTextAlign: string\r\n waterFontColor: string\r\n shadowColor: string\r\n shadowOffsetX: number\r\n shadowOffsetY: number\r\n shadowBlur: number\r\n}\r\n\r\nexport default function useWatermarkBg(props: Props) {\r\n // 获取数组中宽度最长的文本和对应的最大宽度\r\n function getLongestTextByWidth(textArr: string | string[], ctx: CanvasRenderingContext2D) {\r\n // 边界处理:空数组直接返回空结果\r\n if (!Array.isArray(textArr) || textArr.length === 0) {\r\n return {\r\n longestText: '',\r\n maxWidth: 0,\r\n }\r\n }\r\n\r\n // 字符串直接返回宽度\r\n if (typeof textArr === 'string') {\r\n return {\r\n longestText: textArr,\r\n maxWidth: ctx.measureText(textArr).width,\r\n }\r\n }\r\n\r\n // 初始化变量:记录最长文本和最大宽度\r\n let longestText = textArr[0]\r\n let maxWidth = ctx.measureText(longestText as string).width\r\n\r\n // 遍历数组,逐个测量并比较宽度\r\n for (let i = 1; i < textArr.length; i++) {\r\n const currentText = textArr[i]\r\n const currentWidth = ctx.measureText(currentText as string).width\r\n\r\n if (currentWidth > maxWidth) {\r\n maxWidth = currentWidth\r\n longestText = currentText\r\n }\r\n }\r\n\r\n return {\r\n longestText,\r\n maxWidth,\r\n }\r\n }\r\n\r\n return computed(() => {\r\n const canvas = document.createElement('canvas')\r\n const devicePixelRatio = window.devicePixelRatio || 1\r\n const fontSize = props.waterFontSize * devicePixelRatio\r\n const font = `${props.waterFontWeight} ${fontSize}px ${props.waterFontFamily}`\r\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D\r\n const waterGap = props.waterGap * devicePixelRatio // 统一缓存适配后的间距\r\n\r\n // 第一步:先设置字体,测量文字最大宽度\r\n ctx.font = font\r\n const { maxWidth } = getLongestTextByWidth(props.waterName, ctx)\r\n\r\n // 第二步:精准计算 Canvas 尺寸(平衡左右上下空间,无冗余浪费)\r\n // 核心:基于文字实际宽度+字体大小+间距,计算旋转后刚好容纳的画布尺寸\r\n const textTotalWidth = maxWidth // 文字原始最大宽度\r\n const textTotalHeight = Array.isArray(props.waterName)\r\n ? (props.waterName.length - 1) * (fontSize + 6 * devicePixelRatio) + fontSize\r\n : fontSize // 文字整体高度(多行文适配)\r\n // 画布基础尺寸:文字整体尺寸 + 双向间距(左右/上下各留一份gap,避免贴边)\r\n const baseCanvasSize =\r\n Math.max(100, Math.sqrt(textTotalWidth ** 2 + textTotalHeight ** 2)) + waterGap * 2\r\n canvas.width = baseCanvasSize\r\n canvas.height = baseCanvasSize\r\n\r\n // 第三步:重置上下文,配置样式(保持原有功能不变)\r\n ctx.clearRect(0, 0, canvas.width, canvas.height)\r\n ctx.font = font\r\n ctx.fillStyle = props.waterFontColor\r\n // @ts-ignore\r\n ctx.textAlign = props.waterTextAlign\r\n ctx.textBaseline = 'middle'\r\n ctx.shadowColor = props.shadowColor\r\n ctx.shadowOffsetX = props.shadowOffsetX\r\n ctx.shadowOffsetY = props.shadowOffsetY\r\n ctx.shadowBlur = props.shadowBlur\r\n\r\n // 第四步:核心修复——合理设置旋转中心+最小必要偏移(平衡左右上下)\r\n // 1. 旋转中心移至画布「左侧中间偏上」(而非中心),避免左右空间失衡\r\n // 2. 仅预留最小必要偏移(waterGap),既避免左上角截取,又不挤占右侧空间\r\n const safeOffset = waterGap // 安全偏移量(最小必要,兼顾上下左右)\r\n ctx.translate(safeOffset, canvas.height / 2) // 水平仅偏移安全间距,垂直居中\r\n\r\n // 旋转水印(保持原有配置,无过度偏移)\r\n ctx.rotate((Math.PI / 180) * -props.waterRotate)\r\n\r\n // 第五步:绘制文字(基于安全偏移,无过度靠右,保证右侧完整)\r\n if (Array.isArray(props.waterName)) {\r\n props.waterName.forEach((item, index) => {\r\n // 垂直间距:适配设备像素比,保持原有排版逻辑\r\n const verticalGap = fontSize + 6 * devicePixelRatio\r\n // 绘制起点:(0, index*verticalGap),无额外水平偏移,避免右侧溢出\r\n ctx.fillText(item, 0, index * verticalGap)\r\n })\r\n } else {\r\n // 单文本直接绘制,起点(0,0),基于已有的safeOffset保证左上角不截取\r\n ctx.fillText(props.waterName, 0, 0)\r\n }\r\n\r\n // 返回水印信息\r\n return {\r\n base64: canvas.toDataURL(),\r\n size: baseCanvasSize,\r\n styleSize: baseCanvasSize / devicePixelRatio,\r\n }\r\n })\r\n}\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","import watermark from './water-mark/index.vue';\r\n\r\nconst components = [ watermark ]\r\n\r\n// 注册组件\r\nconst install = function (Vue) {\r\n components.forEach((k) => {\r\n Vue.component(k.name, k)\r\n })\r\n}\r\n\r\nexport default install;"],"mappings":"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"}
1
+ {"version":3,"file":"watermark.es.js","names":["watermark"],"sources":["../../src/packages/water-mark/useWatermarkBg.ts","../../src/packages/water-mark/index.vue","../../src/packages/water-mark/index.vue","../../src/packages/index.js"],"sourcesContent":["import { computed } from 'vue'\r\n\r\ntype Props = {\r\n waterName: string | string[]\r\n waterFontSize: number\r\n waterFontWeight: number\r\n waterGap: number\r\n waterRotate: number\r\n waterFontFamily: string\r\n waterTextAlign: string\r\n waterFontColor: string\r\n shadowColor: string\r\n shadowOffsetX: number\r\n shadowOffsetY: number\r\n shadowBlur: number\r\n}\r\n\r\nexport default function useWatermarkBg(props: Props) {\r\n // 获取数组中宽度最长的文本和对应的最大宽度\r\n function getLongestTextByWidth(textArr: string | string[], ctx: CanvasRenderingContext2D) {\r\n // 边界处理:空数组直接返回空结果\r\n if (!Array.isArray(textArr) || textArr.length === 0) {\r\n return {\r\n longestText: '',\r\n maxWidth: 0,\r\n }\r\n }\r\n\r\n // 字符串直接返回宽度\r\n if (typeof textArr === 'string') {\r\n return {\r\n longestText: textArr,\r\n maxWidth: ctx.measureText(textArr).width,\r\n }\r\n }\r\n\r\n // 初始化变量:记录最长文本和最大宽度\r\n let longestText = textArr[0]\r\n let maxWidth = ctx.measureText(longestText as string).width\r\n\r\n // 遍历数组,逐个测量并比较宽度\r\n for (let i = 1; i < textArr.length; i++) {\r\n const currentText = textArr[i]\r\n const currentWidth = ctx.measureText(currentText as string).width\r\n\r\n if (currentWidth > maxWidth) {\r\n maxWidth = currentWidth\r\n longestText = currentText\r\n }\r\n }\r\n\r\n return {\r\n longestText,\r\n maxWidth,\r\n }\r\n }\r\n\r\n return computed(() => {\r\n const canvas = document.createElement('canvas')\r\n const devicePixelRatio = window.devicePixelRatio || 1\r\n const fontSize = props.waterFontSize * devicePixelRatio\r\n const font = `${props.waterFontWeight} ${fontSize}px ${props.waterFontFamily}`\r\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D\r\n const waterGap = props.waterGap * devicePixelRatio // 统一缓存适配后的间距\r\n\r\n // 第一步:先设置字体,测量文字最大宽度\r\n ctx.font = font\r\n const { maxWidth } = getLongestTextByWidth(props.waterName, ctx)\r\n\r\n // 第二步:精准计算 Canvas 尺寸(平衡左右上下空间,无冗余浪费)\r\n // 核心:基于文字实际宽度+字体大小+间距,计算旋转后刚好容纳的画布尺寸\r\n const textTotalWidth = maxWidth // 文字原始最大宽度\r\n const textTotalHeight = Array.isArray(props.waterName)\r\n ? (props.waterName.length - 1) * (fontSize + 6 * devicePixelRatio) + fontSize\r\n : fontSize // 文字整体高度(多行文适配)\r\n // 画布基础尺寸:文字整体尺寸 + 双向间距(左右/上下各留一份gap,避免贴边)\r\n const baseCanvasSize =\r\n Math.max(100, Math.sqrt(textTotalWidth ** 2 + textTotalHeight ** 2)) + waterGap * 2\r\n canvas.width = baseCanvasSize\r\n canvas.height = baseCanvasSize\r\n\r\n // 第三步:重置上下文,配置样式(保持原有功能不变)\r\n ctx.clearRect(0, 0, canvas.width, canvas.height)\r\n ctx.font = font\r\n ctx.fillStyle = props.waterFontColor\r\n // @ts-ignore\r\n ctx.textAlign = props.waterTextAlign\r\n ctx.textBaseline = 'middle'\r\n ctx.shadowColor = props.shadowColor\r\n ctx.shadowOffsetX = props.shadowOffsetX\r\n ctx.shadowOffsetY = props.shadowOffsetY\r\n ctx.shadowBlur = props.shadowBlur\r\n\r\n // 第四步:核心修复——合理设置旋转中心+最小必要偏移(平衡左右上下)\r\n // 1. 旋转中心移至画布「左侧中间偏上」(而非中心),避免左右空间失衡\r\n // 2. 仅预留最小必要偏移(waterGap),既避免左上角截取,又不挤占右侧空间\r\n const safeOffset = waterGap // 安全偏移量(最小必要,兼顾上下左右)\r\n ctx.translate(safeOffset, canvas.height / 2) // 水平仅偏移安全间距,垂直居中\r\n\r\n // 旋转水印(保持原有配置,无过度偏移)\r\n ctx.rotate((Math.PI / 180) * -props.waterRotate)\r\n\r\n // 第五步:绘制文字(基于安全偏移,无过度靠右,保证右侧完整)\r\n if (Array.isArray(props.waterName)) {\r\n props.waterName.forEach((item, index) => {\r\n // 垂直间距:适配设备像素比,保持原有排版逻辑\r\n const verticalGap = fontSize + 6 * devicePixelRatio\r\n // 绘制起点:(0, index*verticalGap),无额外水平偏移,避免右侧溢出\r\n ctx.fillText(item, 0, index * verticalGap)\r\n })\r\n } else {\r\n // 单文本直接绘制,起点(0,0),基于已有的safeOffset保证左上角不截取\r\n ctx.fillText(props.waterName, 0, 0)\r\n }\r\n\r\n // 返回水印信息\r\n return {\r\n base64: canvas.toDataURL(),\r\n size: baseCanvasSize,\r\n styleSize: baseCanvasSize / devicePixelRatio,\r\n }\r\n })\r\n}\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","import watermark from './water-mark/index.vue'\r\n\r\nconst components = [watermark]\r\n\r\n// 注册组件\r\nconst install = function (Vue) {\r\n components.forEach((k) => {\r\n Vue.component('watermark', k)\r\n })\r\n}\r\n\r\nexport default install\r\n"],"mappings":"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"}
package/watermark.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t(require(`vue`)):typeof define==`function`&&define.amd?define([`vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,e.Watermark=t(e.Vue))})(this,function(e){function t(t){function n(e,t){if(!Array.isArray(e)||e.length===0)return{longestText:``,maxWidth:0};if(typeof e==`string`)return{longestText:e,maxWidth:t.measureText(e).width};let n=e[0],r=t.measureText(n).width;for(let i=1;i<e.length;i++){let a=e[i],o=t.measureText(a).width;o>r&&(r=o,n=a)}return{longestText:n,maxWidth:r}}return(0,e.computed)(()=>{let e=document.createElement(`canvas`),r=window.devicePixelRatio||1,i=t.waterFontSize*r,a=`${t.waterFontWeight} ${i}px ${t.waterFontFamily}`,o=e.getContext(`2d`),s=t.waterGap*r;o.font=a;let{maxWidth:c}=n(t.waterName,o),l=c,u=Array.isArray(t.waterName)?(t.waterName.length-1)*(i+6*r)+i:i,d=Math.max(100,Math.sqrt(l**2+u**2))+s*2;e.width=d,e.height=d,o.clearRect(0,0,e.width,e.height),o.font=a,o.fillStyle=t.waterFontColor,o.textAlign=t.waterTextAlign,o.textBaseline=`middle`,o.shadowColor=t.shadowColor,o.shadowOffsetX=t.shadowOffsetX,o.shadowOffsetY=t.shadowOffsetY,o.shadowBlur=t.shadowBlur;let f=s;return o.translate(f,e.height/2),o.rotate(Math.PI/180*-t.waterRotate),Array.isArray(t.waterName)?t.waterName.forEach((e,t)=>{let n=i+6*r;o.fillText(e,0,t*n)}):o.fillText(t.waterName,0,0),{base64:e.toDataURL(),size:d,styleSize:d/r}})}var n=[(0,e.defineComponent)({__name:`index`,props:{waterName:{type:[String,Array],required:!0,default:`watermark`},waterFontSize:{type:Number,default:20},waterFontWeight:{type:Number,default:700},waterGap:{type:Number,default:100},waterRotate:{type:Number,default:20},waterFontFamily:{type:String,default:`Microsoft Yahei`},waterTextAlign:{type:String,default:`left`},waterFontColor:{type:String,default:`rgba(255, 255, 255, 1)`},shadowColor:{type:String,default:`rgba(61, 61, 61, 0.25)`},shadowOffsetX:{type:Number,default:1.5},shadowOffsetY:{type:Number,default:1.5},shadowBlur:{type:Number,default:0}},setup(n){let r=n,i=(0,e.ref)(),a=(0,e.ref)(),o=(0,e.ref)(),s=(0,e.ref)(),c=(0,e.ref)(),l=t(r);s.value=l.value.base64,c.value=l.value.styleSize,(0,e.watch)(r,e=>{let n=t(e);s.value=n.value.base64,c.value=n.value.styleSize,document.querySelectorAll(`.w-water-mask-bg`).forEach(e=>{e.remove()}),u()},{deep:!0}),(0,e.onMounted)(()=>{u(),o.value=new MutationObserver(e=>{for(let t of e){for(let e of t.removedNodes)if(e===a.value){a.value?.remove(),u();return}if(t.target===a.value){a.value.remove(),u();return}}}),o.value?.observe(i.value,{childList:!0,attributes:!0,subtree:!0})});let u=()=>{a.value=document.createElement(`div`),a.value.style.backgroundImage=`url(${s.value})`,a.value.classList.add(`w-water-mask-bg`),a.value.style.inset=0,a.value.style.zIndex=9999,a.value.style.position=`absolute`,a.value.style.backgroundRepeat=`repeat`,a.value.style.backgroundSize=`${c.value}px ${c.value}px`,a.value.style.pointerEvents=`none`,i.value?.appendChild(a.value)};return(t,n)=>((0,e.openBlock)(),(0,e.createElementBlock)(`div`,{class:`watermark-container`,ref_key:`watermarkContainer`,ref:i},[(0,e.renderSlot)(t.$slots,`default`)],512))}})];return function(e){n.forEach(t=>{e.component(t.name,t)})}});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t(require(`vue`)):typeof define==`function`&&define.amd?define([`vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,e.Watermark=t(e.Vue))})(this,function(e){function t(t){function n(e,t){if(!Array.isArray(e)||e.length===0)return{longestText:``,maxWidth:0};if(typeof e==`string`)return{longestText:e,maxWidth:t.measureText(e).width};let n=e[0],r=t.measureText(n).width;for(let i=1;i<e.length;i++){let a=e[i],o=t.measureText(a).width;o>r&&(r=o,n=a)}return{longestText:n,maxWidth:r}}return(0,e.computed)(()=>{let e=document.createElement(`canvas`),r=window.devicePixelRatio||1,i=t.waterFontSize*r,a=`${t.waterFontWeight} ${i}px ${t.waterFontFamily}`,o=e.getContext(`2d`),s=t.waterGap*r;o.font=a;let{maxWidth:c}=n(t.waterName,o),l=c,u=Array.isArray(t.waterName)?(t.waterName.length-1)*(i+6*r)+i:i,d=Math.max(100,Math.sqrt(l**2+u**2))+s*2;e.width=d,e.height=d,o.clearRect(0,0,e.width,e.height),o.font=a,o.fillStyle=t.waterFontColor,o.textAlign=t.waterTextAlign,o.textBaseline=`middle`,o.shadowColor=t.shadowColor,o.shadowOffsetX=t.shadowOffsetX,o.shadowOffsetY=t.shadowOffsetY,o.shadowBlur=t.shadowBlur;let f=s;return o.translate(f,e.height/2),o.rotate(Math.PI/180*-t.waterRotate),Array.isArray(t.waterName)?t.waterName.forEach((e,t)=>{let n=i+6*r;o.fillText(e,0,t*n)}):o.fillText(t.waterName,0,0),{base64:e.toDataURL(),size:d,styleSize:d/r}})}var n=[(0,e.defineComponent)({__name:`index`,props:{waterName:{type:[String,Array],required:!0,default:`watermark`},waterFontSize:{type:Number,default:20},waterFontWeight:{type:Number,default:700},waterGap:{type:Number,default:100},waterRotate:{type:Number,default:20},waterFontFamily:{type:String,default:`Microsoft Yahei`},waterTextAlign:{type:String,default:`left`},waterFontColor:{type:String,default:`rgba(255, 255, 255, 1)`},shadowColor:{type:String,default:`rgba(61, 61, 61, 0.25)`},shadowOffsetX:{type:Number,default:1.5},shadowOffsetY:{type:Number,default:1.5},shadowBlur:{type:Number,default:0}},setup(n){let r=n,i=(0,e.ref)(),a=(0,e.ref)(),o=(0,e.ref)(),s=(0,e.ref)(),c=(0,e.ref)(),l=t(r);s.value=l.value.base64,c.value=l.value.styleSize,(0,e.watch)(r,e=>{let n=t(e);s.value=n.value.base64,c.value=n.value.styleSize,document.querySelectorAll(`.w-water-mask-bg`).forEach(e=>{e.remove()}),u()},{deep:!0}),(0,e.onMounted)(()=>{u(),o.value=new MutationObserver(e=>{for(let t of e){for(let e of t.removedNodes)if(e===a.value){a.value?.remove(),u();return}if(t.target===a.value){a.value.remove(),u();return}}}),o.value?.observe(i.value,{childList:!0,attributes:!0,subtree:!0})});let u=()=>{a.value=document.createElement(`div`),a.value.style.backgroundImage=`url(${s.value})`,a.value.classList.add(`w-water-mask-bg`),a.value.style.inset=0,a.value.style.zIndex=9999,a.value.style.position=`absolute`,a.value.style.backgroundRepeat=`repeat`,a.value.style.backgroundSize=`${c.value}px ${c.value}px`,a.value.style.pointerEvents=`none`,i.value?.appendChild(a.value)};return(t,n)=>((0,e.openBlock)(),(0,e.createElementBlock)(`div`,{class:`watermark-container`,ref_key:`watermarkContainer`,ref:i},[(0,e.renderSlot)(t.$slots,`default`)],512))}})];return function(e){n.forEach(t=>{e.component(`watermark`,t)})}});
2
2
  //# sourceMappingURL=watermark.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"watermark.umd.js","names":["watermark"],"sources":["../../src/packages/water-mark/useWatermarkBg.ts","../../src/packages/water-mark/index.vue","../../src/packages/water-mark/index.vue","../../src/packages/index.js"],"sourcesContent":["import { computed } from 'vue'\r\n\r\ntype Props = {\r\n waterName: string | string[]\r\n waterFontSize: number\r\n waterFontWeight: number\r\n waterGap: number\r\n waterRotate: number\r\n waterFontFamily: string\r\n waterTextAlign: string\r\n waterFontColor: string\r\n shadowColor: string\r\n shadowOffsetX: number\r\n shadowOffsetY: number\r\n shadowBlur: number\r\n}\r\n\r\nexport default function useWatermarkBg(props: Props) {\r\n // 获取数组中宽度最长的文本和对应的最大宽度\r\n function getLongestTextByWidth(textArr: string | string[], ctx: CanvasRenderingContext2D) {\r\n // 边界处理:空数组直接返回空结果\r\n if (!Array.isArray(textArr) || textArr.length === 0) {\r\n return {\r\n longestText: '',\r\n maxWidth: 0,\r\n }\r\n }\r\n\r\n // 字符串直接返回宽度\r\n if (typeof textArr === 'string') {\r\n return {\r\n longestText: textArr,\r\n maxWidth: ctx.measureText(textArr).width,\r\n }\r\n }\r\n\r\n // 初始化变量:记录最长文本和最大宽度\r\n let longestText = textArr[0]\r\n let maxWidth = ctx.measureText(longestText as string).width\r\n\r\n // 遍历数组,逐个测量并比较宽度\r\n for (let i = 1; i < textArr.length; i++) {\r\n const currentText = textArr[i]\r\n const currentWidth = ctx.measureText(currentText as string).width\r\n\r\n if (currentWidth > maxWidth) {\r\n maxWidth = currentWidth\r\n longestText = currentText\r\n }\r\n }\r\n\r\n return {\r\n longestText,\r\n maxWidth,\r\n }\r\n }\r\n\r\n return computed(() => {\r\n const canvas = document.createElement('canvas')\r\n const devicePixelRatio = window.devicePixelRatio || 1\r\n const fontSize = props.waterFontSize * devicePixelRatio\r\n const font = `${props.waterFontWeight} ${fontSize}px ${props.waterFontFamily}`\r\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D\r\n const waterGap = props.waterGap * devicePixelRatio // 统一缓存适配后的间距\r\n\r\n // 第一步:先设置字体,测量文字最大宽度\r\n ctx.font = font\r\n const { maxWidth } = getLongestTextByWidth(props.waterName, ctx)\r\n\r\n // 第二步:精准计算 Canvas 尺寸(平衡左右上下空间,无冗余浪费)\r\n // 核心:基于文字实际宽度+字体大小+间距,计算旋转后刚好容纳的画布尺寸\r\n const textTotalWidth = maxWidth // 文字原始最大宽度\r\n const textTotalHeight = Array.isArray(props.waterName)\r\n ? (props.waterName.length - 1) * (fontSize + 6 * devicePixelRatio) + fontSize\r\n : fontSize // 文字整体高度(多行文适配)\r\n // 画布基础尺寸:文字整体尺寸 + 双向间距(左右/上下各留一份gap,避免贴边)\r\n const baseCanvasSize =\r\n Math.max(100, Math.sqrt(textTotalWidth ** 2 + textTotalHeight ** 2)) + waterGap * 2\r\n canvas.width = baseCanvasSize\r\n canvas.height = baseCanvasSize\r\n\r\n // 第三步:重置上下文,配置样式(保持原有功能不变)\r\n ctx.clearRect(0, 0, canvas.width, canvas.height)\r\n ctx.font = font\r\n ctx.fillStyle = props.waterFontColor\r\n // @ts-ignore\r\n ctx.textAlign = props.waterTextAlign\r\n ctx.textBaseline = 'middle'\r\n ctx.shadowColor = props.shadowColor\r\n ctx.shadowOffsetX = props.shadowOffsetX\r\n ctx.shadowOffsetY = props.shadowOffsetY\r\n ctx.shadowBlur = props.shadowBlur\r\n\r\n // 第四步:核心修复——合理设置旋转中心+最小必要偏移(平衡左右上下)\r\n // 1. 旋转中心移至画布「左侧中间偏上」(而非中心),避免左右空间失衡\r\n // 2. 仅预留最小必要偏移(waterGap),既避免左上角截取,又不挤占右侧空间\r\n const safeOffset = waterGap // 安全偏移量(最小必要,兼顾上下左右)\r\n ctx.translate(safeOffset, canvas.height / 2) // 水平仅偏移安全间距,垂直居中\r\n\r\n // 旋转水印(保持原有配置,无过度偏移)\r\n ctx.rotate((Math.PI / 180) * -props.waterRotate)\r\n\r\n // 第五步:绘制文字(基于安全偏移,无过度靠右,保证右侧完整)\r\n if (Array.isArray(props.waterName)) {\r\n props.waterName.forEach((item, index) => {\r\n // 垂直间距:适配设备像素比,保持原有排版逻辑\r\n const verticalGap = fontSize + 6 * devicePixelRatio\r\n // 绘制起点:(0, index*verticalGap),无额外水平偏移,避免右侧溢出\r\n ctx.fillText(item, 0, index * verticalGap)\r\n })\r\n } else {\r\n // 单文本直接绘制,起点(0,0),基于已有的safeOffset保证左上角不截取\r\n ctx.fillText(props.waterName, 0, 0)\r\n }\r\n\r\n // 返回水印信息\r\n return {\r\n base64: canvas.toDataURL(),\r\n size: baseCanvasSize,\r\n styleSize: baseCanvasSize / devicePixelRatio,\r\n }\r\n })\r\n}\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","import watermark from './water-mark/index.vue';\r\n\r\nconst components = [ watermark ]\r\n\r\n// 注册组件\r\nconst install = function (Vue) {\r\n components.forEach((k) => {\r\n Vue.component(k.name, k)\r\n })\r\n}\r\n\r\nexport default install;"],"mappings":"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"}
1
+ {"version":3,"file":"watermark.umd.js","names":["watermark"],"sources":["../../src/packages/water-mark/useWatermarkBg.ts","../../src/packages/water-mark/index.vue","../../src/packages/water-mark/index.vue","../../src/packages/index.js"],"sourcesContent":["import { computed } from 'vue'\r\n\r\ntype Props = {\r\n waterName: string | string[]\r\n waterFontSize: number\r\n waterFontWeight: number\r\n waterGap: number\r\n waterRotate: number\r\n waterFontFamily: string\r\n waterTextAlign: string\r\n waterFontColor: string\r\n shadowColor: string\r\n shadowOffsetX: number\r\n shadowOffsetY: number\r\n shadowBlur: number\r\n}\r\n\r\nexport default function useWatermarkBg(props: Props) {\r\n // 获取数组中宽度最长的文本和对应的最大宽度\r\n function getLongestTextByWidth(textArr: string | string[], ctx: CanvasRenderingContext2D) {\r\n // 边界处理:空数组直接返回空结果\r\n if (!Array.isArray(textArr) || textArr.length === 0) {\r\n return {\r\n longestText: '',\r\n maxWidth: 0,\r\n }\r\n }\r\n\r\n // 字符串直接返回宽度\r\n if (typeof textArr === 'string') {\r\n return {\r\n longestText: textArr,\r\n maxWidth: ctx.measureText(textArr).width,\r\n }\r\n }\r\n\r\n // 初始化变量:记录最长文本和最大宽度\r\n let longestText = textArr[0]\r\n let maxWidth = ctx.measureText(longestText as string).width\r\n\r\n // 遍历数组,逐个测量并比较宽度\r\n for (let i = 1; i < textArr.length; i++) {\r\n const currentText = textArr[i]\r\n const currentWidth = ctx.measureText(currentText as string).width\r\n\r\n if (currentWidth > maxWidth) {\r\n maxWidth = currentWidth\r\n longestText = currentText\r\n }\r\n }\r\n\r\n return {\r\n longestText,\r\n maxWidth,\r\n }\r\n }\r\n\r\n return computed(() => {\r\n const canvas = document.createElement('canvas')\r\n const devicePixelRatio = window.devicePixelRatio || 1\r\n const fontSize = props.waterFontSize * devicePixelRatio\r\n const font = `${props.waterFontWeight} ${fontSize}px ${props.waterFontFamily}`\r\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D\r\n const waterGap = props.waterGap * devicePixelRatio // 统一缓存适配后的间距\r\n\r\n // 第一步:先设置字体,测量文字最大宽度\r\n ctx.font = font\r\n const { maxWidth } = getLongestTextByWidth(props.waterName, ctx)\r\n\r\n // 第二步:精准计算 Canvas 尺寸(平衡左右上下空间,无冗余浪费)\r\n // 核心:基于文字实际宽度+字体大小+间距,计算旋转后刚好容纳的画布尺寸\r\n const textTotalWidth = maxWidth // 文字原始最大宽度\r\n const textTotalHeight = Array.isArray(props.waterName)\r\n ? (props.waterName.length - 1) * (fontSize + 6 * devicePixelRatio) + fontSize\r\n : fontSize // 文字整体高度(多行文适配)\r\n // 画布基础尺寸:文字整体尺寸 + 双向间距(左右/上下各留一份gap,避免贴边)\r\n const baseCanvasSize =\r\n Math.max(100, Math.sqrt(textTotalWidth ** 2 + textTotalHeight ** 2)) + waterGap * 2\r\n canvas.width = baseCanvasSize\r\n canvas.height = baseCanvasSize\r\n\r\n // 第三步:重置上下文,配置样式(保持原有功能不变)\r\n ctx.clearRect(0, 0, canvas.width, canvas.height)\r\n ctx.font = font\r\n ctx.fillStyle = props.waterFontColor\r\n // @ts-ignore\r\n ctx.textAlign = props.waterTextAlign\r\n ctx.textBaseline = 'middle'\r\n ctx.shadowColor = props.shadowColor\r\n ctx.shadowOffsetX = props.shadowOffsetX\r\n ctx.shadowOffsetY = props.shadowOffsetY\r\n ctx.shadowBlur = props.shadowBlur\r\n\r\n // 第四步:核心修复——合理设置旋转中心+最小必要偏移(平衡左右上下)\r\n // 1. 旋转中心移至画布「左侧中间偏上」(而非中心),避免左右空间失衡\r\n // 2. 仅预留最小必要偏移(waterGap),既避免左上角截取,又不挤占右侧空间\r\n const safeOffset = waterGap // 安全偏移量(最小必要,兼顾上下左右)\r\n ctx.translate(safeOffset, canvas.height / 2) // 水平仅偏移安全间距,垂直居中\r\n\r\n // 旋转水印(保持原有配置,无过度偏移)\r\n ctx.rotate((Math.PI / 180) * -props.waterRotate)\r\n\r\n // 第五步:绘制文字(基于安全偏移,无过度靠右,保证右侧完整)\r\n if (Array.isArray(props.waterName)) {\r\n props.waterName.forEach((item, index) => {\r\n // 垂直间距:适配设备像素比,保持原有排版逻辑\r\n const verticalGap = fontSize + 6 * devicePixelRatio\r\n // 绘制起点:(0, index*verticalGap),无额外水平偏移,避免右侧溢出\r\n ctx.fillText(item, 0, index * verticalGap)\r\n })\r\n } else {\r\n // 单文本直接绘制,起点(0,0),基于已有的safeOffset保证左上角不截取\r\n ctx.fillText(props.waterName, 0, 0)\r\n }\r\n\r\n // 返回水印信息\r\n return {\r\n base64: canvas.toDataURL(),\r\n size: baseCanvasSize,\r\n styleSize: baseCanvasSize / devicePixelRatio,\r\n }\r\n })\r\n}\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","<template>\r\n <div class=\"watermark-container\" ref=\"watermarkContainer\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n<script setup lang=\"ts\">\r\nimport { onMounted, ref, watch } from 'vue'\r\nimport useWatermarkBg from './useWatermarkBg'\r\nconst props = defineProps({\r\n waterName: {\r\n // 传入水印的文本\r\n type: [String, Array] as unknown as () => string | string[],\r\n required: true,\r\n default: 'watermark',\r\n },\r\n waterFontSize: {\r\n // 字体的大小\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontWeight: {\r\n // 字体的粗细\r\n type: Number,\r\n default: 700,\r\n },\r\n waterGap: {\r\n // 水印重复的间隔\r\n type: Number,\r\n default: 100,\r\n },\r\n waterRotate: {\r\n // 水印倾斜\r\n type: Number,\r\n default: 20,\r\n },\r\n waterFontFamily: {\r\n // 水印字体\r\n type: String,\r\n default: 'Microsoft Yahei',\r\n },\r\n waterTextAlign: {\r\n // 水印对齐方式\r\n type: String,\r\n default: 'left',\r\n },\r\n waterFontColor: {\r\n // 水印字体颜色\r\n type: String,\r\n default: 'rgba(255, 255, 255, 1)', // rgba(255, 255, 255, 0.2)\r\n },\r\n shadowColor: {\r\n // 阴影颜色\r\n type: String,\r\n default: 'rgba(61, 61, 61, 0.25)',\r\n },\r\n shadowOffsetX: {\r\n // 阴影水平偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowOffsetY: {\r\n // 阴影垂直偏移\r\n type: Number,\r\n default: 1.5,\r\n },\r\n shadowBlur: {\r\n // 阴影模糊半径\r\n type: Number,\r\n default: 0,\r\n },\r\n})\r\n\r\nconst watermarkContainer = ref<HTMLElement>() // 挂载的dom节点\r\nconst div = ref<HTMLElement>() // 水印节点\r\nconst k = ref<MutationObserver>() // 监听器\r\nconst base64 = ref<string>() // base64\r\nconst styleSize = ref<number>() // styleSize\r\nconst bg = useWatermarkBg(props) // 水印参数\r\nbase64.value = bg.value.base64\r\nstyleSize.value = bg.value.styleSize\r\n\r\nwatch(\r\n props,\r\n (val) => {\r\n const bg = useWatermarkBg(val) // 水印参数\r\n base64.value = bg.value.base64\r\n styleSize.value = bg.value.styleSize\r\n // 删除w-water-mask-bg class\r\n document.querySelectorAll('.w-water-mask-bg').forEach((item) => {\r\n item.remove()\r\n })\r\n // 重新添加水印\r\n addDom()\r\n },\r\n {\r\n deep: true,\r\n },\r\n)\r\n\r\nonMounted(() => {\r\n addDom()\r\n\r\n // 添加dom监控器\r\n k.value = new MutationObserver((records) => {\r\n for (const record of records) {\r\n for (const dom of record.removedNodes) {\r\n if (dom === div.value) {\r\n div.value?.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n if (record.target === div.value) {\r\n div.value.remove()\r\n addDom()\r\n return\r\n }\r\n }\r\n })\r\n k.value?.observe(watermarkContainer.value as HTMLElement, {\r\n childList: true,\r\n attributes: true,\r\n subtree: true,\r\n })\r\n})\r\nconst addDom = () => {\r\n div.value = document.createElement('div')\r\n div.value.style.backgroundImage = `url(${base64.value})`\r\n // 添加个class\r\n div.value.classList.add('w-water-mask-bg')\r\n // @ts-ignore\r\n div.value.style.inset = 0\r\n // @ts-ignore\r\n div.value.style.zIndex = 9999\r\n div.value.style.position = 'absolute'\r\n div.value.style.backgroundRepeat = 'repeat'\r\n div.value.style.backgroundSize = `${styleSize.value}px ${styleSize.value}px`\r\n div.value.style.pointerEvents = 'none'\r\n watermarkContainer.value?.appendChild(div.value)\r\n}\r\n</script>\r\n","import watermark from './water-mark/index.vue'\r\n\r\nconst components = [watermark]\r\n\r\n// 注册组件\r\nconst install = function (Vue) {\r\n components.forEach((k) => {\r\n Vue.component('watermark', k)\r\n })\r\n}\r\n\r\nexport default install\r\n"],"mappings":"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"}