feat(Watermark): add new Watermark component (#11721)

* feat(watermark): add watermark component

* docs(watermark): i18n

* feat(watermark): revoke useless url

* feat(watermark): add opacity, replace fullPage default to true, replace Zindex default to 100, replace fontColor default to #dedee0

* test(watermark): add watermark snap test

* docs(watermark): fix opacity and default value
This commit is contained in:
Cyrbuzz 2023-04-05 21:06:20 +08:00 committed by GitHub
parent cac003cef5
commit 69b113717f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 834 additions and 0 deletions

View File

@ -0,0 +1,119 @@
# Watermark
### Intro
Add watermark for page.
### Install
Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways.
```js
import { createApp } from 'vue';
import { Watermark } from 'vant';
const app = createApp();
app.use(Watermark);
```
## Usage
### Basic Usage
```html
<!-- text watermark -->
<van-watermark content="Vant" />
<!-- image watermark -->
<van-watermark image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" />
```
### Custom Gap
Use `gapX` `gapY` attributes to control the gap between two watermark slice.
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
:gap-x="20"
:gap-y="10"
/>
```
### Custom Opacity
Use `opacity` attribute to control the entirety opacity.
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
:opacity="0.5"
/>
```
### Custom Rotate
Use `rotate` attribute to control the rotate of watermark. Default value is `-22`.
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
rotate="22"
/>
```
### Display Range
Use the `fullPage` attribute to control the display range of the watermark.
```html
<van-watermark
:full-page="true"
content="vant watermark"
font-color="rgba(0, 0, 0, 0.15)"
>
</van-watermark>
```
### HTML Watermark
Use the `default slot` to pass HTML directly. Inline styles are supported, and self-closing tags are not supported.
```html
<van-watermark :width="150">
<div style="background: linear-gradient(45deg, #000 0, #000 50%, #fff 50%)">
<p style="mix-blend-mode: difference; color: #fff">Vant watermark</p>
</div>
</van-watermark>
```
## API
### Props
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| width | Watermark width | _number_ | 100 |
| height | Watermark height | _number_ | 100 |
| zIndex | Watermark's z-index | _number_ | 100 |
| content | Text watermark content | _string_ | - |
| image | Image watermark content. If `content` and `image` are passed at the same time, use the `image` watermark first | _string_ | - |
| fullPage | Whether to display the watermark in full screen | _boolean_ | true |
| gapX | Horizontal spacing between watermarks | _number_ | 0 |
| gapY | Vertical spacing between watermarks | _number_ | 0 |
| fontColor | Color of text watermark | _string_ | #dcdee0 |
| opacity | opacity of watermark | _number_ | 1 |
### Slots
| Attribute | Description |
| --- | --- |
| default | Content of HTML watermark. Inline styles are supported, and self-closing tags are not supported. This slot is invalid if `content` or `image` is passed |
### Types
The component exports the following type definitions:
```ts
import type { WaterProps } from 'vant';
```

View File

@ -0,0 +1,119 @@
# Watermark 水印
### 介绍
页面上添加特定的文字或图案,可用于防止信息盗用
### 引入
通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。
```js
import { createApp } from 'vue';
import { Watermark } from 'vant';
const app = createApp();
app.use(Watermark);
```
## 代码演示
### 基础用法
```html
<!-- 文字水印 -->
<van-watermark content="Vant" />
<!-- 图片水印 -->
<van-watermark image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" />
```
### 自定义间隔
通过 `gapX` `gapY` 属性来控制重复水印之间的间隔。
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
:gap-x="20"
:gap-y="10"
/>
```
### 自定义透明度
通过 `opacity` 属性来控制水印的整体透明度。
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
:opacity="0.5"
/>
```
### 自定义倾斜角度
通过 `rotate` 属性来控制水印的倾斜角度,默认值为`-22`
```html
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
rotate="22"
/>
```
### 显示范围
通过 `fullPage` 属性来控制水印的显示范围。
```html
<van-watermark
:full-page="true"
content="vant watermark"
font-color="rgba(0, 0, 0, 0.15)"
>
</van-watermark>
```
### HTML 水印
通过默认插槽可以直接传入 HTMLHTML 样式仅支持行内样式同时不支持传入自闭合标签。
```html
<van-watermark :width="150">
<div style="background: linear-gradient(45deg, #000 0, #000 50%, #fff 50%)">
<p style="mix-blend-mode: difference; color: #fff">Vant watermark</p>
</div>
</van-watermark>
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| width | 水印宽度 | _number_ | 100 |
| height | 水印高度 | _number_ | 100 |
| zIndex | 水印的 z-index | _number_ | 100 |
| content | 文字水印的内容 | _string_ | - |
| image | 图片水印的内容,如果与 content 同时传入,优先使用图片水印 | _string_ | - |
| fullPage | 水印是否全屏显示 | _boolean_ | false |
| gapX | 水印水平间隔 | _number_ | 0 |
| gapY | 水印垂直间隔 | _number_ | 0 |
| fontColor | 文字水印的颜色 | _string_ | #dcdee0 |
| opacity | 水印的透明度 | _number_ | 1 |
### Slots
| 名称 | 说明 |
| --- | --- |
| default | HTML 水印的内容,仅支持行内样式同时不支持传入自闭合标签,存在 content 或 image 时此插槽无效 |
### 类型定义
组件导出以下类型定义:
```ts
import type { WaterProps } from 'vant';
```

View File

@ -0,0 +1,184 @@
import {
defineComponent,
nextTick,
onUnmounted,
ref,
watch,
watchEffect,
type ExtractPropTypes,
} from 'vue';
import {
createNamespace,
makeNumberProp,
makeNumericProp,
makeStringProp,
truthProp,
} from '../utils';
const [name, bem] = createNamespace('watermark');
export const watermarkProps = {
width: makeNumberProp(100),
height: makeNumberProp(100),
rotate: makeNumericProp(-22),
zIndex: makeNumberProp(100),
content: String,
image: String,
fullPage: truthProp,
gapX: makeNumberProp(0),
gapY: makeNumberProp(0),
fontColor: makeStringProp('#dcdee0'),
opacity: makeNumberProp(1),
};
export type WatermarkProps = ExtractPropTypes<typeof watermarkProps>;
export default defineComponent({
name,
props: watermarkProps,
setup(props, { slots }) {
const svgElRef = ref<HTMLDivElement>();
const watermarkUrl = ref('');
const imageBase64 = ref('');
const renderWatermark = () => {
const svgInner = () => {
if (props.image) {
return (
<image
href={imageBase64.value}
x="0"
y="0"
width={props.width}
height={props.height}
style={{
transformOrigin: 'center',
transform: `rotate(${props.rotate}deg)`,
}}
></image>
);
}
return (
<foreignObject x="0" y="0" width={props.width} height={props.height}>
<div
// @ts-ignore
xmlns="http://www.w3.org/1999/xhtml"
style={{
transform: `rotate(${props.rotate}deg)`,
}}
>
{props.content ? (
<span
style={{
color: props.fontColor,
}}
>
{props.content}
</span>
) : (
slots?.default?.()
)}
</div>
</foreignObject>
);
};
const svgWidth = props.width + props.gapX;
const svgHeight = props.height + props.gapY;
return (
<svg
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
width={svgWidth}
height={svgHeight}
xmlns="http://www.w3.org/2000/svg"
style={{
padding: `0 ${props.gapX}px ${props.gapY}px 0`,
opacity: props.opacity,
}}
>
{svgInner()}
</svg>
);
};
const makeImageToBase64 = (url: string) => {
const canvas = document.createElement('canvas');
const image = new Image();
image.crossOrigin = 'anonymous';
image.referrerPolicy = 'no-referrer';
image.onload = () => {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx?.drawImage(image, 0, 0);
imageBase64.value = canvas.toDataURL();
};
image.src = url;
};
const makeSvgToBlobUrl = (svgStr: string) => {
const svgBlob = new Blob([svgStr], {
type: 'image/svg+xml;charset=utf-8',
});
return URL.createObjectURL(svgBlob);
};
watchEffect(() => {
if (props.image) {
makeImageToBase64(props.image);
}
});
watch(
() => [
imageBase64.value,
props.content,
props.fontColor,
props.height,
props.width,
props.rotate,
props.gapX,
props.gapY,
],
() => {
// 路径为 renderWatermark渲染的实际HTML => SVG字符串转换为blob图片 => 放到background-image中。
nextTick(() => {
if (svgElRef.value) {
if (watermarkUrl.value) {
URL.revokeObjectURL(watermarkUrl.value);
}
watermarkUrl.value = makeSvgToBlobUrl(svgElRef.value.innerHTML);
}
});
},
{
immediate: true,
}
);
onUnmounted(() => {
if (watermarkUrl.value) {
URL.revokeObjectURL(watermarkUrl.value);
}
});
return () => (
<div
class={bem()}
style={{
position: props.fullPage ? 'fixed' : 'absolute',
backgroundImage: `url(${watermarkUrl.value})`,
zIndex: props.zIndex,
}}
>
<div style={{ display: 'none' }} ref={svgElRef}>
{renderWatermark()}
</div>
</div>
);
},
});

View File

@ -0,0 +1,141 @@
<script setup lang="ts">
import { ref } from 'vue';
import VanButton from '../../button';
import VanWatermark from '..';
import { useTranslate } from '../../../docs/site';
const t = useTranslate({
'zh-CN': {
customOpacity: '自定义透明度',
customGap: '自定义间隔',
customImage: '自定义图片',
customRotate: '自定义倾斜角度',
displayRange: '显示范围',
htmlWatermark: 'HTML 水印',
textWatermark: '文字水印',
imageWatermark: '图片水印',
switch: '切换',
},
'en-US': {
customOpacity: 'Custom opacity',
customGap: 'Custom Gap',
customRotate: 'Custom Rotate',
displayRange: 'Display Range',
htmlWatermark: 'HTML Watermark',
textWatermark: 'Text Watermark',
imageWatermark: 'Image Watermark',
switch: 'Swtich',
},
});
const baseWatermarkFlag = ref<'text' | 'image'>('text');
const fullPage = ref(false);
</script>
<template>
<demo-block :title="t('basicUsage')">
<div class="demo-watermark-wrapper">
<div style="position: relative; z-index: 9999">
<van-button
@click="
() => {
baseWatermarkFlag = 'text';
}
"
>{{ t('textWatermark') }}</van-button
>
<van-button
@click="
() => {
baseWatermarkFlag = 'image';
}
"
style="margin: 0 var(--van-padding-md)"
>{{ t('imageWatermark') }}</van-button
>
</div>
<van-watermark
content="Vant"
v-if="baseWatermarkFlag === 'text'"
:full-page="false"
></van-watermark>
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
v-if="baseWatermarkFlag === 'image'"
:full-page="false"
></van-watermark>
</div>
</demo-block>
<demo-block :title="t('customGap')">
<div class="demo-watermark-wrapper">
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
:gap-x="20"
:gap-y="10"
:full-page="false"
/>
</div>
</demo-block>
<demo-block :title="t('customOpacity')">
<div class="demo-watermark-wrapper">
<van-watermark
:full-page="false"
:opacity="0.5"
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
>
</van-watermark>
</div>
</demo-block>
<demo-block :title="t('customRotate')">
<div class="demo-watermark-wrapper">
<van-watermark
image="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
rotate="22"
:full-page="false"
/>
</div>
</demo-block>
<demo-block :title="t('displayRange')">
<div class="demo-watermark-wrapper">
<van-button
@click="
() => {
fullPage = !fullPage;
}
"
>
{{ t('switch') }}
</van-button>
<van-watermark
:full-page="fullPage"
content="vant watermark"
font-color="rgba(0, 0, 0, 0.15)"
>
</van-watermark>
</div>
</demo-block>
<demo-block :title="t('htmlWatermark')">
<div class="demo-watermark-wrapper">
<van-watermark :width="150" :full-page="false">
<div
style="background: linear-gradient(45deg, #000 0, #000 50%, #fff 50%)"
>
<p style="mix-blend-mode: difference; color: #fff">Vant watermark</p>
</div>
</van-watermark>
</div>
</demo-block>
</template>
<style lang="less">
.demo-watermark-wrapper {
position: relative;
height: 150px;
background-color: var(--van-background-2);
padding: var(--van-padding-md);
}
</style>

View File

@ -0,0 +1,8 @@
.van-watermark {
background-repeat: repeat;
height: 100%;
width: 100%;
left: 0;
top: 0;
pointer-events: none;
}

View File

@ -0,0 +1,13 @@
import { withInstall } from '../utils';
import _Watermark from './Watermark';
export const Watermark = withInstall(_Watermark);
export default Watermark;
export { watermarkProps } from './Watermark';
export type { WatermarkProps } from './Watermark';
declare module 'vue' {
export interface GlobalComponents {
VanWatermark: typeof Watermark;
}
}

View File

@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`watermark should render content 1`] = `
<div class="van-watermark"
style="position: fixed; background-image: url(); z-index: 100;"
>
<div style="display: none;">
<svg viewbox="0 0 100 100"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg"
style="padding: 0px 0px 0px 0px; opacity: 1;"
>
<foreignObject x="0"
y="0"
width="100"
height="100"
>
<div xmlns="http://www.w3.org/1999/xhtml"
style="transform: rotate(-22deg);"
>
<span style="color: red;">
Vant
</span>
</div>
</foreignObject>
</svg>
</div>
</div>
`;
exports[`watermark should render html 1`] = `
<div class="van-watermark"
style="position: fixed; background-image: url(); z-index: 100;"
>
<div style="display: none;">
<svg viewbox="0 0 100 100"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg"
style="padding: 0px 0px 0px 0px; opacity: 1;"
>
<foreignObject x="0"
y="0"
width="100"
height="100"
>
<div xmlns="http://www.w3.org/1999/xhtml"
style="transform: rotate(-22deg);"
>
vant watermark test
</div>
</foreignObject>
</svg>
</div>
</div>
`;
exports[`watermark should render image 1`] = `
<div class="van-watermark"
style="position: fixed; background-image: url(); z-index: 100;"
>
<div style="display: none;">
<svg viewbox="0 0 100 100"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg"
style="padding: 0px 0px 0px 0px; opacity: 0.5;"
>
<image href="base64Url"
x="0"
y="0"
width="100"
height="100"
style="transform-origin: center; transform: rotate(-22deg);"
>
</image>
</svg>
</div>
</div>
`;
exports[`watermark test false value fullPage 1`] = `
<div class="van-watermark"
style="position: absolute; background-image: url(); z-index: 100;"
>
<div style="display: none;">
<svg viewbox="0 0 100 100"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg"
style="padding: 0px 0px 0px 0px; opacity: 1;"
>
<foreignObject x="0"
y="0"
width="100"
height="100"
>
<div xmlns="http://www.w3.org/1999/xhtml"
style="transform: rotate(-22deg);"
>
vant watermark test
</div>
</foreignObject>
</svg>
</div>
</div>
`;
exports[`watermark test width, height, rotate, zIndex 1`] = `
<div class="van-watermark"
style="position: fixed; background-image: url(); z-index: 200;"
>
<div style="display: none;">
<svg viewbox="0 0 20 20"
width="20"
height="20"
xmlns="http://www.w3.org/2000/svg"
style="padding: 0px 0px 0px 0px; opacity: 1;"
>
<foreignObject x="0"
y="0"
width="20"
height="20"
>
<div xmlns="http://www.w3.org/1999/xhtml"
style="transform: rotate(20deg);"
>
vant watermark test
</div>
</foreignObject>
</svg>
</div>
</div>
`;

