iter(g54): 新增 watermark 水印组件
This commit is contained in:
@@ -52,3 +52,4 @@ export { datePicker } from './datePicker.js';
|
||||
export { backTop } from './backTop.js';
|
||||
export { colorPicker } from './colorPicker.js';
|
||||
export { tour } from './tour.js';
|
||||
export { watermark } from './watermark.js';
|
||||
|
||||
@@ -590,4 +590,14 @@ export interface TourHandle {
|
||||
close: () => void; // 立即关闭
|
||||
}
|
||||
|
||||
/*** 水印入参 ***/
|
||||
export interface WatermarkProps {
|
||||
text: string; // 水印文字,支持 \n 多行
|
||||
opacity?: number; // 透明度;缺省 0.12
|
||||
rotate?: number; // 旋转角度(度);缺省 -20
|
||||
gap?: number; // 平铺间距像素;缺省 140
|
||||
fontSize?: number; // 字号像素;缺省 14
|
||||
color?: string; // 颜色(CSS 串或令牌名);缺省取 --ua-color-text-muted 的实色
|
||||
}
|
||||
|
||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { h } from '@ui-arch/frame';
|
||||
import type { WatermarkProps } from './types.js';
|
||||
|
||||
/*** 取令牌实色:canvas 不认 var(--ua-*),先解析成计算值,解析不到再退回中性灰 ***/
|
||||
function tokenColor(expr: string): string {
|
||||
const name = /^var\((--[\w-]+)\)$/.exec(expr.trim())?.[1]; // 形如 var(--ua-color-text-muted)
|
||||
if (!name) return expr; // 已经是实际色值,直接用
|
||||
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return v || 'rgb(128 128 128)'; // 兜底:中性灰,避免透明水印
|
||||
}
|
||||
|
||||
/*** 生成平铺单元:一块 gap 见方的画布,居中画一行旋转文字,靠 background-repeat 铺满 ***/
|
||||
function tile(props: WatermarkProps): string {
|
||||
const gap = props.gap ?? 140; // 平铺单元尺寸
|
||||
const size = props.fontSize ?? 14; // 字号
|
||||
const ratio = window.devicePixelRatio || 1; // 高分屏按 DPR 放大,避免糊
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = gap * ratio;
|
||||
cv.height = gap * ratio;
|
||||
const ctx = cv.getContext('2d');
|
||||
if (!ctx) return ''; // 卫语句:环境不支持 canvas 时不出图
|
||||
ctx.scale(ratio, ratio);
|
||||
ctx.translate(gap / 2, gap / 2);
|
||||
ctx.rotate(((props.rotate ?? -20) * Math.PI) / 180);
|
||||
ctx.font = `${size}px system-ui, sans-serif`;
|
||||
ctx.fillStyle = tokenColor(props.color ?? 'var(--ua-color-text-muted)');
|
||||
ctx.globalAlpha = props.opacity ?? 0.12;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
props.text.split('\n').forEach((line, i, arr) => {
|
||||
ctx.fillText(line, 0, (i - (arr.length - 1) / 2) * (size + 4)); // 多行按行距垂直居中
|
||||
});
|
||||
return cv.toDataURL('image/png');
|
||||
}
|
||||
|
||||
/*** 水印:内容容器 + 覆盖层,覆盖层用 repeat 背景平铺,pointer-events:none 不挡交互 ***/
|
||||
export function watermark(props: WatermarkProps, ...children: unknown[]): HTMLElement {
|
||||
const img = tile(props);
|
||||
const layer = h('div', { class: 'ua-watermark-layer', 'aria-hidden': 'true' }) as HTMLElement;
|
||||
if (img) layer.style.backgroundImage = `url(${img})`; // 画布不可用时降级为无水印,不影响内容
|
||||
return h('div', { class: 'ua-watermark' }, h('div', { class: 'ua-watermark-body' }, ...children), layer) as HTMLElement;
|
||||
}
|
||||
Reference in New Issue
Block a user