瀏覽代碼

添加 cl-watermark 水印组件

icssoa 5 月之前
父節點
當前提交
7acd449496

+ 80 - 0
locale/en.json

@@ -324,6 +324,10 @@
     "Signature"
   ],
   [
+    "水印",
+    "Watermark"
+  ],
+  [
     "图片裁剪",
     "Image Cropping"
   ],
@@ -2498,5 +2502,81 @@
   [
     "Calendar 日历",
     "Calendar Calendar"
+  ],
+  [
+    "这是一段需要保护的内容",
+    "This is content that needs to be protected"
+  ],
+  [
+    "水印会覆盖在内容上方,防止内容被盗用",
+    "Watermarks will be overlaid on the content to prevent unauthorized use"
+  ],
+  [
+    "可自定义的水印内容区域",
+    "Customizable watermark content area"
+  ],
+  [
+    "水印文本",
+    "Watermark Text"
+  ],
+  [
+    "字体大小",
+    "Font Size"
+  ],
+  [
+    "透明度",
+    "Opacity"
+  ],
+  [
+    "旋转角度",
+    "Rotation Angle"
+  ],
+  [
+    "水印宽度",
+    "Watermark Width"
+  ],
+  [
+    "水印高度",
+    "Watermark Height"
+  ],
+  [
+    "水平间距",
+    "Horizontal Spacing"
+  ],
+  [
+    "垂直间距",
+    "Vertical Spacing"
+  ],
+  [
+    "字体粗细",
+    "Font Weight"
+  ],
+  [
+    "正常",
+    "Normal"
+  ],
+  [
+    "加粗",
+    "Bold"
+  ],
+  [
+    "多行文本水印",
+    "Multi-line Text Watermark"
+  ],
+  [
+    "重要文档",
+    "Important Document"
+  ],
+  [
+    "这是一份重要的文档内容,需要添加水印保护。",
+    "This is an important document that requires watermark protection."
+  ],
+  [
+    "水印可以防止内容被未授权的复制和传播。",
+    "Watermarks can prevent unauthorized copying and distribution of content."
+  ],
+  [
+    "图片保护",
+    "Image Protection"
   ]
 ]

+ 80 - 0
locale/zh-cn.json

@@ -724,6 +724,10 @@
     ""
   ],
   [
+    "水印",
+    ""
+  ],
+  [
     "图片裁剪",
     ""
   ],
@@ -2498,5 +2502,81 @@
   [
     "我的",
     ""
+  ],
+  [
+    "这是一段需要保护的内容",
+    ""
+  ],
+  [
+    "水印会覆盖在内容上方,防止内容被盗用",
+    ""
+  ],
+  [
+    "可自定义的水印内容区域",
+    ""
+  ],
+  [
+    "水印文本",
+    ""
+  ],
+  [
+    "字体大小",
+    ""
+  ],
+  [
+    "透明度",
+    ""
+  ],
+  [
+    "旋转角度",
+    ""
+  ],
+  [
+    "水印宽度",
+    ""
+  ],
+  [
+    "水印高度",
+    ""
+  ],
+  [
+    "水平间距",
+    ""
+  ],
+  [
+    "垂直间距",
+    ""
+  ],
+  [
+    "字体粗细",
+    ""
+  ],
+  [
+    "正常",
+    ""
+  ],
+  [
+    "加粗",
+    ""
+  ],
+  [
+    "多行文本水印",
+    ""
+  ],
+  [
+    "重要文档",
+    ""
+  ],
+  [
+    "这是一份重要的文档内容,需要添加水印保护。",
+    ""
+  ],
+  [
+    "水印可以防止内容被未授权的复制和传播。",
+    ""
+  ],
+  [
+    "图片保护",
+    ""
   ]
 ]

+ 6 - 0
pages.json

