wui
组件

水印 Watermark

在内容区域上方添加重复的文字水印。

基础示例

Loading…

安装组件:

pnpm dlx wui@latest add @wui/watermark
如果项目中尚未提供共享的 cn 工具函数,请先安装其依赖
pnpm add clsx tailwind-merge
将源码复制到 components/ui/watermark.tsx
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> 的全部属性,并额外提供:

PropTypeDefaultDescription
contentstring | string[]"WUI"重复显示的一行或多行水印文字。
rotatenumber-22顺时针旋转角度(度)。
gap[number, number][80, 80]水印单元格之间的水平和垂直间距(像素)。
offset[number, number][0, 0]重复图案的水平和垂直偏移(像素)。
widthnumber160单个水印标记预留的宽度(像素)。
heightnumber64单个水印标记预留的高度(像素)。
fontWatermarkFont文字颜色、字体、字号、样式和字重选项。
opacitynumber0.15整个水印层的透明度。
zIndexnumber10水印层在隔离容器内的堆叠顺序。
classNamestring应用到根内容容器的额外类名。

WatermarkFont 接受 colorfontFamilyfontSizefontStylefontWeight。默认颜色跟随当前主题的前景色。

事件

根元素会透传原生 <div> 事件和属性。水印视觉层使用 pointer-events: none,因此不会成为事件目标。

PropTypeDefaultDescription
onClick(event: React.MouseEvent<HTMLDivElement>) => void点击根内容区域时触发。
onFocus / onBlur(event: React.FocusEvent<HTMLDivElement>) => void透传到根元素的原生焦点生命周期事件。

扩展使用

多行文字与自定义间距

Loading…

content 传入数组即可展示多行文字。较长标签需要更多空间时,可同时调整 widthheightgap,再微调 rotateopacity,确保受保护内容仍清晰可读。