wui
组件

水印 Watermark

在页面、卡片或敏感数据区域平铺防泄密版权文本或自定义图章的无干扰背景水印组件。

基础用法

为经营简报添加平铺倾斜的“内部资料”水印。水印图层不拦截指针事件,正文仍可正常选择与点击:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/watermark
安装基础依赖
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[]
  /** Image URL used as the repeated watermark. Takes precedence over content. */
  image?: 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,
  image,
  rotate = -22,
  gap = [80, 80],
  offset = [0, 0],
  width = 160,
  height = 64,
  font,
  opacity = 0.15,
  zIndex = 10,
  ...props
}: WatermarkProps) {
  // useId() may contain characters such as ":" or "«»" that are awkward
  // inside url(#id) references, so keep only safe identifier characters.
  const patternId = `watermark-${React.useId().replace(/[^\w-]/g, "")}`
  const resolvedContent = content ?? "WUI"
  const lines = Array.isArray(resolvedContent)
    ? resolvedContent
    : [resolvedContent]
  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})`}>
              {image ? (
                <image
                  data-slot="watermark-image"
                  href={image}
                  x={centerX - width / 2}
                  y={centerY - height / 2}
                  width={width}
                  height={height}
                  preserveAspectRatio="xMidYMid meet"
                />
              ) : (
                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 }

属性 Props

属性类型默认值说明
childrenReact.ReactNode—受水印保护的页面或卡片子内容。
contentstring | string[]"WUI"水印展示的单行文字或多行文字数组(多行会自动计算垂直居中)。
imagestring—水印图片的 URL 路径(传入图片时将优先于文字渲染)。
rotatenumber-22水印旋转角度(以顺时针度数为单位)。
gap[number, number][80, 80]相邻水印单元之间的水平与垂直间距 [gapX, gapY](像素)。
offset[number, number][0, 0]水印图案在画布上的初始 [offsetX, offsetY] 偏移(像素)。
widthnumber160单个水印单元的绘制宽度(像素)。
heightnumber64单个水印单元的绘制高度(像素)。
fontWatermarkFont—文字排版配置对象:包含 `color`(默认继承 foreground,暗色模式自动适配)、`fontSize`(默认 16)、`fontWeight`、`fontFamily`、`fontStyle`。
opacitynumber0.15水印图层的不透明度(0 到 1 之间)。
zIndexnumber10水印图层的层叠顺序(z-index)。
classNamestring—应用于外层容器的 CSS 类名。

事件 Events

该组件为纯展示型安全与版权图层,不阻挡交互且不派发专用事件。

使用场景与设计规范

Watermark 适用于敏感后台报表、企业内部保密文档、证件与发票预览、图片防盗:

  • 轻量且随尺寸自适应:基于 SVG <pattern> 原生能力构建,无需 Canvas 手动计算重绘,容器尺寸变化时图案自动铺满,缩放无失真。
  • 不是安全边界:水印仅用于标识与追溯,用户仍可通过开发者工具移除;敏感数据的访问控制应在服务端完成。
  • 单元尺寸:文字不会自动换行,较长的文案请同步增大 width 与 gap,避免旋转后被图案单元裁切。
  • 不影响日常操作:始终配置 pointer-events-none 与 user-select: none,用户可正常选中底层数据文本或点击表格行。

场景示例

参数调试

实时调整文字、旋转角度与不透明度,观察水印的平铺效果:

Loading…

多行自定义内容

展示查看人姓名、工号与时间戳的多行水印,便于截图外传时追溯来源:

Loading…

自定义图章图片

使用企业品牌 Logo 作为平铺水印,覆盖在开票记录等业务表格上:

Loading…

无障碍与交互 Accessibility

  • 交互零阻挡:鼠标点击、滚动、文本拖拽选择均直接穿透到子内容。
  • 无障碍树忽略:水印 SVG 默认标记 aria-hidden="true",屏幕阅读器不受多余重复文本的干扰。