@@ -449,6 +449,12 @@
 					}
 				},
 				{
+					"path": "other/watermark",
+					"style": {
+						"navigationBarTitleText": "Watermark 水印"
+					}
+				},
+				{
 					"path": "other/day-uts",
 					"style": {
 						"navigationBarTitleText": "DayUts 日期"

+ 147 - 0
pages/demo/other/watermark.uvue

@@ -0,0 +1,147 @@
+<template>
+	<cl-page>
+		<view class="p-3">
+			<demo-item :label="t('自定义样式')">
+				<view class="flex">
+					<cl-watermark
+						:text="customText"
+						:font-size="fontSize"
+						:color="color"
+						:dark-color="darkColor"
+						:opacity="opacity"
+						:rotate="rotate"
+						:width="width"
+						:height="height"
+						:gap-x="gapX"
+						:gap-y="gapY"
+						:font-weight="fontWeight"
+					>
+						<view
+							class="flex flex-col p-4 rounded-xl bg-surface-50 dark:bg-surface-800 h-[400rpx]"
+						>
+							<cl-text>
+								明月几时有?把酒问青天。 不知天上宫阙,今夕是何年。
+								我欲乘风归去,又恐琼楼玉宇,高处不胜寒。 起舞弄清影,何似在人间。
+							</cl-text>
+						</view>
+					</cl-watermark>
+				</view>
+
+				<cl-list border class="mt-3">
+					<cl-list-item :label="t('水印文本')">
+						<cl-input
+							v-model="customText"
+							placeholder="请输入水印文本"
+							:pt="{ className: 'w-[300rpx]' }"
+						/>
+					</cl-list-item>
+
+					<cl-list-item :label="t('字体大小')">
+						<view class="w-[300rpx]">
+							<cl-slider v-model="fontSize" :min="12" :max="32" :step="1"></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<!-- #ifndef APP -->
+					<cl-list-item :label="t('透明度')">
+						<view class="w-[300rpx]">
+							<cl-slider
+								v-model="opacity"
+								:min="0.1"
+								:max="1"
+								:step="0.05"
+							></cl-slider>
+						</view>
+					</cl-list-item>
+					<!-- #endif -->
+
+					<cl-list-item :label="t('旋转角度')">
+						<view class="w-[300rpx]">
+							<cl-slider
+								v-model="rotate"
+								:min="-180"
+								:max="180"
+								:step="5"
+							></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<cl-list-item :label="t('水印宽度')">
+						<view class="w-[300rpx]">
+							<cl-slider v-model="width" :min="80" :max="300" :step="10"></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<cl-list-item :label="t('水印高度')">
+						<view class="w-[300rpx]">
+							<cl-slider v-model="height" :min="40" :max="200" :step="10"></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<cl-list-item :label="t('水平间距')">
+						<view class="w-[300rpx]">
+							<cl-slider v-model="gapX" :min="20" :max="200" :step="10"></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<cl-list-item :label="t('垂直间距')">
+						<view class="w-[300rpx]">
+							<cl-slider v-model="gapY" :min="20" :max="200" :step="10"></cl-slider>
+						</view>
+					</cl-list-item>
+
+					<cl-list-item :label="t('字体粗细')">
+						<cl-tabs
+							v-model="fontWeight"
+							:list="fontWeightList"
+							:height="60"
+							show-slider
+						></cl-tabs>
+					</cl-list-item>
+				</cl-list>
+			</demo-item>
+
+			<demo-item :label="t('图片保护')">
+				<view class="flex">
+					<cl-watermark text="© Cool UI" :width="200" :height="80" :opacity="0.9">
+						<image
+							src="https://unix.cool-js.com/images/demo/avatar.jpg"
+							mode="aspectFit"
+							class="w-full"
+						></image>
+					</cl-watermark>
+				</view>
+			</demo-item>
+		</view>
+	</cl-page>
+</template>
+
+<script lang="ts" setup>
+import DemoItem from "../components/item.uvue";
+import { ref } from "vue";
+import { t } from "@/locale";
+import type { ClTabsItem } from "@/uni_modules/cool-ui";
+
+const customText = ref("Cool UI");
+const fontSize = ref(16);
+const color = ref("rgba(0, 0, 0, 0.15)");
+const darkColor = ref("rgba(255, 255, 255, 0.15)");
+const opacity = ref(1);
+const rotate = ref(-22);
+const width = ref(120);
+const height = ref(64);
+const gapX = ref(100);
+const gapY = ref(100);
+const fontWeight = ref("normal");
+
+const fontWeightList = [
+	{
+		label: t("正常"),
+		value: "normal"
+	},
+	{
+		label: t("加粗"),
+		value: "bold"
+	}
+] as ClTabsItem[];
+</script>

+ 5 - 0
pages/index/home.uvue

@@ -422,6 +422,11 @@ const data = computed<Item[]>(() => {
 					path: "/pages/demo/other/sign"
 				},
 				{
+					label: t("水印"),
+					icon: "copyright-line",
+					path: "/pages/demo/other/watermark"
+				},
+				{
 					label: t("图片裁剪"),
 					icon: "crop-line",
 					path: "/pages/demo/other/cropper"

+ 277 - 0
uni_modules/cool-ui/components/cl-watermark/cl-watermark.uvue

@@ -0,0 +1,277 @@
+<template>
+	<view class="cl-watermark" :class="[pt.className]">
+		<view class="cl-watermark__content" :class="[pt.container?.className]">
+			<slot></slot>
+		</view>
+
+		<canvas
+			ref="canvasRef"
+			type="2d"
+			:canvas-id="canvasId"
+			:id="canvasId"
+			class="cl-watermark__canvas"
+			:style="{
+				width: containerWidth + 'px',
+				height: containerHeight + 'px',
+				zIndex
+			}"
+		></canvas>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import {
+	ref,
+	computed,
+	onMounted,
+	watch,
+	getCurrentInstance,
+	nextTick,
+	shallowRef,
+	onUnmounted
+} from "vue";
+import { getDevicePixelRatio, isDark, parsePt, uuid } from "@/cool";
+import type { PassThroughProps } from "../../types";
+
+defineOptions({
+	name: "cl-watermark"
+});
+
+const props = defineProps({
+	// 透传样式
+	pt: {
+		type: Object,
+		default: () => ({})
+	},
+	// 水印文本
+	text: {
+		type: String,
+		default: "Watermark"
+	},
+	// 水印字体大小
+	fontSize: {
+		type: Number,
+		default: 16
+	},
+	// 水印文字颜色(浅色模式)
+	color: {
+		type: String,
+		default: "rgba(0, 0, 0, 0.15)"
+	},
+	// 水印文字颜色(深色模式)
+	darkColor: {
+		type: String,
+		default: "rgba(255, 255, 255, 0.15)"
+	},
+	// 水印透明度
+	opacity: {
+		type: Number,
+		default: 1
+	},
+	// 水印旋转角度
+	rotate: {
+		type: Number,
+		default: -22
+	},
+	// 水印宽度
+	width: {
+		type: Number,
+		default: 120
+	},
+	// 水印高度
+	height: {
+		type: Number,
+		default: 64
+	},
+	// 水印之间的水平间距
+	gapX: {
+		type: Number,
+		default: 100
+	},
+	// 水印之间的垂直间距
+	gapY: {
+		type: Number,
+		default: 100
+	},
+	// 水印层级
+	zIndex: {
+		type: Number,
+		default: 9
+	},
+	// 字体粗细
+	fontWeight: {
+		type: String,
+		default: "normal"
+	},
+	// 字体样式
+	fontFamily: {
+		type: String,
+		default: "sans-serif"
+	}
+});
+
+// 透传样式类型定义
+type PassThrough = {
+	className?: string;
+	container?: PassThroughProps;
+};
+
+// 解析透传样式配置
+const pt = computed(() => parsePt<PassThrough>(props.pt));
+
+// 获取当前实例
+const { proxy } = getCurrentInstance()!;
+
+// 创建canvas实例
+const canvasRef = shallowRef<UniElement | null>(null);
+// 创建canvas ID
+const canvasId = `cl-watermark-${uuid()}`;
+
+// 容器高度
+const containerWidth = ref(0);
+
+// 容器宽度
+const containerHeight = ref(0);
+
+// 计算当前水印颜色
+const currentColor = computed(() => {
+	if (isDark.value) {
+		return props.darkColor;
+	}
+	return props.color;
+});
+
+/**
+ * 获取容器尺寸
+ */
+function getContainerSize(): Promise<void> {
+	return new Promise((resolve) => {
+		uni.createSelectorQuery()
+			.in(proxy)
+			.select(".cl-watermark")
+			.boundingClientRect((rect) => {
+				containerHeight.value = (rect as NodeInfo).height ?? 0;
+				containerWidth.value = (rect as NodeInfo).width ?? 0;
+
+				resolve();
+			})
+			.exec();
+	});
+}
+
+/**
+ * 绘制水印 - 使用Canvas
+ */
+async function drawWatermark() {
+	await nextTick();
+
+	// 获取容器尺寸
+	await getContainerSize();
+	if (containerWidth.value <= 0 || containerHeight.value <= 0) return;
+
+	uni.createCanvasContextAsync({
+		id: canvasId,
+		component: proxy,
+		success: (canvasContext: CanvasContext) => {
+			const drawCtx = canvasContext.getContext("2d")!;
+
+			// 设置canvas尺寸
+			drawCtx.canvas.width = containerWidth.value;
+			drawCtx.canvas.height = containerHeight.value;
+
+			// 清空画布
+			drawCtx.reset();
+			drawCtx.clearRect(0, 0, containerWidth.value, containerHeight.value);
+
+			// 缩放画布以适配高分屏
+			const ratio = getDevicePixelRatio();
+			drawCtx.scale(ratio, ratio);
+
+			// 设置全局透明度
+			drawCtx.globalAlpha = props.opacity;
+
+			// 设置字体
+			drawCtx.font = `${props.fontWeight} ${props.fontSize}px ${props.fontFamily}`;
+			drawCtx.fillStyle = currentColor.value;
+			drawCtx.textAlign = "center";
+			drawCtx.textBaseline = "middle";
+
+			// 计算水印单元的总宽高(包含间距)
+			const cellWidth = props.width + props.gapX;
+			const cellHeight = props.height + props.gapY;
+
+			// 计算需要多少行和列
+			const cols = Math.ceil(containerWidth.value / cellWidth) + 1;
+			const rows = Math.ceil(containerHeight.value / cellHeight) + 1;
+
+			// 遍历绘制水印
+			for (let row = 0; row < rows; row++) {
+				for (let col = 0; col < cols; col++) {
+					const x = col * cellWidth + props.width / 2;
+					const y = row * cellHeight + props.height / 2;
+
+					drawCtx.save();
+					drawCtx.translate(x, y);
+					drawCtx.rotate((props.rotate * Math.PI) / 180);
+					drawCtx.fillText(props.text, 0, 0);
+					drawCtx.restore();
+				}
+			}
+		}
+	});
+}
+
+// 监听深色模式变化
+const stopWatchDark = watch(isDark, () => {
+	drawWatermark();
+});
+
+// 监听属性变化
+const stopWatchProps = watch(
+	computed(() => [
+		props.text,
+		props.fontSize,
+		props.color,
+		props.darkColor,
+		props.opacity,
+		props.rotate,
+		props.width,
+		props.height,
+		props.gapX,
+		props.gapY,
+		props.fontWeight,
+		props.fontFamily
+	]),
+	() => {
+		drawWatermark();
+	}
+);
+
+onMounted(() => {
+	drawWatermark();
+});
+
+onUnmounted(() => {
+	stopWatchDark();
+	stopWatchProps();
+});
+
+defineExpose({
+	refresh: drawWatermark
+});
+</script>
+
+<style lang="scss" scoped>
+.cl-watermark {
+	@apply relative w-full h-full;
+
+	&__content {
+		@apply relative w-full h-full;
+		z-index: 1;
+	}
+
+	&__canvas {
+		@apply absolute top-0 left-0 pointer-events-none;
+	}
+}
+</style>

+ 24 - 0
uni_modules/cool-ui/components/cl-watermark/props.ts

@@ -0,0 +1,24 @@
+import type { PassThroughProps } from "../../types";
+
+export type ClWatermarkPassThrough = {
+	className?: string;
+	container?: PassThroughProps;
+};
+
+export type ClWatermarkProps = {
+	className?: string;
+	pt?: ClWatermarkPassThrough;
+	text?: string;
+	fontSize?: number;
+	color?: string;
+	darkColor?: string;
+	opacity?: number;
+	rotate?: number;
+	width?: number;
+	height?: number;
+	gapX?: number;
+	gapY?: number;
+	zIndex?: number;
+	fontWeight?: string;
+	fontFamily?: string;
+};

+ 2 - 0
uni_modules/cool-ui/index.d.ts

@@ -76,6 +76,7 @@ import type { ClTreeProps, ClTreePassThrough } from "./components/cl-tree/props"
 import type { ClTreeItemProps, ClTreeItemPassThrough } from "./components/cl-tree-item/props";
 import type { ClUploadProps, ClUploadPassThrough } from "./components/cl-upload/props";
 import type { ClWaterfallProps, ClWaterfallPassThrough } from "./components/cl-waterfall/props";
+import type { ClWatermarkProps, ClWatermarkPassThrough } from "./components/cl-watermark/props";
 
 export {};
 
@@ -156,5 +157,6 @@ declare module "vue" {
 		"cl-tree-item": (typeof import('./components/cl-tree-item/cl-tree-item.uvue')['default']) & import('vue').DefineComponent<ClTreeItemProps>;
 		"cl-upload": (typeof import('./components/cl-upload/cl-upload.uvue')['default']) & import('vue').DefineComponent<ClUploadProps>;
 		"cl-waterfall": (typeof import('./components/cl-waterfall/cl-waterfall.uvue')['default']) & import('vue').DefineComponent<ClWaterfallProps>;
+		"cl-watermark": (typeof import('./components/cl-watermark/cl-watermark.uvue')['default']) & import('vue').DefineComponent<ClWatermarkProps>;
 	}
 }