cl-watermark.uvue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. await nextTick();
  153. // 获取容器尺寸
  154. await getContainerSize();
  155. if (containerWidth.value <= 0 || containerHeight.value <= 0) return;
  156. uni.createCanvasContextAsync({
  157. id: canvasId,
  158. component: proxy,
  159. success: (canvasContext: CanvasContext) => {
  160. const drawCtx = canvasContext.getContext("2d")!;
  161. // 设置canvas尺寸
  162. drawCtx.canvas.width = containerWidth.value;
  163. drawCtx.canvas.height = containerHeight.value;
  164. // 清空画布
  165. drawCtx.reset();
  166. drawCtx.clearRect(0, 0, containerWidth.value, containerHeight.value);
  167. // 缩放画布以适配高分屏
  168. const ratio = getDevicePixelRatio();
  169. drawCtx.scale(ratio, ratio);
  170. // 设置全局透明度
  171. drawCtx.globalAlpha = props.opacity;
  172. // 设置字体
  173. drawCtx.font = `${props.fontWeight} ${props.fontSize}px ${props.fontFamily}`;
  174. drawCtx.fillStyle = currentColor.value;
  175. drawCtx.textAlign = "center";
  176. drawCtx.textBaseline = "middle";
  177. // 计算水印单元的总宽高(包含间距)
  178. const cellWidth = props.width + props.gapX;
  179. const cellHeight = props.height + props.gapY;
  180. // 计算需要多少行和列
  181. const cols = Math.ceil(containerWidth.value / cellWidth) + 1;
  182. const rows = Math.ceil(containerHeight.value / cellHeight) + 1;
  183. // 遍历绘制水印
  184. for (let row = 0; row < rows; row++) {
  185. for (let col = 0; col < cols; col++) {
  186. const x = col * cellWidth + props.width / 2;
  187. const y = row * cellHeight + props.height / 2;
  188. drawCtx.save();
  189. drawCtx.translate(x, y);
  190. drawCtx.rotate((props.rotate * Math.PI) / 180);
  191. drawCtx.fillText(props.text, 0, 0);
  192. drawCtx.restore();
  193. }
  194. }
  195. }
  196. });
  197. }
  198. // 监听深色模式变化
  199. const stopWatchDark = watch(isDark, () => {
  200. drawWatermark();
  201. });
  202. // 监听属性变化
  203. const stopWatchProps = watch(
  204. computed(() => [
  205. props.text,
  206. props.fontSize,
  207. props.color,
  208. props.darkColor,
  209. props.opacity,
  210. props.rotate,
  211. props.width,
  212. props.height,
  213. props.gapX,
  214. props.gapY,
  215. props.fontWeight,
  216. props.fontFamily
  217. ]),
  218. () => {
  219. drawWatermark();
  220. }
  221. );
  222. onMounted(() => {
  223. drawWatermark();
  224. });
  225. onUnmounted(() => {
  226. stopWatchDark();
  227. stopWatchProps();
  228. });
  229. defineExpose({
  230. refresh: drawWatermark
  231. });
  232. </script>
  233. <style lang="scss" scoped>
  234. .cl-watermark {
  235. @apply relative w-full h-full;
  236. &__content {
  237. @apply relative w-full h-full;
  238. z-index: 1;
  239. }
  240. &__canvas {
  241. @apply absolute top-0 left-0 pointer-events-none;
  242. }
  243. }
  244. </style>