Skip to content

Commit ae7d91b

Browse files
authored
feat(Watermark): add Watermark component (#5099)
* feat: 增加水印组件 * feat: 增加样式 * doc: 增加菜单 * doc: 增加路由源码映射 * doc: 增加示例 * refactor: 增加脚本 * feat: 增加防篡改逻辑 * refactor: 增加更新逻辑 * doc: 更新示例 * feat: 增加颜色参数支持 * doc: 更新示例 * feat: 增加 Gap 参数 * doc: 增加默认值 * doc: 更新文档 * refactor: 更新参数数据类型 * doc: 更新示例 * test: 更新单元测试 * chore: bump version 9.2.7
1 parent 0d7e077 commit ae7d91b

File tree

13 files changed

+366
-5
lines changed

13 files changed

+366
-5
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@page "/watermark"
2+
@inject IStringLocalizer<Watermarks> Localizer
3+
4+
<h3>@Localizer["WatermarkTitle"]</h3>
5+
6+
<h4>@Localizer["Watermarkntro"]</h4>
7+
8+
<DemoBlock Title="@Localizer["WatermarkNormalTitle"]" Introduction="@Localizer["WatermarkNormalIntro"]" Name="Normal">
9+
<section ignore>
10+
<div class="row form-inline g-3">
11+
<div class="col-12 col-sm-6">
12+
<BootstrapInputGroup>
13+
<BootstrapInputGroupLabel DisplayText="Text"></BootstrapInputGroupLabel>
14+
<BootstrapInput @bind-Value="@_text"></BootstrapInput>
15+
</BootstrapInputGroup>
16+
</div>
17+
<div class="col-12 col-sm-6">
18+
<BootstrapInputGroup>
19+
<BootstrapInputGroupLabel DisplayText="FontSize"></BootstrapInputGroupLabel>
20+
<Slider @bind-Value="@_fontSize" Min="12" Max="20"></Slider>
21+
</BootstrapInputGroup>
22+
</div>
23+
<div class="col-12 col-sm-6">
24+
<BootstrapInputGroup>
25+
<BootstrapInputGroupLabel DisplayText="Color"></BootstrapInputGroupLabel>
26+
<ColorPicker @bind-Value="@_color" IsSupportOpacity="true"></ColorPicker>
27+
</BootstrapInputGroup>
28+
</div>
29+
<div class="col-12 col-sm-6">
30+
<BootstrapInputGroup>
31+
<BootstrapInputGroupLabel DisplayText="Rotate"></BootstrapInputGroupLabel>
32+
<Slider @bind-Value="@_rotate" Min="-180" Max="180"></Slider>
33+
</BootstrapInputGroup>
34+
</div>
35+
<div class="col-12 col-sm-6">
36+
<BootstrapInputGroup>
37+
<BootstrapInputGroupLabel DisplayText="Gap"></BootstrapInputGroupLabel>
38+
<Slider @bind-Value="@_gap" Min="0" Max="100"></Slider>
39+
</BootstrapInputGroup>
40+
</div>
41+
</div>
42+
</section>
43+
<Watermark Text="@_text" FontSize="@_fontSize" Color="@_color" Rotate="@_rotate"
44+
Gap="@_gap">
45+
<div style="height: 500px; padding-top: 40px; text-align: center;">
46+
<p>this is a watermark demo</p>
47+
<div>这是 watermark 演示</div>
48+
</div>
49+
</Watermark>
50+
</DemoBlock>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Server.Components.Samples;
7+
8+
/// <summary>
9+
/// Watermarks 组件
10+
/// </summary>
11+
public partial class Watermarks
12+
{
13+
private string _text = "BootstrapBlazor";
14+
15+
private int _fontSize = 16;
16+
17+
private int _gap = 40;
18+
19+
private int _rotate = -40;
20+
21+
private string _color = "#0000004d";
22+
}

src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,12 @@ void AddData(DemoMenuItem item)
774774
{
775775
Text = Localizer["Waterfall"],
776776
Url = "tutorials/waterfall"
777+
},
778+
new()
779+
{
780+
IsNew = true,
781+
Text = Localizer["Watermark"],
782+
Url = "watermark"
777783
}
778784
};
779785
AddBadge(item);

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4787,7 +4787,8 @@
47874787
"Player": "Player",
47884788
"RDKit": "RDKit",
47894789
"SmilesDrawer": "SmilesDrawer",
4790-
"Affix": "Affix"
4790+
"Affix": "Affix",
4791+
"Watermark": "Watermark"
47914792
},
47924793
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
47934794
"TablesHeaderTitle": "Header grouping function",
@@ -6878,5 +6879,11 @@
68786879
"AffixPositionTitle": "Position",
68796880
"AffixPositionIntro": "Use the parameter <code>Position</code> to control whether the top or bottom is fixed",
68806881
"AffixOffsetDesc": "The parameter <code>Position</code> controls whether the top or bottom is fixed, and the <code>Offset</code> value sets the offset to the top or bottom"
6882+
},
6883+
"BootstrapBlazor.Server.Components.Samples.Watermarks": {
6884+
"WatermarkTitle": "Watermark",
6885+
"Watermarkntro": "Add specific text or patterns to the page",
6886+
"WatermarkNormalTitle": "Basic usage",
6887+
"WatermarkNormalIntro": "Use the <code>Text</code> property to set a string to specify the watermark text"
68816888
}
68826889
}

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4787,7 +4787,8 @@
47874787
"Player": "播放器 Player",
47884788
"RDKit": "分子式组件 RDKit",
47894789
"SmilesDrawer": "分子式组件 SmilesDrawer",
4790-
"Affix": "固钉组件 Affix"
4790+
"Affix": "固钉组件 Affix",
4791+
"Watermark": "水印组件 Watermark"
47914792
},
47924793
"BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": {
47934794
"TablesHeaderTitle": "表头分组功能",
@@ -6877,5 +6878,11 @@
68776878
"AffixNormalIntro": "固钉默认固定在页面顶部",
68786879
"AffixPositionTitle": "位置与距离",
68796880
"AffixPositionIntro": "通过参数 <code>Position</code> 控制固定顶端还是底端,通过 <code>Offset</code> 值设置到顶端或者底端距离偏移量"
6881+
},
6882+
"BootstrapBlazor.Server.Components.Samples.Watermarks": {
6883+
"WatermarkTitle": "Watermark 水印组件",
6884+
"Watermarkntro": "在页面上添加文本或图片等水印信息",
6885+
"WatermarkNormalTitle": "基础用法",
6886+
"WatermarkNormalIntro": "使用 <code>Text</code> 属性设置一个字符串指定水印内容"
68806887
}
68816888
}

