mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 12:39:49 +08:00
feat: New Component Watermark (#39064)
* feat: New Component Watermark * docs: add watermark docs * docs: add watermark demo * test: add watermark test * test: add watermark snapshot * chore: add jest-canvas-mock * feat: Watermark calculates the width and height of content by default * docs: update docs * docs: update demo * test: update snapshot * docs: update docs * chore: update bundlesize * chore: Optimize code logic * chore: update size-limit * test: update watermark snapshot
This commit is contained in:
parent
451d2f6ee2
commit
6f83c63d74
2
.jest.js
2
.jest.js
@ -34,7 +34,7 @@ function getTestRegex(libDir) {
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
setupFiles: ['./tests/setup.js', 'jest-canvas-mock'],
|
||||
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
|
||||
modulePathIgnorePatterns: ['/_site/'],
|
||||
|
@ -67,6 +67,7 @@ exports[`antd exports modules correctly 1`] = `
|
||||
"TreeSelect",
|
||||
"Typography",
|
||||
"Upload",
|
||||
"Watermark",
|
||||
"message",
|
||||
"notification",
|
||||
"theme",
|
||||
|
@ -150,6 +150,8 @@ export { default as Typography } from './typography';
|
||||
export type { TypographyProps } from './typography';
|
||||
export { default as Upload } from './upload';
|
||||
export type { UploadFile, UploadProps } from './upload';
|
||||
export { default as Watermark } from './watermark';
|
||||
export type { WatermarkProps } from './watermark';
|
||||
export { default as QRCode } from './qrcode';
|
||||
export type { QRCodeProps, QRPropsCanvas } from './qrcode/interface';
|
||||
export { default as version } from './version';
|
||||
|
File diff suppressed because it is too large
Load Diff
788
components/watermark/__tests__/__snapshots__/demo.test.ts.snap
Normal file
788
components/watermark/__tests__/__snapshots__/demo.test.ts.snap
Normal file
@ -0,0 +1,788 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./components/watermark/demo/basic.tsx correctly 1`] = `
|
||||
<div
|
||||
style="position:relative"
|
||||
>
|
||||
<div
|
||||
style="height:500px"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/watermark/demo/custom.tsx correctly 1`] = `
|
||||
<div
|
||||
style="display:flex"
|
||||
>
|
||||
<div
|
||||
style="position:relative"
|
||||
>
|
||||
<article
|
||||
class="ant-typography"
|
||||
>
|
||||
<div
|
||||
class="ant-typography"
|
||||
>
|
||||
The light-speed iteration of the digital world makes products more complex. However, human consciousness and attention resources are limited. Facing this design contradiction, the pursuit of natural interaction will be the consistent direction of Ant Design.
|
||||
</div>
|
||||
<div
|
||||
class="ant-typography"
|
||||
>
|
||||
Natural user cognition: According to cognitive psychology, about 80% of external information is obtained through visual channels. The most important visual elements in the interface design, including layout, colors, illustrations, icons, etc., should fully absorb the laws of nature, thereby reducing the user's cognitive cost and bringing authentic and smooth feelings. In some scenarios, opportunely adding other sensory channels such as hearing, touch can create a richer and more natural product experience.
|
||||
</div>
|
||||
<div
|
||||
class="ant-typography"
|
||||
>
|
||||
Natural user behavior: In the interaction with the system, the designer should fully understand the relationship between users, system roles, and task objectives, and also contextually organize system functions and services. At the same time, a series of methods such as behavior analysis, artificial intelligence and sensors could be applied to assist users to make effective decisions and reduce extra operations of users, to save users' mental and physical resources and make human-computer interaction more natural.
|
||||
</div>
|
||||
</article>
|
||||
<img
|
||||
alt="示例图片"
|
||||
src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*zx7LTI_ECSAAAAAAAAAAAABkARQnAQ"
|
||||
style="z-index:10;width:100%;max-width:800px;position:relative"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
class="ant-form ant-form-vertical"
|
||||
style="width:280px;flex-shrink:0;border-left:1px solid #eee;padding-left:20px;margin-left:20px"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="content"
|
||||
title="Content"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
id="content"
|
||||
placeholder="请输入"
|
||||
type="text"
|
||||
value="Ant Design"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="color"
|
||||
title="Color"
|
||||
>
|
||||
Color
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
style="padding:4px;background:#fff;border-radius:2px;border:1px solid #dedede;display:inline-block;cursor:pointer"
|
||||
>
|
||||
<div
|
||||
style="width:36px;height:14px;border-radius:2px;background:rgba(0, 0, 0, 0.15)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="fontSize"
|
||||
title="FontSize"
|
||||
>
|
||||
FontSize
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:16%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="16"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:16%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="zIndex"
|
||||
title="zIndex"
|
||||
>
|
||||
zIndex
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:11%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="11"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:11%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="rotate"
|
||||
title="Rotate"
|
||||
>
|
||||
Rotate
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:43.888888888888886%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-valuemax="180"
|
||||
aria-valuemin="-180"
|
||||
aria-valuenow="-22"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:43.888888888888886%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
style="margin-bottom:0"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Gap"
|
||||
>
|
||||
Gap
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-baseline"
|
||||
style="display:flex"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-valuenow="200"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="gap_0"
|
||||
placeholder="gapX"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value="200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-valuenow="200"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="gap_1"
|
||||
placeholder="gapY"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value="200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
style="margin-bottom:0"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Offset"
|
||||
>
|
||||
Offset
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-baseline"
|
||||
style="display:flex"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="offset_0"
|
||||
placeholder="offsetLeft"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
style="width:100%"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="offset_1"
|
||||
placeholder="offsetTop"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/watermark/demo/image.tsx correctly 1`] = `
|
||||
<div
|
||||
style="position:relative"
|
||||
>
|
||||
<div
|
||||
style="height:500px"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/watermark/demo/multi-line.tsx correctly 1`] = `
|
||||
<div
|
||||
style="position:relative"
|
||||
>
|
||||
<div
|
||||
style="height:500px"
|
||||
/>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Watermark Image watermark snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 320px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Watermark MutationObserver should work properly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="watermark"
|
||||
style="position: relative;"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Watermark Observe the modification of style 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="watermark"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
style=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Watermark The offset should be correct 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="watermark"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
style="z-index: 9; position: absolute; left: 100px; top: 100px; width: calc(100% - 100px); height: calc(100% - 100px); background-size: 214px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Watermark The watermark should render successfully 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="watermark"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 210px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Watermark rtl render component should be rendered correctly in RTL direction 1`] = `
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
style="z-index: 9; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-size: 200px; pointer-events: none; background-repeat: repeat; background-position: 0px 0px; background-image: url('data:image/png;base64,00');"
|
||||
/>
|
||||
</div>
|
||||
`;
|
3
components/watermark/__tests__/demo-extend.test.ts
Normal file
3
components/watermark/__tests__/demo-extend.test.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { extendTest } from '../../../tests/shared/demoTest';
|
||||
|
||||
extendTest('watermark');
|
3
components/watermark/__tests__/demo.test.ts
Normal file
3
components/watermark/__tests__/demo.test.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('watermark');
|
5
components/watermark/__tests__/image.test.ts
Normal file
5
components/watermark/__tests__/image.test.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { imageDemoTest } from '../../../tests/shared/imageTest';
|
||||
|
||||
describe('Watermark image', () => {
|
||||
imageDemoTest('watermark');
|
||||
});
|
69
components/watermark/__tests__/index.test.tsx
Normal file
69
components/watermark/__tests__/index.test.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import Watermark from '..';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { render } from '../../../tests/utils';
|
||||
|
||||
describe('Watermark', () => {
|
||||
mountTest(Watermark);
|
||||
rtlTest(Watermark);
|
||||
|
||||
const mockSrcSet = jest.spyOn(Image.prototype, 'src', 'set');
|
||||
|
||||
beforeAll(() => {
|
||||
mockSrcSet.mockImplementation(function fn() {
|
||||
if (this.onload) {
|
||||
this.onload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockSrcSet.mockRestore();
|
||||
});
|
||||
|
||||
it('The watermark should render successfully', () => {
|
||||
const { container } = render(<Watermark className="watermark" content="Ant Design" />);
|
||||
expect(container.querySelector('.watermark div')).toBeTruthy();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('The offset should be correct', () => {
|
||||
const { container } = render(
|
||||
<Watermark
|
||||
className="watermark"
|
||||
offset={[200, 200]}
|
||||
content={['Ant Design', 'Ant Design Pro']}
|
||||
/>,
|
||||
);
|
||||
const target = container.querySelector('.watermark div') as HTMLDivElement;
|
||||
expect(target.style.left).toBe('100px');
|
||||
expect(target.style.top).toBe('100px');
|
||||
expect(target.style.width).toBe('calc(100% - 100px)');
|
||||
expect(target.style.height).toBe('calc(100% - 100px)');
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Image watermark snapshot', () => {
|
||||
const { container } = render(
|
||||
<Watermark image="https://gw.alipayobjects.com/zos/bmw-prod/59a18171-ae17-4fc5-93a0-2645f64a3aca.svg" />,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('MutationObserver should work properly', () => {
|
||||
const { container } = render(<Watermark className="watermark" content="MutationObserver" />);
|
||||
const target = container.querySelector('.watermark div') as HTMLDivElement;
|
||||
target.remove();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Observe the modification of style', () => {
|
||||
const { container } = render(
|
||||
<Watermark offset={[-200, -200]} className="watermark" content="MutationObserver" />,
|
||||
);
|
||||
const target = container.querySelector('.watermark div') as HTMLDivElement;
|
||||
target.setAttribute('style', '');
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
7
components/watermark/demo/basic.md
Normal file
7
components/watermark/demo/basic.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
最简单的用法。
|
||||
|
||||
## en-US
|
||||
|
||||
The most basic usage.
|
10
components/watermark/demo/basic.tsx
Normal file
10
components/watermark/demo/basic.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Watermark } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Watermark content="Ant Design">
|
||||
<div style={{ height: 500 }} />
|
||||
</Watermark>
|
||||
);
|
||||
|
||||
export default App;
|
7
components/watermark/demo/custom.md
Normal file
7
components/watermark/demo/custom.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过自定义参数配置预览水印效果。
|
||||
|
||||
## en-US
|
||||
|
||||
Preview the watermark effect by configuring custom parameters.
|
166
components/watermark/demo/custom.tsx
Normal file
166
components/watermark/demo/custom.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Watermark, Popover, Typography, Form, Input, Slider, Space, InputNumber } from 'antd';
|
||||
import { SketchPicker } from 'react-color';
|
||||
import type { RGBColor } from 'react-color';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface ColorPickerProps {
|
||||
value?: RGBColor;
|
||||
onChange?: (value: RGBColor) => void;
|
||||
}
|
||||
|
||||
const ColorPicker: React.FC<ColorPickerProps> = ({ value, onChange }) => {
|
||||
const switchStyle = {
|
||||
padding: 4,
|
||||
background: '#fff',
|
||||
borderRadius: 2,
|
||||
border: '1px solid #dedede',
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const colorStyle = {
|
||||
width: 36,
|
||||
height: 14,
|
||||
borderRadius: 2,
|
||||
background: `rgba(${value?.r}, ${value?.g}, ${value?.b}, ${value?.a})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottomLeft"
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
content={<SketchPicker color={value} onChange={(color) => onChange?.(color.rgb)} />}
|
||||
>
|
||||
<div style={switchStyle}>
|
||||
<div style={colorStyle} />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [config, setConfig] = useState({
|
||||
content: 'Ant Design',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.15 },
|
||||
fontSize: 16,
|
||||
zIndex: 11,
|
||||
rotate: -22,
|
||||
gap: [200, 200] as [number, number],
|
||||
offset: undefined,
|
||||
});
|
||||
const { content, color, fontSize, zIndex, rotate, gap, offset } = config;
|
||||
|
||||
const watermarkProps = useMemo(
|
||||
() => ({
|
||||
content,
|
||||
font: {
|
||||
color: `rgba(${color.r},${color.g},${color.b},${color.a})`,
|
||||
fontSize,
|
||||
},
|
||||
zIndex,
|
||||
rotate,
|
||||
gap,
|
||||
offset,
|
||||
}),
|
||||
[config],
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Watermark {...watermarkProps}>
|
||||
<Typography>
|
||||
<Paragraph>
|
||||
The light-speed iteration of the digital world makes products more complex. However,
|
||||
human consciousness and attention resources are limited. Facing this design
|
||||
contradiction, the pursuit of natural interaction will be the consistent direction of
|
||||
Ant Design.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Natural user cognition: According to cognitive psychology, about 80% of external
|
||||
information is obtained through visual channels. The most important visual elements in
|
||||
the interface design, including layout, colors, illustrations, icons, etc., should fully
|
||||
absorb the laws of nature, thereby reducing the user's cognitive cost and bringing
|
||||
authentic and smooth feelings. In some scenarios, opportunely adding other sensory
|
||||
channels such as hearing, touch can create a richer and more natural product experience.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Natural user behavior: In the interaction with the system, the designer should fully
|
||||
understand the relationship between users, system roles, and task objectives, and also
|
||||
contextually organize system functions and services. At the same time, a series of
|
||||
methods such as behavior analysis, artificial intelligence and sensors could be applied
|
||||
to assist users to make effective decisions and reduce extra operations of users, to
|
||||
save users' mental and physical resources and make human-computer interaction more
|
||||
natural.
|
||||
</Paragraph>
|
||||
</Typography>
|
||||
<img
|
||||
style={{
|
||||
zIndex: 10,
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
position: 'relative',
|
||||
}}
|
||||
src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*zx7LTI_ECSAAAAAAAAAAAABkARQnAQ"
|
||||
alt="示例图片"
|
||||
/>
|
||||
</Watermark>
|
||||
<Form
|
||||
style={{
|
||||
width: 280,
|
||||
flexShrink: 0,
|
||||
borderLeft: '1px solid #eee',
|
||||
paddingLeft: 20,
|
||||
marginLeft: 20,
|
||||
}}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={config}
|
||||
onValuesChange={(_, values) => {
|
||||
setConfig(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="content" label="Content">
|
||||
<Input placeholder="请输入" />
|
||||
</Form.Item>
|
||||
<Form.Item name="color" label="Color">
|
||||
<ColorPicker />
|
||||
</Form.Item>
|
||||
<Form.Item name="fontSize" label="FontSize">
|
||||
<Slider step={1} min={0} max={100} />
|
||||
</Form.Item>
|
||||
<Form.Item name="zIndex" label="zIndex">
|
||||
<Slider step={1} min={0} max={100} />
|
||||
</Form.Item>
|
||||
<Form.Item name="rotate" label="Rotate">
|
||||
<Slider step={1} min={-180} max={180} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Gap" style={{ marginBottom: 0 }}>
|
||||
<Space style={{ display: 'flex' }} align="baseline">
|
||||
<Form.Item name={['gap', 0]}>
|
||||
<InputNumber placeholder="gapX" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['gap', 1]}>
|
||||
<InputNumber placeholder="gapY" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item label="Offset" style={{ marginBottom: 0 }}>
|
||||
<Space style={{ display: 'flex' }} align="baseline">
|
||||
<Form.Item name={['offset', 0]}>
|
||||
<InputNumber placeholder="offsetLeft" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['offset', 1]}>
|
||||
<InputNumber placeholder="offsetTop" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
7
components/watermark/demo/image.md
Normal file
7
components/watermark/demo/image.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `image` 指定图片地址。为保证图片高清且不被拉伸,请设置 width 和 height, 并上传至少两倍的宽高的 logo 图片地址。
|
||||
|
||||
## en-US
|
||||
|
||||
Specify the image address via 'image'. To ensure that the image is high definition and not stretched, set the width and height, and upload at least twice the width and height of the logo image address.
|
14
components/watermark/demo/image.tsx
Normal file
14
components/watermark/demo/image.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Watermark } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Watermark
|
||||
height={30}
|
||||
width={130}
|
||||
image="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*lkAoRbywo0oAAAAAAAAAAAAADrJ8AQ/original"
|
||||
>
|
||||
<div style={{ height: 500 }} />
|
||||
</Watermark>
|
||||
);
|
||||
|
||||
export default App;
|
7
components/watermark/demo/multi-line.md
Normal file
7
components/watermark/demo/multi-line.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `content` 设置 字符串数组 指定多行文字水印内容。
|
||||
|
||||
## en-US
|
||||
|
||||
Use 'content' to set a string array to specify multi-line text watermark content.
|
10
components/watermark/demo/multi-line.tsx
Normal file
10
components/watermark/demo/multi-line.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Watermark } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Watermark content={['Ant Design', 'Happy Working']}>
|
||||
<div style={{ height: 500 }} />
|
||||
</Watermark>
|
||||
);
|
||||
|
||||
export default App;
|
50
components/watermark/index.en-US.md
Normal file
50
components/watermark/index.en-US.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
category: Components
|
||||
group: Other
|
||||
title: Watermark
|
||||
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original
|
||||
demo:
|
||||
cols: 1
|
||||
---
|
||||
|
||||
Add specific text or patterns to the page.
|
||||
|
||||
## When To Use
|
||||
|
||||
- Use when the page needs to be watermarked to identify the copyright.
|
||||
- Suitable for preventing information theft.
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<code src="./demo/basic.tsx">Basic</code>
|
||||
<code src="./demo/multi-line.tsx">Multi-line watermark</code>
|
||||
<code src="./demo/image.tsx">Image watermark</code>
|
||||
<code src="./demo/custom.tsx">Custom configuration</code>
|
||||
|
||||
## API
|
||||
|
||||
### Watermark
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| width | The width of the watermark, the default value of `content` is its own width | number | 120 | |
|
||||
| height | The height of the watermark, the default value of `content` is its own height | number | 64 | |
|
||||
| rotate | When the watermark is drawn, the rotation Angle, unit `°` | number | -22 | |
|
||||
| zIndex | The z-index of the appended watermark element | number | 9 | |
|
||||
| image | Image source, it is recommended to export 2x or 3x image, high priority | string | - | |
|
||||
| content | Watermark text content | string \| string[] | - | |
|
||||
| font | Text style | [Font](#Font) | [Font](#Font) | |
|
||||
| gap | The spacing between watermarks | \[number, number\] | \[200, 200\] | |
|
||||
| offset | The offset of the watermark from the upper left corner of the container. The default is `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | |
|
||||
|
||||
### Font
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| color | font color | string | rgba(0,0,0,.15) | |
|
||||
| fontSize | font size | number | 16 | |
|
||||
| fontWeight | font weight | `normal` \| `light` \| `weight` \| number | normal | |
|
||||
| fontFamily | font family | string | sans-serif | |
|
||||
| fontStyle | font style | `none` \| `normal` \| `italic` \| `oblique` | normal | |
|
262
components/watermark/index.tsx
Normal file
262
components/watermark/index.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import useMutationObserver from './useMutationObserver';
|
||||
|
||||
const FontGap = 3;
|
||||
|
||||
const getStyleStr = (style: Record<string, string | number>): string => {
|
||||
const styleArr = Object.keys(style).map((item) => {
|
||||
const key = item.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
return `${key}: ${style[item]};`;
|
||||
});
|
||||
return styleArr.join(' ');
|
||||
};
|
||||
|
||||
export interface WatermarkProps {
|
||||
zIndex?: number;
|
||||
rotate?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
image?: string;
|
||||
content?: string | string[];
|
||||
font?: {
|
||||
color?: string;
|
||||
fontSize?: number | string;
|
||||
fontWeight?: 'normal' | 'light' | 'weight' | number;
|
||||
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique';
|
||||
fontFamily?: string;
|
||||
};
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
gap?: [number, number];
|
||||
offset?: [number, number];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Watermark: React.FC<WatermarkProps> = (props) => {
|
||||
const {
|
||||
/**
|
||||
* The antd content layer zIndex is basically below 10
|
||||
* https://github.com/ant-design/ant-design/blob/6192403b2ce517c017f9e58a32d58774921c10cd/components/style/themes/default.less#L335
|
||||
*/
|
||||
zIndex = 9,
|
||||
rotate = -22,
|
||||
width,
|
||||
height,
|
||||
image,
|
||||
content,
|
||||
font = {},
|
||||
style,
|
||||
className,
|
||||
gap = [200, 200],
|
||||
offset,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
color = 'rgba(0,0,0,.15)',
|
||||
fontSize = 16,
|
||||
fontWeight = 'normal',
|
||||
fontStyle = 'normal',
|
||||
fontFamily = 'sans-serif',
|
||||
} = font;
|
||||
|
||||
const [gapX, gapY] = gap;
|
||||
const gapXCenter = gapX / 2;
|
||||
const gapYCenter = gapY / 2;
|
||||
const offsetLeft = offset?.[0] ?? gapXCenter;
|
||||
const offsetTop = offset?.[1] ?? gapYCenter;
|
||||
|
||||
const getMarkStyle = (markWidth: number) => {
|
||||
const markStyle: React.CSSProperties = {
|
||||
zIndex,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundSize: `${gapX + markWidth}px`,
|
||||
pointerEvents: 'none',
|
||||
backgroundRepeat: 'repeat',
|
||||
};
|
||||
|
||||
/** Calculate the style of the offset */
|
||||
let positionLeft = offsetLeft - gapXCenter;
|
||||
let positionTop = offsetTop - gapYCenter;
|
||||
if (positionLeft > 0) {
|
||||
markStyle.left = `${positionLeft}px`;
|
||||
markStyle.width = `calc(100% - ${positionLeft}px)`;
|
||||
positionLeft = 0;
|
||||
}
|
||||
if (positionTop > 0) {
|
||||
markStyle.top = `${positionTop}px`;
|
||||
markStyle.height = `calc(100% - ${positionTop}px)`;
|
||||
positionTop = 0;
|
||||
}
|
||||
markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`;
|
||||
|
||||
return markStyle;
|
||||
};
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const watermarkRef = useRef<HTMLDivElement>();
|
||||
const { createObserver, destroyObserver } = useMutationObserver();
|
||||
|
||||
const destroyWatermark = () => {
|
||||
if (watermarkRef.current) {
|
||||
watermarkRef.current.remove();
|
||||
watermarkRef.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const reRendering = (mutation: MutationRecord) => {
|
||||
let flag = false;
|
||||
// Whether to delete the watermark node
|
||||
if (mutation.removedNodes.length) {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node === watermarkRef.current) {
|
||||
flag = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Whether the watermark dom property value has been modified
|
||||
if (mutation.type === 'attributes' && mutation.target === watermarkRef.current) {
|
||||
flag = true;
|
||||
}
|
||||
return flag;
|
||||
};
|
||||
|
||||
const appendWatermark = (base64Url: string, markWidth: number) => {
|
||||
if (containerRef.current && watermarkRef.current) {
|
||||
destroyObserver();
|
||||
watermarkRef.current.setAttribute(
|
||||
'style',
|
||||
getStyleStr({
|
||||
...getMarkStyle(markWidth),
|
||||
backgroundImage: `url('${base64Url}')`,
|
||||
}),
|
||||
);
|
||||
containerRef.current?.append(watermarkRef.current);
|
||||
createObserver(containerRef.current, (mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (reRendering(mutation)) {
|
||||
destroyWatermark();
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
renderWatermark();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the width and height of the watermark. The default values are as follows
|
||||
* Image: [120, 64]; Content: It's calculated by content;
|
||||
*/
|
||||
const getMarkSize = (ctx: CanvasRenderingContext2D) => {
|
||||
let defaultWidth = 120;
|
||||
let defaultHeight = 64;
|
||||
if (!image && ctx.measureText) {
|
||||
ctx.font = `${Number(fontSize)}px ${fontFamily}`;
|
||||
const contents = Array.isArray(content) ? content : [content];
|
||||
const widths = contents.map((item) => ctx.measureText(item as string).width);
|
||||
defaultWidth = Math.ceil(Math.max(...widths));
|
||||
defaultHeight = Number(fontSize) * contents.length + (contents.length - 1) * FontGap;
|
||||
}
|
||||
return [width ?? defaultWidth, height ?? defaultHeight];
|
||||
};
|
||||
|
||||
const renderWatermark = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (ctx) {
|
||||
if (!watermarkRef.current) {
|
||||
watermarkRef.current = document.createElement('div');
|
||||
}
|
||||
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const [markWidth, markHeight] = getMarkSize(ctx);
|
||||
const canvasWidth = `${(gapX + markWidth) * ratio}px`;
|
||||
const canvasHeight = `${(gapY + markHeight) * ratio}px`;
|
||||
canvas.setAttribute('width', canvasWidth);
|
||||
canvas.setAttribute('height', canvasHeight);
|
||||
|
||||
const mergedMarkWidth = markWidth * ratio;
|
||||
const mergedMarkHeight = markHeight * ratio;
|
||||
const mergedGapXCenter = (gapX * ratio) / 2;
|
||||
const mergedGapYCenter = (gapY * ratio) / 2;
|
||||
|
||||
/** Rotate with the canvas as the center point */
|
||||
const centerX = (mergedMarkWidth + gapX * ratio) / 2;
|
||||
const centerY = (mergedMarkHeight + gapY * ratio) / 2;
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate((Math.PI / 180) * Number(rotate));
|
||||
ctx.translate(-centerX, -centerY);
|
||||
|
||||
if (image) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, mergedGapXCenter, mergedGapYCenter, mergedMarkWidth, mergedMarkHeight);
|
||||
appendWatermark(canvas.toDataURL(), markWidth);
|
||||
};
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = image;
|
||||
} else {
|
||||
const mergedFontSize = Number(fontSize) * ratio;
|
||||
ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${mergedMarkHeight}px ${fontFamily}`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.translate(mergedMarkWidth / 2, 0);
|
||||
if (Array.isArray(content)) {
|
||||
content?.forEach((item: string, index: number) =>
|
||||
ctx.fillText(
|
||||
item,
|
||||
mergedGapXCenter,
|
||||
mergedGapYCenter + index * (mergedFontSize + FontGap * ratio),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ctx.fillText(content ?? '', mergedGapXCenter, mergedGapYCenter);
|
||||
}
|
||||
appendWatermark(canvas.toDataURL(), markWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
renderWatermark();
|
||||
}, [
|
||||
rotate,
|
||||
zIndex,
|
||||
width,
|
||||
height,
|
||||
image,
|
||||
content,
|
||||
color,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fontStyle,
|
||||
fontFamily,
|
||||
gapX,
|
||||
gapY,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
...style,
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Watermark;
|
51
components/watermark/index.zh-CN.md
Normal file
51
components/watermark/index.zh-CN.md
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
category: Components
|
||||
subtitle: 水印
|
||||
group: 其他
|
||||
title: Watermark
|
||||
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original
|
||||
demo:
|
||||
cols: 1
|
||||
---
|
||||
|
||||
给页面的某个区域加上水印。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 页面需要添加水印标识版权时使用。
|
||||
- 适用于防止信息盗用。
|
||||
|
||||
## 代码演示
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<code src="./demo/basic.tsx">基本</code>
|
||||
<code src="./demo/multi-line.tsx">多行水印</code>
|
||||
<code src="./demo/image.tsx">图片水印</code>
|
||||
<code src="./demo/custom.tsx">自定义配置</code>
|
||||
|
||||
## API
|
||||
|
||||
### Watermark
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| width | 水印的宽度,`content` 的默认值为自身的宽度 | number | 120 | |
|
||||
| height | 水印的高度,`content` 的默认值为自身的高度 | number | 64 | |
|
||||
| rotate | 水印绘制时,旋转的角度,单位 `°` | number | -22 | |
|
||||
| zIndex | 追加的水印元素的 z-index | number | 9 | |
|
||||
| image | 图片源,建议导出 2 倍或 3 倍图,优先级高 | string | - | |
|
||||
| content | 水印文字内容 | string \| string[] | - | |
|
||||
| font | 文字样式 | [Font](#Font) | [Font](#Font) | |
|
||||
| gap | 水印之间的间距 | \[number, number\] | \[200, 200\] | |
|
||||
| offset | 水印距离容器左上角的偏移量,默认为 `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | |
|
||||
|
||||
### Font
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| color | 字体颜色 | string | rgba(0,0,0,.15) | |
|
||||
| fontSize | 字体大小 | number | 16 | |
|
||||
| fontWeight | 字体粗细 | `normal` \| `light` \| `weight` \| number | normal | |
|
||||
| fontFamily | 字体类型 | string | sans-serif | |
|
||||
| fontStyle | 字体样式 | `none` \| `normal` \| `italic` \| `oblique` | normal | |
|
32
components/watermark/useMutationObserver.ts
Normal file
32
components/watermark/useMutationObserver.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function useMutationObserver() {
|
||||
const instance = useRef<MutationObserver>();
|
||||
|
||||
const destroyObserver = () => {
|
||||
if (instance.current) {
|
||||
instance.current.takeRecords();
|
||||
instance.current.disconnect();
|
||||
instance.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const createObserver = (target: Node, callback: MutationCallback) => {
|
||||
if (MutationObserver) {
|
||||
destroyObserver();
|
||||
instance.current = new MutationObserver(callback);
|
||||
instance.current.observe(target, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributeFilter: ['style', 'class'],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => () => destroyObserver(), []);
|
||||
|
||||
return {
|
||||
createObserver,
|
||||
destroyObserver,
|
||||
};
|
||||
}
|
@ -243,6 +243,7 @@
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-axe": "^7.0.0",
|
||||
"jest-canvas-mock": "^2.4.0",
|
||||
"jest-environment-jsdom": "^29.0.1",
|
||||
"jest-environment-node": "^29.0.0",
|
||||
"jest-image-snapshot": "^6.0.0",
|
||||
@ -312,7 +313,7 @@
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"limit": "381 KiB"
|
||||
"limit": "382 KiB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/antd-with-locales.min.js",
|
||||
@ -322,7 +323,7 @@
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"maxSize": "381 kB"
|
||||
"maxSize": "382 kB"
|
||||
}
|
||||
],
|
||||
"tnpm": {
|
||||
|
@ -67,6 +67,7 @@ exports[`antd dist files exports modules correctly 1`] = `
|
||||
"TreeSelect",
|
||||
"Typography",
|
||||
"Upload",
|
||||
"Watermark",
|
||||
"message",
|
||||
"notification",
|
||||
"theme",
|
||||
|
Loading…
Reference in New Issue
Block a user