Skip to content

feat: add watermark #6300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions components/_util/hooks/_vueuse/useMutationObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { tryOnScopeDispose } from './tryOnScopeDispose';
import { watch } from 'vue';
import type { MaybeElementRef } from './unrefElement';
import { unrefElement } from './unrefElement';
import { useSupported } from './useSupported';
import type { ConfigurableWindow } from './_configurable';
import { defaultWindow } from './_configurable';

export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {}

/**
* Watch for changes being made to the DOM tree.
*
* @see https://vueuse.org/useMutationObserver
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN
* @param target
* @param callback
* @param options
*/
export function useMutationObserver(
target: MaybeElementRef,
callback: MutationCallback,
options: UseMutationObserverOptions = {},
) {
const { window = defaultWindow, ...mutationOptions } = options;
let observer: MutationObserver | undefined;
const isSupported = useSupported(() => window && 'MutationObserver' in window);

const cleanup = () => {
if (observer) {
observer.disconnect();
observer = undefined;
}
};

const stopWatch = watch(
() => unrefElement(target),
el => {
cleanup();

if (isSupported.value && window && el) {
observer = new MutationObserver(callback);
observer!.observe(el, mutationOptions);
}
},
{ immediate: true },
);

const stop = () => {
cleanup();
stopWatch();
};

tryOnScopeDispose(stop);

return {
isSupported,
stop,
};
}

export type UseMutationObserverReturn = ReturnType<typeof useMutationObserver>;
4 changes: 3 additions & 1 deletion components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export { default as Upload, UploadDragger } from './upload';

export { default as LocaleProvider } from './locale-provider';

export type { SegmentedProps } from './segmented';
export { default as Watermark } from './watermark';
export type { WatermarkProps } from './watermark';

export type { SegmentedProps } from './segmented';
export { default as Segmented } from './segmented';
3 changes: 3 additions & 0 deletions components/watermark/__tests__/demo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';

demoTest('watermark');
29 changes: 29 additions & 0 deletions components/watermark/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Watermark from '..';
import mountTest from '../../../tests/shared/mountTest';
import { mount } from '@vue/test-utils';

describe('Watermark', () => {
mountTest(Watermark);
const mockSrcSet = jest.spyOn(Image.prototype, 'src', 'set');
beforeAll(() => {
mockSrcSet.mockImplementation(function fn() {
this.onload?.();
});
});

afterAll(() => {
mockSrcSet.mockRestore();
});

it('The watermark should render successfully ', function () {
const wrapper = mount({
setup() {
return () => {
return <Watermark class="watermark" content="Ant Design" />;
};
},
});
expect(wrapper.find('.watermark').exists()).toBe(true);
expect(wrapper.html()).toMatchSnapshot();
});
});
23 changes: 23 additions & 0 deletions components/watermark/demo/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<docs>
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---

## zh-CN

最简单的用法。

## en-US

The most basic usage.

</docs>

<template>
<a-watermark content="Ant Design Vue">
<div style="height: 500px" />
</a-watermark>
</template>
118 changes: 118 additions & 0 deletions components/watermark/demo/custom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<docs>
---
order: 0
title:
zh-CN: 自定义配置
en-US: Custom
---

## zh-CN

通过自定义参数配置预览水印效果。

## en-US

Preview the watermark effect by configuring custom parameters.

</docs>

<template>
<div style="display: flex">
<a-watermark v-bind="model">
<a-typography>
<a-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.
</a-typography-paragraph>
<a-typography-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&apos;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.
</a-typography-paragraph>
<a-typography-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&apos; mental and physical resources and make human-computer interaction more
natural.
</a-typography-paragraph>
</a-typography>
<img
style="z-index: 10; width: 100%; max-width: 800px; position: relative"
src="https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*zx7LTI_ECSAAAAAAAAAAAABkARQnAQ"
alt="示例图片"
/>
</a-watermark>
<a-form
style="
width: 280px;
flex-shrink: 0;
border-left: 1px solid #eee;
padding-left: 20px;
margin-left: 20px;
"
layout="vertical"
:model="model"
>
<a-form-item name="content" label="Content">
<a-input v-model:value="model.content" />
</a-form-item>
<a-form-item name="font.fontSize" label="FontSize">
<a-slider v-model:value="model.font.fontSize" :step="1" :min="0" :max="100" />
</a-form-item>
<a-form-item name="zIndex" label="zIndex">
<a-slider v-model:value="model.zIndex" :step="1" :min="0" :max="100" />
</a-form-item>
<a-form-item name="rotate" label="Rotate">
<a-slider v-model:value="model.rotate" :step="1" :min="-180" :max="180" />
</a-form-item>
<a-form-item label="Gap" style="margin-bottom: 0">
<a-space style="display: flex" align="baseline">
<a-form-item :name="['gap', 0]">
<a-input-number v-model:value="model.gap[0]" placeholder="gapX" />
</a-form-item>
<a-form-item :name="['gap', 1]">
<a-input-number v-model:value="model.gap[1]" placeholder="gapY" />
</a-form-item>
</a-space>
</a-form-item>
<a-form-item label="Offset" style="margin-bottom: 0">
<a-space style="display: flex" align="baseline">
<a-form-item :name="['offset', 0]">
<a-input-number v-model:value="model.offset[0]" placeholder="offsetLeft" />
</a-form-item>
<a-form-item :name="['offset', 1]">
<a-input-number v-model:value="model.offset[1]" placeholder="offsetTop" />
</a-form-item>
</a-space>
</a-form-item>
</a-form>
</div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
export default defineComponent({
setup() {
const model = reactive({
content: 'Ant Design Vue',
font: {
fontSize: 16,
},
zIndex: 11,
rotate: -22,
gap: [100, 100] as [number, number],
offset: [],
});
return {
model,
};
},
});
</script>
27 changes: 27 additions & 0 deletions components/watermark/demo/image.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<docs>
---
order: 0
title:
zh-CN: 图片水印
en-US: Image watermark
---

## 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.

</docs>

<template>
<a-watermark
:height="30"
:width="130"
image="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*lkAoRbywo0oAAAAAAAAAAAAADrJ8AQ/original"
>
<div style="height: 500px" />
</a-watermark>
</template>
28 changes: 28 additions & 0 deletions components/watermark/demo/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<demo-sort :cols="1">
<basic />
<multi-line />
<watermark-image />
<custom />
</demo-sort>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import Basic from './basic.vue';
import MultiLine from './multi-line.vue';
import WatermarkImage from './image.vue';
import Custom from './custom.vue';
export default defineComponent({
CN,
US,
components: {
Basic,
MultiLine,
WatermarkImage,
Custom,
},
});
</script>
23 changes: 23 additions & 0 deletions components/watermark/demo/multi-line.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<docs>
---
order: 0
title:
zh-CN: 多行水印
en-US: Multi-line watermark
---

## zh-CN

通过 `content` 设置 字符串数组 指定多行文字水印内容。

## en-US

Use 'content' to set a string array to specify multi-line text watermark content.

</docs>

<template>
<a-watermark :content="['Ant Design Vue', 'Happy Working']">
<div style="height: 500px" />
</a-watermark>
</template>
41 changes: 41 additions & 0 deletions components/watermark/index.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
category: Components
type: Other
title: Watermark
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*duAQQbjHlHQAAAAAAAAAAAAADrJ8AQ/original
---

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.

## 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\] | \[100, 100\] | |
| 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 -->
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| 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 | |
Loading