添加 cl-watermark 水印组件

This commit is contained in:
icssoa
2025-10-22 12:35:37 +08:00
parent 4634962271
commit 7acd449496
8 changed files with 621 additions and 0 deletions

View File

@@ -323,6 +323,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"
]
]

View File

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

View File

@@ -448,6 +448,12 @@
"navigationBarTitleText": "Sign 签名"
}
},
{
"path": "other/watermark",
"style": {
"navigationBarTitleText": "Watermark 水印"
}
},
{
"path": "other/day-uts",
"style": {

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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;
};

View File

@@ -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>;
}
}