cl-watermark.uvue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <template>
  2. <view class="cl-watermark" :class="[pt.className]">
  3. <view class="cl-watermark__content" :class="[pt.container?.className]">
  4. <slot></slot>
  5. </view>
  6. <canvas
  7. ref="canvasRef"
  8. type="2d"
  9. :canvas-id="canvasId"
  10. :id="canvasId"
  11. class="cl-watermark__canvas"
  12. :style="{
  13. width: containerWidth + 'px',
  14. height: containerHeight + 'px',
  15. zIndex
  16. }"
  17. ></canvas>
  18. </view>
  19. </template>
  20. <script lang="ts" setup>
  21. import {
  22. ref,
  23. computed,
  24. onMounted,
  25. watch,
  26. getCurrentInstance,
  27. nextTick,
  28. shallowRef,
  29. onUnmounted
  30. } from "vue";
  31. import { getDevicePixelRatio, isDark, parsePt, uuid } from "@/.cool";
  32. import type { PassThroughProps } from "../../types";
  33. defineOptions({
  34. name: "cl-watermark"
  35. });
  36. const props = defineProps({
  37. // 透传样式
  38. pt: {
  39. type: Object,
  40. default: () => ({})
  41. },
  42. // 水印文本
  43. text: {
  44. type: String,
  45. default: "Watermark"
  46. },
  47. // 水印字体大小
  48. fontSize: {
  49. type: Number,
  50. default: 16
  51. },
  52. // 水印文字颜色(浅色模式)
  53. color: {
  54. type: String,
  55. default: "rgba(0, 0, 0, 0.15)"
  56. },
  57. // 水印文字颜色(深色模式)
  58. darkColor: {
  59. type: String,
  60. default: "rgba(255, 255, 255, 0.15)"
  61. },
  62. // 水印透明度
  63. opacity: {
  64. type: Number,
  65. default: 1
  66. },
  67. // 水印旋转角度
  68. rotate: {
  69. type: Number,
  70. default: -22
  71. },
  72. // 水印宽度
  73. width: {
  74. type: Number,
  75. default: 120
  76. },
  77. // 水印高度
  78. height: {
  79. type: Number,
  80. default: 64
  81. },
  82. // 水印之间的水平间距
  83. gapX: {
  84. type: Number,
  85. default: 100
  86. },
  87. // 水印之间的垂直间距
  88. gapY: {
  89. type: Number,
  90. default: 100
  91. },
  92. // 水印层级
  93. zIndex: {
  94. type: Number,
  95. default: 9
  96. },
  97. // 字体粗细
  98. fontWeight: {
  99. type: String,
  100. default: "normal"
  101. },
  102. // 字体样式
  103. fontFamily: {
  104. type: String,
  105. default: "sans-serif"
  106. }
  107. });
  108. // 透传样式类型定义
  109. type PassThrough = {
  110. className?: string;
  111. container?: PassThroughProps;
  112. };
  113. // 解析透传样式配置
  114. const pt = computed(() => parsePt<PassThrough>(props.pt));
  115. // 获取当前实例
  116. const { proxy } = getCurrentInstance()!;
  117. // 创建canvas实例
  118. const canvasRef = shallowRef<UniElement | null>(null);
  119. // 创建canvas ID
  120. const canvasId = `cl-watermark-${uuid()}`;
  121. // 容器高度
  122. const containerWidth = ref(0);
  123. // 容器宽度
  124. const containerHeight = ref(0);
  125. // 计算当前水印颜色
  126. const currentColor = computed(() => {
  127. if (isDark.value) {
  128. return props.darkColor;
  129. }
  130. return props.color;
  131. });
  132. /**
  133. * 获取容器尺寸
  134. */
  135. function getContainerSize(): Promise<void> {
  136. return new Promise((resolve) => {
  137. uni.createSelectorQuery()
  138. .in(proxy)
  139. .select(".cl-watermark")
  140. .boundingClientRect((rect) => {
  141. containerHeight.value = (rect as NodeInfo).height ?? 0;
  142. containerWidth.value = (rect as NodeInfo).width ?? 0;
  143. resolve();
  144. })
  145. .exec();
  146. });
  147. }
  148. /**
  149. * 绘制水印 - 使用Canvas
  150. */
  151. async function drawWatermark() {
  152. // 等待渲染完成
  153. await nextTick();
  154. // 获取容器尺寸
  155. await getContainerSize();
  156. // 等待渲染完成
  157. await nextTick();
  158. if (containerWidth.value <= 0 || containerHeight.value <= 0) return;
  159. uni.createCanvasContextAsync({
  160. id: canvasId,
  161. component: proxy,
  162. success: (canvasContext: CanvasContext) => {
  163. const drawCtx = canvasContext.getContext("2d")!;
  164. // 设置canvas尺寸
  165. drawCtx.canvas.width = containerWidth.value;
  166. drawCtx.canvas.height = containerHeight.value;
  167. // 清空画布
  168. // #ifdef APP
  169. drawCtx.reset();
  170. // #endif
  171. drawCtx.clearRect(0, 0, containerWidth.value, containerHeight.value);
  172. // 缩放画布以适配高分屏
  173. // #ifdef APP
  174. const ratio = getDevicePixelRatio();
  175. drawCtx.scale(ratio, ratio);
  176. // #endif
  177. // 设置全局透明度
  178. drawCtx.globalAlpha = props.opacity;
  179. // 设置字体
  180. drawCtx.font = `${props.fontWeight} ${props.fontSize}px ${props.fontFamily}`;
  181. drawCtx.fillStyle = currentColor.value;
  182. drawCtx.textAlign = "center";
  183. drawCtx.textBaseline = "middle";
  184. // 计算水印单元的总宽高(包含间距)
  185. const cellWidth = props.width + props.gapX;
  186. const cellHeight = props.height + props.gapY;
  187. // 计算需要多少行和列
  188. const cols = Math.ceil(containerWidth.value / cellWidth) + 1;
  189. const rows = Math.ceil(containerHeight.value / cellHeight) + 1;
  190. // 遍历绘制水印
  191. for (let row = 0; row < rows; row++) {
  192. for (let col = 0; col < cols; col++) {
  193. const x = col * cellWidth + props.width / 2;
  194. const y = row * cellHeight + props.height / 2;
  195. drawCtx.save();
  196. drawCtx.translate(x, y);
  197. drawCtx.rotate((props.rotate * Math.PI) / 180);
  198. drawCtx.fillText(props.text, 0, 0);
  199. drawCtx.restore();
  200. }
  201. }
  202. }
  203. });
  204. }
  205. // 监听深色模式变化
  206. const stopWatchDark = watch(isDark, () => {
  207. drawWatermark();
  208. });
  209. // 监听属性变化
  210. const stopWatchProps = watch(
  211. computed(() => [
  212. props.text,
  213. props.fontSize,
  214. props.color,
  215. props.darkColor,
  216. props.opacity,
  217. props.rotate,
  218. props.width,
  219. props.height,
  220. props.gapX,
  221. props.gapY,
  222. props.fontWeight,
  223. props.fontFamily
  224. ]),
  225. () => {
  226. drawWatermark();
  227. }
  228. );
  229. onMounted(() => {
  230. drawWatermark();
  231. });
  232. onUnmounted(() => {
  233. stopWatchDark();
  234. stopWatchProps();
  235. });
  236. defineExpose({
  237. refresh: drawWatermark
  238. });
  239. </script>
  240. <style lang="scss" scoped>
  241. .cl-watermark {
  242. @apply relative w-full h-full;
  243. &__content {
  244. @apply relative w-full h-full;
  245. z-index: 1;
  246. }
  247. &__canvas {
  248. @apply absolute top-0 left-0 pointer-events-none;
  249. }
  250. }
  251. </style>