组件
水印 Watermark
在内容区域上方添加重复的文字水印。
基础示例
Loading…
安装组件:
pnpm dlx wui@latest add @wui/watermark如果项目中尚未提供共享的
cn 工具函数,请先安装其依赖pnpm add clsx tailwind-merge将源码复制到
components/ui/watermark.tsx"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface WatermarkFont {
/** Watermark text color. Inherits the current foreground color by default. */
color?: string
/** CSS font family used by the watermark text. */
fontFamily?: string
/** Font size in pixels. @default 16 */
fontSize?: number
/** CSS font style. @default "normal" */
fontStyle?: React.CSSProperties["fontStyle"]
/** CSS font weight. @default 500 */
fontWeight?: React.CSSProperties["fontWeight"]
}
interface WatermarkProps extends Omit<React.ComponentProps<"div">, "content"> {
/** One line or multiple lines of repeated watermark text. @default "WUI" */
content?: string | string[]
/** Clockwise rotation angle in degrees. @default -22 */
rotate?: number
/** Horizontal and vertical space between watermark cells. @default [80, 80] */
gap?: [number, number]
/** Horizontal and vertical offset of the repeating pattern. @default [0, 0] */
offset?: [number, number]
/** Width of one watermark mark in pixels. @default 160 */
width?: number
/** Height of one watermark mark in pixels. @default 64 */
height?: number
/** Text typography options. */
font?: WatermarkFont
/** Opacity of the watermark layer. @default 0.15 */
opacity?: number
/** Stacking order of the watermark layer. @default 10 */
zIndex?: number
}
function Watermark({
children,
className,
content = "WUI",
rotate = -22,
gap = [80, 80],
offset = [0, 0],
width = 160,
height = 64,
font,
opacity = 0.15,
zIndex = 10,
...props
}: WatermarkProps) {
const patternId = `watermark-${React.useId().replace(/:/g, "")}`
const lines = Array.isArray(content) ? content : [content]
const fontSize = font?.fontSize ?? 16
const lineHeight = fontSize * 1.4
const patternWidth = width + gap[0]
const patternHeight = height + gap[1]
const centerX = patternWidth / 2
const centerY = patternHeight / 2
const firstLineY = centerY - ((lines.length - 1) * lineHeight) / 2
return (
<div
data-slot="watermark"
className={cn("relative isolate", className)}
{...props}
>
{children}
<svg
data-slot="watermark-layer"
aria-hidden="true"
className="text-foreground pointer-events-none absolute inset-0 size-full select-none"
style={{ color: font?.color, opacity, zIndex }}
>
<defs>
<pattern
id={patternId}
width={patternWidth}
height={patternHeight}
x={offset[0]}
y={offset[1]}
patternUnits="userSpaceOnUse"
>
<g transform={`rotate(${rotate} ${centerX} ${centerY})`}>
{lines.map((line, index) => (
<text
key={`${line}-${index}`}
data-slot="watermark-text"
x={centerX}
y={firstLineY + index * lineHeight}
fill="currentColor"
textAnchor="middle"
dominantBaseline="middle"
style={{
fontFamily: font?.fontFamily,
fontSize,
fontStyle: font?.fontStyle ?? "normal",
fontWeight: font?.fontWeight ?? 500,
}}
>
{line}
</text>
))}
</g>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${patternId})`} />
</svg>
</div>
)
}
export { Watermark, type WatermarkFont, type WatermarkProps }
组件作用
Watermark 在内容区域上叠加重复文字,用于表达所有权、保密级别或文档状态。水印层会忽略
指针事件,因此其下方的控件和可选择文本仍可正常使用。
- 适合用于报告、预览、导出文档页面和受限内容。
- 需要同时展示查看者身份或时间戳时,可以传入多行文本。
- 水印只是一种视觉提示,不能代替权限控制。
特意不使用动画
Watermark 默认不包含任何动画。它是一个被动的环境层,应该安静地位于内容之上;动画既会
抢夺注意力、影响受保护内容的可读性,也会导致大面积平铺区域反复重绘。并非每个组件都需要
动效,取舍原则见 docs/COMPONENT-SPEC.md 第 9 节。
组件属性
Watermark 继承原生 <div> 的全部属性,并额外提供:
| Prop | Type | Default | Description |
|---|---|---|---|
| content | string | string[] | "WUI" | 重复显示的一行或多行水印文字。 |
| rotate | number | -22 | 顺时针旋转角度(度)。 |
| gap | [number, number] | [80, 80] | 水印单元格之间的水平和垂直间距(像素)。 |
| offset | [number, number] | [0, 0] | 重复图案的水平和垂直偏移(像素)。 |
| width | number | 160 | 单个水印标记预留的宽度(像素)。 |
| height | number | 64 | 单个水印标记预留的高度(像素)。 |
| font | WatermarkFont | — | 文字颜色、字体、字号、样式和字重选项。 |
| opacity | number | 0.15 | 整个水印层的透明度。 |
| zIndex | number | 10 | 水印层在隔离容器内的堆叠顺序。 |
| className | string | — | 应用到根内容容器的额外类名。 |
WatermarkFont 接受 color、fontFamily、fontSize、fontStyle 和
fontWeight。默认颜色跟随当前主题的前景色。
事件
根元素会透传原生 <div> 事件和属性。水印视觉层使用 pointer-events: none,因此不会成为事件目标。
| Prop | Type | Default | Description |
|---|---|---|---|
| onClick | (event: React.MouseEvent<HTMLDivElement>) => void | — | 点击根内容区域时触发。 |
| onFocus / onBlur | (event: React.FocusEvent<HTMLDivElement>) => void | — | 透传到根元素的原生焦点生命周期事件。 |
扩展使用
多行文字与自定义间距
Loading…
向 content 传入数组即可展示多行文字。较长标签需要更多空间时,可同时调整 width、
height 和 gap,再微调 rotate 与 opacity,确保受保护内容仍清晰可读。