更新记录
1.0.0(2026-03-01)
- feat: 提供全局页面水印 API(
showPageWatermark/hidePageWatermark) - feat: 支持
grid/stagger/jitter/dual-angle模式与seed固定分布 - feat: Android(Kotlin)与 iOS(Swift)原生渲染实现
- feat: 统一错误码与日志开关(
setLogEnabled/isLogEnabled) - docs: 补充完整使用文档(平台支持、参数、错误码、排查)
平台兼容性
uni-app(4.87)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | - | - | √ | √ | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(4.87)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | √ | √ | √ | - |
hans-watermark
适用于 uni-app 与 uni-app x 的 UTS 全局页面水印插件(Android Kotlin + iOS Swift + Harmony)。
功能概览
- 提供页面级全局覆盖水印,不依赖业务页面结构。
- 支持
grid/stagger/jitter/dual-angle四种平铺模式。 - 支持通过
seed固定jitter模式分布,便于回归测试和视觉一致性。 - 提供统一错误码和回调风格(
success/fail/complete)。
接入方式
从官方插件市场安装:
- 在 HBuilderX 中打开
插件市场。 - 搜索
hans-watermark并安装到当前项目。 - 安装后会自动生成
uni_modules/hans-watermark。
业务侧直接引入:
import {
showPageWatermark,
hidePageWatermark,
setLogEnabled,
isLogEnabled,
WatermarkFail
} from '@/uni_modules/hans-watermark'
快速开始
import {
showPageWatermark,
hidePageWatermark,
setLogEnabled
} from '@/uni_modules/hans-watermark'
setLogEnabled(true)
showPageWatermark({
text: 'CONFIDENTIAL',
mode: 'jitter',
seed: 20260228,
success: (res) => {
console.log('watermark visible =', res.visible)
},
fail: (err) => {
console.error(err.errCode, err.errMsg)
}
})
hidePageWatermark({
success: (res) => {
console.log('watermark visible =', res.visible)
}
})
Harmony 接入(必须)
Harmony 方案使用 WindowStage 子窗口承载 overlay。除安装 uni_modules/hans-watermark 外,还需要在项目根目录提供 harmony-configs 中的原生页面配置。
最小目录结构:
<project-root>/
harmony-configs/
entry/src/main/resources/base/profile/main_pages.json
entry/src/main/ets/pages/HansWatermarkOverlay.ets
main_pages.json 至少包含:
{
"src": [
"pages/Index",
"pages/HansWatermarkOverlay"
]
}
entry/src/main/ets/pages/HansWatermarkOverlay.ets 可直接使用下面模板:
import display from '@ohos.display'
import { __getOverlayConfigJSONForHarmony } from '@uni_modules/hans-watermark'
interface OverlaySnapshot {
version: number
visible: boolean
text: string
opacity: number
rotation: number
fontSize: number
color: string
gapX: number
gapY: number
mode: string
jitterPx: number
angleVariance: number
seed: number
}
interface OverlayCell {
id: string
left: number
top: number
rotation: number
}
function clamp(value: number, min: number, max: number): number {
if (value < min) {
return min
}
if (value > max) {
return max
}
return value
}
function resolveMode(mode: string): string {
if (mode === 'stagger') {
return 'stagger'
}
if (mode === 'jitter') {
return 'jitter'
}
if (mode === 'dual-angle') {
return 'dual-angle'
}
return 'grid'
}
function u32(value: number): number {
return value >>> 0
}
function cellNoise(seed: number, row: number, col: number, channel: number): number {
let value = u32(u32(seed) * 73856093)
value = u32(value ^ u32(row * 19349663))
value = u32(value ^ u32(col * 83492791))
value = u32(value ^ u32(channel * 265443576))
value = u32(value ^ (value >>> 13))
value = u32(value * 1274126177)
value = u32(value ^ (value >>> 16))
const positive = value & 0x7fffffff
const normalized = positive / 1073741824.0
return normalized - 1.0
}
@Entry
@Component
struct HansWatermarkOverlay {
@State private cells: Array<OverlayCell> = []
private timerId: number = 0
private lastVersion: number = -1
private wmVisible: boolean = false
private wmText: string = ''
private wmOpacity: number = 0.15
private wmRotation: number = -25
private wmFontSize: number = 18
private wmColor: string = '#000000'
private wmGapX: number = 120
private wmGapY: number = 88
private wmMode: string = 'grid'
private wmJitterPx: number = 10
private wmAngleVariance: number = 12
private wmSeed: number = 0
private viewportWidth: number = 390
private viewportHeight: number = 844
aboutToAppear(): void {
this.pullConfig(true)
this.timerId = setInterval(() => {
this.pullConfig(false)
}, 240)
}
aboutToDisappear(): void {
if (this.timerId > 0) {
clearInterval(this.timerId)
this.timerId = 0
}
}
private pullConfig(force: boolean): void {
let payload = ''
try {
payload = __getOverlayConfigJSONForHarmony()
} catch (_) {
return
}
if (payload.length == 0) {
return
}
let snapshot: OverlaySnapshot
try {
snapshot = JSON.parse(payload) as OverlaySnapshot
} catch (_) {
return
}
if (!force && snapshot.version == this.lastVersion) {
return
}
this.lastVersion = snapshot.version
this.wmVisible = snapshot.visible
this.wmText = snapshot.text.trim()
this.wmOpacity = clamp(snapshot.opacity, 0, 1)
this.wmRotation = snapshot.rotation
this.wmFontSize = clamp(snapshot.fontSize, 8, 120)
this.wmColor = snapshot.color
this.wmGapX = clamp(snapshot.gapX, 0, 400)
this.wmGapY = clamp(snapshot.gapY, 0, 400)
this.wmMode = resolveMode(snapshot.mode)
this.wmJitterPx = clamp(snapshot.jitterPx, 0, 80)
this.wmAngleVariance = clamp(snapshot.angleVariance, 0, 45)
this.wmSeed = Math.round(snapshot.seed)
this.rebuildCells()
}
private refreshDisplaySize(): void {
try {
const info = display.getDefaultDisplaySync()
if (info.width > 0) {
this.viewportWidth = info.width
}
if (info.height > 0) {
this.viewportHeight = info.height
}
} catch (_) {
}
}
private estimateTextWidth(): number {
let wideCount = 0
const size = this.wmText.length
for (let i = 0; i < size; i++) {
const code = this.wmText.charCodeAt(i)
if (code > 255) {
wideCount++
}
}
const asciiCount = size - wideCount
return Math.max(wideCount * this.wmFontSize + asciiCount * this.wmFontSize * 0.58, this.wmFontSize * 2)
}
private rebuildCells(): void {
if (!this.wmVisible || this.wmText.length == 0) {
this.cells = []
return
}
this.refreshDisplaySize()
if (this.viewportWidth <= 0 || this.viewportHeight <= 0) {
this.cells = []
return
}
const textWidth = this.estimateTextWidth()
const textHeight = Math.max(this.wmFontSize * 1.2, 10)
const stepX = Math.max(textWidth + this.wmGapX, textWidth + 24)
const stepY = Math.max(textHeight + this.wmGapY, textHeight + 18)
const result: Array<OverlayCell> = []
let row = 0
let y = -textHeight - stepY
while (y < this.viewportHeight + stepY * 2) {
const rowOffsetX = (this.wmMode == 'stagger' && row % 2 != 0) ? (stepX * 0.5) : 0
let col = 0
let x = -textWidth - stepX + rowOffsetX
while (x < this.viewportWidth + stepX * 2) {
let drawX = x
let drawY = y
let drawRotation = this.wmRotation
if (this.wmMode == 'dual-angle') {
const sign = ((row + col) % 2 == 0) ? -1 : 1
drawRotation = this.wmRotation + sign * this.wmAngleVariance
} else if (this.wmMode == 'jitter') {
const noiseX = cellNoise(this.wmSeed, row, col, 11)
const noiseY = cellNoise(this.wmSeed, row, col, 29)
const noiseR = cellNoise(this.wmSeed, row, col, 53)
drawX += noiseX * this.wmJitterPx
drawY += noiseY * this.wmJitterPx
drawRotation = this.wmRotation + noiseR * this.wmAngleVariance
}
result.push({
id: `${row}-${col}`,
left: drawX,
top: drawY,
rotation: drawRotation
})
col++
x += stepX
}
row++
y += stepY
}
this.cells = result
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
ForEach(this.cells, (cell: OverlayCell) => {
Text(this.wmText)
.position({ x: cell.left, y: cell.top })
.fontSize(this.wmFontSize)
.fontColor(this.wmColor)
.opacity(this.wmOpacity)
.rotate({ x: 0, y: 0, z: 1, angle: cell.rotation })
}, (cell: OverlayCell) => cell.id)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Transparent)
}
}
说明:
pages/HansWatermarkOverlay是子窗口setUIContent的加载目标。- 若缺少该页面或未注册到
main_pages.json,会出现setUIContent failed ... code: 401。 - 本仓库 playground 已包含可用的
harmony-configs参考实现。
API
setLogEnabled(enabled: boolean): void
开启/关闭插件日志。
isLogEnabled(): boolean
读取当前日志开关状态。
showPageWatermark(options): void
显示或更新页面水印。重复调用会更新同一层覆盖视图。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
text |
string |
是 | - | 水印文本,不能为空 |
opacity |
number |
否 | 0.15 |
透明度,范围会被钳制到 0~1 |
rotation |
number |
否 | -25 |
旋转角度(度) |
fontSize |
number |
否 | 18 |
字号,范围会被钳制到 8~120 |
color |
string |
否 | #000000 |
文本颜色(推荐 #RRGGBB) |
gapX |
number |
否 | 120 |
水平间距,范围会被钳制到 0~400 |
gapY |
number |
否 | 88 |
垂直间距,范围会被钳制到 0~400 |
mode |
'grid' \| 'stagger' \| 'jitter' \| 'dual-angle' |
否 | grid |
水印排布模式 |
jitterPx |
number |
否 | 10 |
抖动位移,范围会被钳制到 0~80 |
angleVariance |
number |
否 | 12 |
角度扰动,范围会被钳制到 0~45 |
seed |
number |
否 | 0 |
随机种子,jitter 模式在同平台可稳定复现分布 |
success |
(res) => void |
否 | - | 成功回调 |
fail |
(err) => void |
否 | - | 失败回调,err 为 WatermarkFail |
complete |
(res) => void |
否 | - | 完成回调 |
成功回调 res:
{
visible: boolean
}
hidePageWatermark(options?): void
关闭页面水印层,支持可选回调参数:
success(res)fail(err)complete(res)
模式说明
grid: 常规均匀网格。stagger: 奇偶行错位半个步长。jitter: 在网格基础上加入随机位移和随机角度(受seed控制)。dual-angle: 邻近文本正负角度交替,增强视觉破坏性。
错误码
| 错误码 | 含义 | 常见触发 |
|---|---|---|
9010001 |
参数错误 | text 为空 |
9010005 |
平台异常 | 原生层执行失败、超时等 |
9010006 |
当前平台不支持 | 预留错误码(非支持平台调用) |
9010008 |
页面水印上下文不可用 | 获取当前页面窗口失败 |
调试与排查
- 打开日志:
setLogEnabled(true)。 - Android 日志关键字:
[hans-watermark][android]。 - iOS 日志关键字:
[hans-watermark][ios]。 - Harmony 日志关键字:
[hans-watermark][harmony]。 - 如果出现
9010008,优先确认调用时机(建议页面onReady/onShow后)以及当前窗口是否可用。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 249
赞赏 0
下载 11262848
赞赏 1866
赞赏
京公网安备:11010802035340号