src/BootstrapBlazor.Server/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@
223223
"player": "Players",
224224
"rdkit": "Rdkits",
225225
"smiles-drawer": "SmilesDrawers",
226-
"affix": "Affixs"
226+
"affix": "Affixs",
227+
"watermark": "Watermarks"
227228
},
228229
"video": {
229230
"table": "BV1ap4y1x7Qn?p=1",

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Razor">
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.2.7-beta05</Version>
4+
<Version>9.2.7</Version>
55
</PropertyGroup>
66

77
<ItemGroup>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@namespace BootstrapBlazor.Components
2+
@inherits BootstrapModuleComponentBase
3+
@attribute [BootstrapModuleAutoLoader]
4+
5+
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString">
6+
@ChildContent
7+
</div>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Components;
7+
8+
/// <summary>
9+
/// Watermark 组件
10+
/// </summary>
11+
public partial class Watermark
12+
{
13+
/// <summary>
14+
/// 获得/设置 组件内容
15+
/// </summary>
16+
[Parameter]
17+
[EditorRequired]
18+
public RenderFragment? ChildContent { get; set; }
19+
20+
/// <summary>
21+
/// 获得/设置 水印文本 默认 BootstrapBlazor
22+
/// </summary>
23+
[Parameter]
24+
public string? Text { get; set; }
25+
26+
/// <summary>
27+
/// 获得/设置 字体大小 默认 null 未设置 默认使用 16px 字体大小 单位 px
28+
/// </summary>
29+
[Parameter]
30+
public int? FontSize { get; set; }
31+
32+
/// <summary>
33+
/// 获得/设置 颜色 默认 null 未设置
34+
/// </summary>
35+
[Parameter]
36+
public string? Color { get; set; }
37+
38+
/// <summary>
39+
/// 获得/设置 水印的旋转角度 默认 null 45°
40+
/// </summary>
41+
[Parameter]
42+
public int? Rotate { get; set; }
43+
44+
/// <summary>
45+
/// 获得/设置 水印元素的 z-index 值 默认 null
46+
/// </summary>
47+
[Parameter]
48+
public int? ZIndex { get; set; }
49+
50+
/// <summary>
51+
/// 获得/设置 水印之间的间距 值 默认 null
52+
/// </summary>
53+
[Parameter]
54+
public int? Gap { get; set; }
55+
56+
private string? ClassString => CssBuilder.Default("bb-watermark")
57+
.AddClassFromAttributes(AdditionalAttributes)
58+
.Build();
59+
60+
/// <summary>
61+
/// <inheritdoc/>
62+
/// </summary>
63+
protected override void OnParametersSet()
64+
{
65+
base.OnParametersSet();
66+
67+
Text ??= "BootstrapBlazor";
68+
}
69+
70+
/// <summary>
71+
/// <inheritdoc/>
72+
/// </summary>
73+
/// <param name="firstRender"></param>
74+
/// <returns></returns>
75+
protected override async Task OnAfterRenderAsync(bool firstRender)
76+
{
77+
await base.OnAfterRenderAsync(firstRender);
78+
79+
if (!firstRender)
80+
{
81+
await InvokeVoidAsync("update", Id, GetOptions());
82+
}
83+
}
84+
85+
/// <summary>
86+
/// <inheritdoc/>
87+
/// </summary>
88+
/// <returns></returns>
89+
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, GetOptions());
90+
91+
private object GetOptions() => new
92+
{
93+
Text,
94+
FontSize,
95+
Color,
96+
Rotate,
97+
Gap,
98+
ZIndex
99+
};
100+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import Data from "../../modules/data.js"
2+
3+
export function init(id, options) {
4+
const el = document.getElementById(id);
5+
if (el === null) {
6+
return;
7+
}
8+
const watermark = { el, options };
9+
createWatermark(watermark);
10+
11+
const observer = ob => {
12+
ob.observe(el, {
13+
childList: true,
14+
attributes: true,
15+
subtree: true
16+
});
17+
}
18+
19+
const observerCallback = records => {
20+
ob.disconnect();
21+
updateWatermark(records, watermark);
22+
observer(ob);
23+
};
24+
25+
const ob = new MutationObserver(observerCallback);
26+
observer(ob);
27+
watermark.ob = ob;
28+
29+
Data.set(id, watermark);
30+
}
31+
32+
export function update(id, options) {
33+
const watermark = Data.get(id);
34+
watermark.options = options;
35+
36+
createWatermark(watermark);
37+
}
38+
39+
export function dispose(id) {
40+
const watermark = Data.get(id);
41+
Data.remove(id);
42+
43+
if (watermark) {
44+
const { ob } = watermark;
45+
ob.disconnect();
46+
47+
delete watermark.ob;
48+
}
49+
}
50+
51+
const updateWatermark = (records, watermark) => {
52+
for (const record of records) {
53+
for (const dom of record.removedNodes) {
54+
if (dom.classList.contains('bb-watermark-bg')) {
55+
createWatermark(watermark);
56+
return;
57+
}
58+
}
59+
60+
if (record.target.classList.contains('bb-watermark-bg')) {
61+
createWatermark(watermark);
62+
return;
63+
}
64+
}
65+
}
66+
67+
const createWatermark = watermark => {
68+
const { el, options } = watermark;
69+
const defaults = {
70+
gap: 40,
71+
fontSize: 16,
72+
text: 'BootstrapBlazor',
73+
rotate: -40,
74+
color: '#0000004d'
75+
};
76+
77+
for (const key in options) {
78+
if (options[key] === void 0 || options[key] === null) {
79+
delete options[key];
80+
}
81+
}
82+
83+
const bg = getWatermark({ ...defaults, ...options });
84+
const div = document.createElement('div');
85+
const { base64, styleSize } = bg;
86+
div.style.backgroundImage = `url(${base64})`;
87+
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
88+
div.style.backgroundRepeat = 'repeat';
89+
div.style.pointerEvents = 'none';
90+
div.style.opacity = '1';
91+
div.style.position = 'absolute';
92+
div.style.inset = '0';
93+
div.style.zIndex = '999';
94+
div.classList.add("bb-watermark-bg");
95+
96+
const mark = el.querySelector('.bb-watermark-bg');
97+
if (mark) {
98+
mark.remove();
99+
}
100+
el.appendChild(div);
101+
}
102+
103+
const getWatermark = props => {
104+
const canvas = document.createElement('canvas');
105+
const devicePixelRatio = window.devicePixelRatio || 1;
106+
107+
const fontSize = props.fontSize * devicePixelRatio;
108+
const font = fontSize + 'px serif';
109+
const ctx = canvas.getContext('2d');
110+
111+
ctx.font = font;
112+
const { width } = ctx.measureText(props.text);
113+
const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio;
114+
canvas.width = canvasSize;
115+
canvas.height = canvasSize;
116+
ctx.translate(canvas.width / 2, canvas.height / 2);
117+
118+
ctx.rotate((Math.PI / 180) * props.rotate);
119+
ctx.fillStyle = props.color;
120+
ctx.font = font;
121+
ctx.textAlign = 'center';
122+
ctx.textBaseline = 'middle';
123+
124+
ctx.fillText(props.text, 0, 0);
125+
return {
126+
base64: canvas.toDataURL(),
127+
size: canvasSize,
128+
styleSize: canvasSize / devicePixelRatio,
129+
};
130+
}

0 commit comments

Comments
 (0)