From 72206c0a3cbe259a8a742a0eb526544ced5bf95e Mon Sep 17 00:00:00 2001 From: samhou1988 Date: Wed, 22 Jun 2022 10:34:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20watermark=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=20(#921)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(watermark): add watermark component watermark component * style(watermark): remove unuse code * refactor(watermark): import style * feat(watermark): watermark CR --- examples/watermark/demos/base.vue | 11 +++ examples/watermark/demos/graylevel.vue | 15 +++ examples/watermark/demos/image.vue | 15 +++ examples/watermark/demos/movingImage.vue | 12 +++ examples/watermark/demos/movingText.vue | 10 ++ examples/watermark/demos/multiline.vue | 16 +++ examples/watermark/usage/index.vue | 35 +++++++ examples/watermark/usage/props.json | 20 ++++ examples/watermark/watermark.md | 40 ++++++++ script/generate-usage/config.js | 16 +++ site/site.config.mjs | 7 ++ src/components.ts | 1 + src/watermark/hooks.ts | 75 ++++++++++++++ src/watermark/index.ts | 7 ++ src/watermark/props.ts | 79 +++++++++++++++ src/watermark/type.ts | 117 ++++++++++++++++++++++ src/watermark/watermark.tsx | 120 +++++++++++++++++++++++ test/snap/__snapshots__/csr.test.js.snap | 96 ++++++++++++++++++ test/snap/__snapshots__/ssr.test.js.snap | 12 +++ test/unit/watermark/index.test.jsx | 17 ++++ 20 files changed, 721 insertions(+) create mode 100644 examples/watermark/demos/base.vue create mode 100644 examples/watermark/demos/graylevel.vue create mode 100644 examples/watermark/demos/image.vue create mode 100644 examples/watermark/demos/movingImage.vue create mode 100644 examples/watermark/demos/movingText.vue create mode 100644 examples/watermark/demos/multiline.vue create mode 100644 examples/watermark/usage/index.vue create mode 100644 examples/watermark/usage/props.json create mode 100644 examples/watermark/watermark.md create mode 100644 src/watermark/hooks.ts create mode 100644 src/watermark/index.ts create mode 100644 src/watermark/props.ts create mode 100644 src/watermark/type.ts create mode 100644 src/watermark/watermark.tsx create mode 100644 test/unit/watermark/index.test.jsx diff --git a/examples/watermark/demos/base.vue b/examples/watermark/demos/base.vue new file mode 100644 index 0000000000..a31a620be2 --- /dev/null +++ b/examples/watermark/demos/base.vue @@ -0,0 +1,11 @@ + diff --git a/examples/watermark/demos/graylevel.vue b/examples/watermark/demos/graylevel.vue new file mode 100644 index 0000000000..e3c8832922 --- /dev/null +++ b/examples/watermark/demos/graylevel.vue @@ -0,0 +1,15 @@ + diff --git a/examples/watermark/demos/image.vue b/examples/watermark/demos/image.vue new file mode 100644 index 0000000000..5c97e17c19 --- /dev/null +++ b/examples/watermark/demos/image.vue @@ -0,0 +1,15 @@ + diff --git a/examples/watermark/demos/movingImage.vue b/examples/watermark/demos/movingImage.vue new file mode 100644 index 0000000000..5def289beb --- /dev/null +++ b/examples/watermark/demos/movingImage.vue @@ -0,0 +1,12 @@ + diff --git a/examples/watermark/demos/movingText.vue b/examples/watermark/demos/movingText.vue new file mode 100644 index 0000000000..72485014eb --- /dev/null +++ b/examples/watermark/demos/movingText.vue @@ -0,0 +1,10 @@ + diff --git a/examples/watermark/demos/multiline.vue b/examples/watermark/demos/multiline.vue new file mode 100644 index 0000000000..bb656d2a14 --- /dev/null +++ b/examples/watermark/demos/multiline.vue @@ -0,0 +1,16 @@ + diff --git a/examples/watermark/usage/index.vue b/examples/watermark/usage/index.vue new file mode 100644 index 0000000000..aba4ff1c20 --- /dev/null +++ b/examples/watermark/usage/index.vue @@ -0,0 +1,35 @@ + + + + diff --git a/examples/watermark/usage/props.json b/examples/watermark/usage/props.json new file mode 100644 index 0000000000..a1390018b1 --- /dev/null +++ b/examples/watermark/usage/props.json @@ -0,0 +1,20 @@ +[ + { + "name": "removable", + "type": "Boolean", + "defaultValue": true, + "options": [] + }, + { + "name": "movable", + "type": "Boolean", + "defaultValue": false, + "options": [] + }, + { + "name": "isRepeat", + "type": "Boolean", + "defaultValue": true, + "options": [] + } +] diff --git a/examples/watermark/watermark.md b/examples/watermark/watermark.md new file mode 100644 index 0000000000..12fde43e39 --- /dev/null +++ b/examples/watermark/watermark.md @@ -0,0 +1,40 @@ +:: BASE_DOC :: + +## API + +### Watermark Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +alpha | Number | 1 | 水印整体透明度,取值范围 [0-1] | N +content | String / Slot / Function | - | 水印所覆盖的内容节点。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +default | String / Slot / Function | - | 水印所覆盖的内容节点,同 `content`。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +height | Number | - | 水印高度 | N +isRepeat | Boolean | true | 水印是否重复出现 | N +lineSpace | Number | 16 | 行间距,只作用在多行(`content` 配置为数组)情况下 | N +movable | Boolean | false | 水印是否可移动 | N +moveInterval | Number | 3000 | 水印发生运动位移的间隙,单位:毫秒 | N +offset | Array | - | 水印在画布上绘制的水平和垂直偏移量,正常情况下水印绘制在中间位置,即 `offset = [gapX / 2, gapY / 2]`。TS 类型:`Array` | N +removable | Boolean | true | 水印是否可被删除,默认会开启水印节点防删 | N +rotate | Number | -22 | 水印旋转的角度,单位 ° | N +watermarkContent | Object / Array | - | 水印内容,需要显示多行情况下可配置为数组。TS 类型:`WatermarkText|WatermarkImage|Array` | N +width | Number | - | 水印宽度 | N +x | Number | - | 水印之间的水平间距 | N +y | Number | - | 水印之间的垂直间距 | N +zIndex | Number | - | 水印元素的 `z-index`,默认值写在 CSS 中 | N + +### WatermarkText + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +fontColor | String | rgba(0,0,0,0.1) | 水印文本文字颜色 | N +fontSize | Number | 16 | 水印文本文字大小 | N +fontWeight | String | normal | 水印文本文字粗细。可选项:normal/lighter/bold/bolder | N +text | String | - | 水印文本内容 | N + +### WatermarkImage + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +isGrayscale | Boolean | false | 水印图片是否需要灰阶显示 | N +url | String | - | 水印图片源地址,为了显示清楚,建议导出 2 倍或 3 倍图 | N diff --git a/script/generate-usage/config.js b/script/generate-usage/config.js index fb10afd6fe..e56da3e3a9 100644 --- a/script/generate-usage/config.js +++ b/script/generate-usage/config.js @@ -541,6 +541,22 @@ module.exports = { `, }, }, + watermark: { + panelStr: `const panelList = [{label: 'watermark', value: 'watermark'}];`, + render: { + watermark: ` +
+ `, + }, + }, + dialog: { panelStr: `const panelList = [{label: 'dialog', value: 'dialog'}];`, script: ` diff --git a/site/site.config.mjs b/site/site.config.mjs index 15ccb0579f..df1a2cec94 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -382,6 +382,13 @@ export default { path: '/vue-next/components/tree', component: () => import('@/examples/tree/tree.md'), }, + { + title: 'Watermark 水印', + name: 'watermark', + docType: 'data', + path: '/vue-next/components/watermark', + component: () => import('@/examples/watermark/watermark.md'), + }, ], }, { diff --git a/src/components.ts b/src/components.ts index 944be48933..3a26f3253f 100644 --- a/src/components.ts +++ b/src/components.ts @@ -54,6 +54,7 @@ export * from './tag'; export * from './tooltip'; export * from './tree'; export * from './collapse'; +export * from './watermark'; // 消息提醒 diff --git a/src/watermark/hooks.ts b/src/watermark/hooks.ts new file mode 100644 index 0000000000..6247bd9509 --- /dev/null +++ b/src/watermark/hooks.ts @@ -0,0 +1,75 @@ +import type { ComponentPublicInstance, Ref } from 'vue'; +import { unref, watch, getCurrentScope, onScopeDispose } from 'vue'; + +export const defaultWindow = typeof window !== 'undefined' ? window : undefined; +export interface ConfigurableWindow { + window?: Window; +} +// eslint-disable-next-line no-undef +export interface MutationObserverOptions extends MutationObserverInit, ConfigurableWindow {} +export type MaybeRef = T | Ref; +export type VueInstance = ComponentPublicInstance; +export type MaybeElementRef = MaybeRef; +export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null; +export type UnRefElementReturn = T extends VueInstance + ? Exclude + : T | undefined; + +export type Fn = () => void; + +export function unrefElement(elRef: MaybeElementRef): UnRefElementReturn { + const plain = unref(elRef); + return (plain as VueInstance)?.$el ?? plain; +} +export function tryOnScopeDispose(fn: Fn) { + if (getCurrentScope()) { + onScopeDispose(fn); + return true; + } + return false; +} + +export function useMutationObserver( + target: MaybeElementRef, + // eslint-disable-next-line no-undef + callback: MutationCallback, + options: MutationObserverOptions = {}, +) { + const { window = defaultWindow, ...mutationOptions } = options; + let observer: MutationObserver | undefined; + const isSupported = window && 'MutationObserver' in window; + + const cleanup = () => { + if (observer) { + observer.disconnect(); + observer = undefined; + } + }; + + const stopWatch = watch( + () => unrefElement(target), + (el) => { + cleanup(); + + if (isSupported && 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; diff --git a/src/watermark/index.ts b/src/watermark/index.ts new file mode 100644 index 0000000000..9902bc8e09 --- /dev/null +++ b/src/watermark/index.ts @@ -0,0 +1,7 @@ +import _Watermark from './watermark'; +import withInstall from '../utils/withInstall'; + +export * from './type'; + +export const Watermark = withInstall(_Watermark); +export default Watermark; diff --git a/src/watermark/props.ts b/src/watermark/props.ts new file mode 100644 index 0000000000..c73c18af93 --- /dev/null +++ b/src/watermark/props.ts @@ -0,0 +1,79 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdWatermarkProps } from './type'; +import { PropType } from 'vue'; + +export default { + /** 水印整体透明度,取值范围 [0-1] */ + alpha: { + type: Number, + default: 1, + }, + /** 水印所覆盖的内容节点 */ + content: { + type: [String, Function] as PropType, + }, + /** 水印所覆盖的内容节点,同 `content` */ + default: { + type: [String, Function] as PropType, + }, + /** 水印高度 */ + height: { + type: Number, + }, + /** 水印是否重复出现 */ + isRepeat: { + type: Boolean, + default: true, + }, + /** 行间距,只作用在多行(`content` 配置为数组)情况下 */ + lineSpace: { + type: Number, + default: 16, + }, + /** 水印是否可移动 */ + movable: Boolean, + /** 水印发生运动位移的间隙,单位:毫秒 */ + moveInterval: { + type: Number, + default: 3000, + }, + /** 水印在画布上绘制的水平和垂直偏移量,正常情况下水印绘制在中间位置,即 `offset = [gapX / 2, gapY / 2]` */ + offset: { + type: Array as PropType, + }, + /** 水印是否可被删除,默认会开启水印节点防删 */ + removable: { + type: Boolean, + default: true, + }, + /** 水印旋转的角度,单位 ° */ + rotate: { + type: Number, + default: -22, + }, + /** 水印内容,需要显示多行情况下可配置为数组 */ + watermarkContent: { + type: [Object, Array] as PropType, + }, + /** 水印宽度 */ + width: { + type: Number, + }, + /** 水印之间的水平间距 */ + x: { + type: Number, + }, + /** 水印之间的垂直间距 */ + y: { + type: Number, + }, + /** 水印元素的 `z-index`,默认值写在 CSS 中 */ + zIndex: { + type: Number, + }, +}; diff --git a/src/watermark/type.ts b/src/watermark/type.ts new file mode 100644 index 0000000000..abfefdf711 --- /dev/null +++ b/src/watermark/type.ts @@ -0,0 +1,117 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TNode } from '../common'; + +export interface TdWatermarkProps { + /** + * 水印整体透明度,取值范围 [0-1] + * @default 1 + */ + alpha?: number; + /** + * 水印所覆盖的内容节点 + */ + content?: string | TNode; + /** + * 水印所覆盖的内容节点,同 `content` + */ + default?: string | TNode; + /** + * 水印高度 + */ + height?: number; + /** + * 水印是否重复出现 + * @default true + */ + isRepeat?: boolean; + /** + * 行间距,只作用在多行(`content` 配置为数组)情况下 + * @default 16 + */ + lineSpace?: number; + /** + * 水印是否可移动 + * @default false + */ + movable?: boolean; + /** + * 水印发生运动位移的间隙,单位:毫秒 + * @default 3000 + */ + moveInterval?: number; + /** + * 水印在画布上绘制的水平和垂直偏移量,正常情况下水印绘制在中间位置,即 `offset = [gapX / 2, gapY / 2]` + */ + offset?: Array; + /** + * 水印是否可被删除,默认会开启水印节点防删 + * @default true + */ + removable?: boolean; + /** + * 水印旋转的角度,单位 ° + * @default -22 + */ + rotate?: number; + /** + * 水印内容,需要显示多行情况下可配置为数组 + */ + watermarkContent?: WatermarkText | WatermarkImage | Array; + /** + * 水印宽度 + */ + width?: number; + /** + * 水印之间的水平间距 + */ + x?: number; + /** + * 水印之间的垂直间距 + */ + y?: number; + /** + * 水印元素的 `z-index`,默认值写在 CSS 中 + */ + zIndex?: number; +} + +export interface WatermarkText { + /** + * 水印文本文字颜色 + * @default rgba(0,0,0,0.1) + */ + fontColor?: string; + /** + * 水印文本文字大小 + * @default 16 + */ + fontSize?: number; + /** + * 水印文本文字粗细 + * @default normal + */ + fontWeight?: 'normal' | 'lighter' | 'bold' | 'bolder'; + /** + * 水印文本内容 + * @default '' + */ + text?: string; +} + +export interface WatermarkImage { + /** + * 水印图片是否需要灰阶显示 + * @default false + */ + isGrayscale?: boolean; + /** + * 水印图片源地址,为了显示清楚,建议导出 2 倍或 3 倍图 + * @default '' + */ + url?: string; +} diff --git a/src/watermark/watermark.tsx b/src/watermark/watermark.tsx new file mode 100644 index 0000000000..2b0c79e8e2 --- /dev/null +++ b/src/watermark/watermark.tsx @@ -0,0 +1,120 @@ +import { computed, onMounted, defineComponent, h, VNode, ref, reactive } from 'vue'; +import props from './props'; +import generateBase64Url from '../_common/js/watermark/generateBase64Url'; +import randomMovingStyle from '../_common/js/watermark/randomMovingStyle'; +import injectStyle from '../_common/js/utils/injectStyle'; +import { usePrefixClass } from '../hooks/useConfig'; +import { useMutationObserver } from './hooks'; +import { useContent } from '../hooks/tnode'; + +export default defineComponent({ + name: 'TWatermark', + props, + setup(props, { slots }) { + const COMPONENT_NAME = usePrefixClass('watermark'); + const backgroundImage = ref(''); + const watermarkRef = ref(); + const parent = ref(); + const renderContent = useContent(); + + const x = ref(props.x || 200); + const y = ref(props.y || 210); + const width = ref(props.width || 120); + const height = ref(props.height || 60); + const offset = reactive(props.offset || []); + const zIndex = ref(props.zIndex || 10); + + const gapX = computed(() => { + return props.movable ? 0 : x.value; + }); + + const gapY = computed(() => { + return props.movable ? 0 : y.value; + }); + + const rotate = computed(() => { + return props.movable ? 0 : props.rotate; + }); + + const backgroundRepeat = computed(() => { + if (props.movable) { + return 'no-repeat'; + } + return props.isRepeat ? 'repeat' : 'no-repeat'; + }); + + const offsetLeft = computed(() => { + return offset[0] || gapX.value / 2; + }); + + const offsetTop = computed(() => { + return offset[1] || gapY.value / 2; + }); + + useMutationObserver( + watermarkRef, + (mutations) => { + if (props.removable) return; + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + const removeNodes = mutation.removedNodes; + removeNodes.forEach((node) => { + watermarkRef.value.appendChild(node); + }); + } + }); + }, + { attributes: true, childList: true, characterData: true, subtree: true }, + ); + + onMounted(() => { + generateBase64Url( + { + width: width.value, + height: height.value, + rotate: rotate.value, + lineSpace: props.lineSpace, + alpha: props.alpha, + gapX: gapX.value, + gapY: gapY.value, + watermarkContent: props.watermarkContent, + offsetLeft: offsetLeft.value, + offsetTop: offsetTop.value, + }, + (base64Url) => { + backgroundImage.value = base64Url; + }, + ); + parent.value = watermarkRef.value.parentElement; + const keyframesStyle = randomMovingStyle(); + injectStyle(keyframesStyle); + }); + + return () => ( +
+ {renderContent('default', 'content')} +
+
+ ); + }, +}); diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap index 5708605d5b..ff8dede129 100644 --- a/test/snap/__snapshots__/csr.test.js.snap +++ b/test/snap/__snapshots__/csr.test.js.snap @@ -143682,3 +143682,99 @@ exports[`csr snapshot test > csr test ./examples/upload/demos/table.vue 1`] = `
`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/base.vue 1`] = ` +
+ +
+ +
+
+`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/graylevel.vue 1`] = ` +
+ +
+ +
+
+`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/image.vue 1`] = ` +
+ +
+ +
+
+`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/movingImage.vue 1`] = ` +
+ +
+ +
+
+`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/movingText.vue 1`] = ` +
+ +
+ +
+
+`; + +exports[`csr snapshot test > csr test ./examples/watermark/demos/multiline.vue 1`] = ` +
+ +
+ +
+
+`; diff --git a/test/snap/__snapshots__/ssr.test.js.snap b/test/snap/__snapshots__/ssr.test.js.snap index 70094a8100..8aefbc88dd 100644 --- a/test/snap/__snapshots__/ssr.test.js.snap +++ b/test/snap/__snapshots__/ssr.test.js.snap @@ -1165,3 +1165,15 @@ exports[`ssr snapshot test > ssr test ./examples/upload/demos/single-custom.vue exports[`ssr snapshot test > ssr test ./examples/upload/demos/single-input.vue 1`] = `"
未选择文件
"`; exports[`ssr snapshot test > ssr test ./examples/upload/demos/table.vue 1`] = `"
文件名
大小
状态
操作

点击上方“选择文件”或将文件拖拽到此区域

"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/base.vue 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/graylevel.vue 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/image.vue 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/movingImage.vue 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/movingText.vue 1`] = `"
"`; + +exports[`ssr snapshot test > ssr test ./examples/watermark/demos/multiline.vue 1`] = `"
"`; diff --git a/test/unit/watermark/index.test.jsx b/test/unit/watermark/index.test.jsx new file mode 100644 index 0000000000..dde3ad595d --- /dev/null +++ b/test/unit/watermark/index.test.jsx @@ -0,0 +1,17 @@ +import { mount } from '@vue/test-utils'; +import Watermark from '@/src/watermark/index.ts'; + +// every component needs four parts: props/events/slots/functions. +describe('Watermark', () => { + // test props api + describe(':props', () => { + it('', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + expect(wrapper.exists()).toBe(true); + }); + }); +});