View File

@ -0,0 +1,107 @@
// @ts-nocheck
import { Watermark } from '..';
import { mount } from '../../../test';
describe('watermark', () => {
beforeEach(() => {
const createElement = document.createElement.bind(document);
document.createElement = (tagName: string) => {
if (tagName === 'canvas') {
return {
...createElement(tagName),
getContext: () => {
() => {};
},
toDataURL: () => 'base64Url',
};
}
return createElement(tagName);
};
global.URL.createObjectURL = jest.fn(() => 'run to here');
global.Image = class {
crossOrigin = 'anonymous';
referrerPolicy = 'no-referrer';
naturalWidth = 800;
naturalHeight = 550;
onload: () => void = () => {};
// just mock to trigge onload
_src = '';
get src() {
return this._src;
}
set src(val) {
this._src = val;
this.onload();
}
};
});
test('should render content', () => {
const wrapper = mount(Watermark, {
props: {
content: 'Vant',
fontColor: 'red',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render image', () => {
const wrapper = mount(Watermark, {
props: {
content: 'Vant',
image: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
opacity: 0.5,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render html', () => {
const wrapper = mount(Watermark, {
slots: {
default: () => 'vant watermark test',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('test width, height, rotate, zIndex', () => {
const wrapper = mount(Watermark, {
props: {
width: 20,
height: 20,
rotate: 20,
zIndex: 200,
},
slots: {
default: () => 'vant watermark test',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('test false value fullPage', () => {
const wrapper = mount(Watermark, {
props: {
fullPage: false,
},
slots: {
default: () => 'vant watermark test',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@ -344,6 +344,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'text-ellipsis',
title: 'TextEllipsis 文本省略',
},
{
path: 'watermark',
title: 'Watermark 水印',
},
],
},
{
@ -784,6 +788,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'text-ellipsis',
title: 'TextEllipsis',
},
{
path: 'watermark',
title: 'Watermark',
},
],
},
{