diff --git a/packages/components/default.less b/packages/components/default.less index a05988052..0f35a0e2d 100644 --- a/packages/components/default.less +++ b/packages/components/default.less @@ -69,3 +69,4 @@ @import './tree-select/style/themes/default.less'; @import './typography/style/themes/default.less'; @import './upload/style/themes/default.less'; +@import './watermark/style/themes/default.less'; diff --git a/packages/components/index.ts b/packages/components/index.ts index 9009d8326..6a3dea9b0 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -67,6 +67,7 @@ import { IxTreeSelect } from '@idux/components/tree-select' import { IxTypography } from '@idux/components/typography' import { IxUpload, IxUploadFiles } from '@idux/components/upload' import { version } from '@idux/components/version' +import { IxWatermark } from '@idux/components/watermark' const components = [ IxAffix, @@ -161,6 +162,7 @@ const components = [ IxTreeSelect, IxUpload, IxUploadFiles, + IxWatermark, ] const directives: Record = { @@ -244,3 +246,4 @@ export * from '@idux/components/tree-select' export * from '@idux/components/typography' export * from '@idux/components/upload' export * from '@idux/components/version' +export * from '@idux/components/watermark' diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less index d55a7c845..bd5cce3f3 100644 --- a/packages/components/style/variable/prefix.less +++ b/packages/components/style/variable/prefix.less @@ -91,6 +91,7 @@ @stepper-prefix: ~'@{idux-prefix}-stepper'; @stepper-item-prefix: ~'@{idux-prefix}-stepper-item'; @affix-prefix: ~'@{idux-prefix}-affix'; +@watermark-prefix: ~'@{idux-prefix}-watermark'; // Private @checkable-list-prefix: ~'@{idux-prefix}-checkable-list'; diff --git a/packages/components/types.d.ts b/packages/components/types.d.ts index d10b85a3a..d3e8fc8c4 100644 --- a/packages/components/types.d.ts +++ b/packages/components/types.d.ts @@ -72,6 +72,7 @@ import type { TooltipComponent } from '@idux/components/tooltip' import type { TransferComponent } from '@idux/components/transfer' import type { TreeComponent } from '@idux/components/tree' import type { UploadComponent, UploadFilesComponent } from '@idux/components/upload' +import type { WatermarkComponent } from '@idux/components/watermark' declare module 'vue' { export interface GlobalComponents { @@ -164,6 +165,7 @@ declare module 'vue' { IxTree: TreeComponent IxUpload: UploadComponent IxUploadFiles: UploadFilesComponent + IxWatermark: WatermarkComponent } } diff --git a/packages/components/watermark/__tests__/__snapshots__/watermark.spec.ts.snap b/packages/components/watermark/__tests__/__snapshots__/watermark.spec.ts.snap new file mode 100644 index 000000000..9fc0b7cc4 --- /dev/null +++ b/packages/components/watermark/__tests__/__snapshots__/watermark.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1 + +exports[`Watermark > xxx work 1`] = ` +"
+ +
+
" +`; + +exports[`Watermark > xxx work 2`] = ` +"
+ +
+
" +`; diff --git a/packages/components/watermark/__tests__/watermark.spec.ts b/packages/components/watermark/__tests__/watermark.spec.ts new file mode 100644 index 000000000..ff3fb65af --- /dev/null +++ b/packages/components/watermark/__tests__/watermark.spec.ts @@ -0,0 +1,25 @@ +import { MountingOptions, mount } from '@vue/test-utils' + +import { renderWork } from '@tests' + +import Watermark from '../src/Watermark' +import { WatermarkProps } from '../src/types' + +describe('Watermark', () => { + const WatermarkMount = (options?: MountingOptions>) => + mount(Watermark, { ...(options as MountingOptions) }) + + renderWork(Watermark, { + props: {}, + }) + + test('watermark work', async () => { + const wrapper = WatermarkMount({ props: { content: 'Xxx' } }) + + expect(wrapper.html()).toMatchSnapshot() + + await wrapper.setProps({ xxx: 'Yyy' }) + + expect(wrapper.html()).toMatchSnapshot() + }) +}) diff --git a/packages/components/watermark/demo/Basic.md b/packages/components/watermark/demo/Basic.md new file mode 100644 index 000000000..0f110f3bc --- /dev/null +++ b/packages/components/watermark/demo/Basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh: 基本使用 + en: Basic usage +--- + +## zh + +最简单的用法。通过`content`传入水印内容,可传入数组作为多行文本水印。 + +## en + +The simplest usage. diff --git a/packages/components/watermark/demo/Basic.vue b/packages/components/watermark/demo/Basic.vue new file mode 100644 index 000000000..be3c2ef7f --- /dev/null +++ b/packages/components/watermark/demo/Basic.vue @@ -0,0 +1,26 @@ + diff --git a/packages/components/watermark/demo/Custom.md b/packages/components/watermark/demo/Custom.md new file mode 100644 index 000000000..93e12f415 --- /dev/null +++ b/packages/components/watermark/demo/Custom.md @@ -0,0 +1,14 @@ +--- +order: 3 +title: + zh: 自定义 + en: custom usage +--- + +## zh + +丰富的自定义用法,另有部分隐藏属性可提供更多样的控制 + +## en + +The custom usage. diff --git a/packages/components/watermark/demo/Custom.vue b/packages/components/watermark/demo/Custom.vue new file mode 100644 index 000000000..a1b8a6759 --- /dev/null +++ b/packages/components/watermark/demo/Custom.vue @@ -0,0 +1,13 @@ + diff --git a/packages/components/watermark/demo/HighDensity.md b/packages/components/watermark/demo/HighDensity.md new file mode 100644 index 000000000..18196e6b4 --- /dev/null +++ b/packages/components/watermark/demo/HighDensity.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh: 水印密度 + en: density usage +--- + +## zh + +可通过`density`参数对水印密度进行三档控制,分别为`low`、`mid`、`high` + +## en + +The simplest usage. diff --git a/packages/components/watermark/demo/HighDensity.vue b/packages/components/watermark/demo/HighDensity.vue new file mode 100644 index 000000000..08dbd5dd8 --- /dev/null +++ b/packages/components/watermark/demo/HighDensity.vue @@ -0,0 +1,108 @@ + + diff --git a/packages/components/watermark/demo/Image.md b/packages/components/watermark/demo/Image.md new file mode 100644 index 000000000..8e7846885 --- /dev/null +++ b/packages/components/watermark/demo/Image.md @@ -0,0 +1,14 @@ +--- +order: 2 +title: + zh: 图像水印 + en: image watermark +--- + +## zh + +通过`type`指定水印类型为`image`,并通过`content`图片地址。为保证图片高清且不被拉伸,请传入水印图片的宽高 width 和 height, 并上传至少两倍的宽高的 logo 图片地址。 + +## en + +The simplest usage. diff --git a/packages/components/watermark/demo/Image.vue b/packages/components/watermark/demo/Image.vue new file mode 100644 index 000000000..3ea0fcf39 --- /dev/null +++ b/packages/components/watermark/demo/Image.vue @@ -0,0 +1,11 @@ + diff --git a/packages/components/watermark/docs/Design.en.md b/packages/components/watermark/docs/Design.en.md new file mode 100644 index 000000000..d1e713d5e --- /dev/null +++ b/packages/components/watermark/docs/Design.en.md @@ -0,0 +1,3 @@ +## Description + +## Usage scenarios diff --git a/packages/components/watermark/docs/Design.zh.md b/packages/components/watermark/docs/Design.zh.md new file mode 100644 index 000000000..a960d4981 --- /dev/null +++ b/packages/components/watermark/docs/Design.zh.md @@ -0,0 +1,7 @@ +## 组件定义 + +提供了为页面区域加印水印的功能 + +## 使用场景 + +当页面需要添加水印标识版权、内部系统防泄密追责等时可使用,组件可提供便捷高可自定义的水印功能,且具备一定程度的水印防篡改能力 diff --git a/packages/components/watermark/docs/Index.en.md b/packages/components/watermark/docs/Index.en.md new file mode 100644 index 000000000..c4a6c4624 --- /dev/null +++ b/packages/components/watermark/docs/Index.en.md @@ -0,0 +1,29 @@ +--- +category: components +type: Other +order: 0 +title: Watermark +subtitle: +--- + +## API + +### IxWatermark + +#### WatermarkProps + +| Name | Description | Type | Default | Global Config | Remark | +| --- | --- | --- | --- | --- | --- | +| - | - | - | - | ✅ | - | + +#### WatermarkSlots + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | + +#### WatermarkMethods + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | diff --git a/packages/components/watermark/docs/Index.zh.md b/packages/components/watermark/docs/Index.zh.md new file mode 100644 index 000000000..b207a606d --- /dev/null +++ b/packages/components/watermark/docs/Index.zh.md @@ -0,0 +1,33 @@ +--- +category: components +type: 其他 +order: 0 +title: Watermark +subtitle: 水印 +--- + +## API + +### IxWatermark + +#### WatermarkProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `type` | 水印类型 | `'text' \| 'image'` | `'text'` | - | 不同的类型需要的`content`数据不同 | +| `content` | 水印内容 | `string \| string[]` | - | - | 根据type值传入相应内容:
`text`: 默认单行文本水印,支持多行文本,需传入字符串数组
`image`: 图像水印 为保证图片高清且不被拉伸,需同时传入水印图片的宽高 width 和 height,建议使用2倍或3倍图源 | +| `density` | 水印密度 | `'low'\|'mid'\|'high'` | `'low'` | - | 修改默认密度后,会根据输入的其它参数进行动态调整 | +| `rotate` | 水印旋转角度 | number | -22 | - | 单位:度 | +| `zIndex` | 水印层叠顺序 | number | 10 | - | - | +| `opacity` | 水印透明度 | number | 1 | - | 默认透明度已由fontColor控制 | +| `fontColor` | 字体颜色 | string | `rgba(0,0,0,.15)` | - | - | +| `fontSize` | 字体大小 | number | 16 | - | 优先级高于`font`配置,但`font`配置时需注意必须包含CSS `fontSize`属性 | +| `font` | 字体样式 | string | `16px sans-serif` | - | 可控制字体粗细、字体类别等细节,请注意尽量使用`px`作为字体大小单位,[具体配置参考 CSS font](https://developer.mozilla.org/docs/Web/CSS/font) | +| `width` | 水印宽度 | number | 120 | - | 一般与图片水印配置 | +|`height`| 水印高度 | number | 64 | - |一般与图片水印配置| + +#### WatermarkSlots + +| 名称 | 说明 | +| --- | --- | +| `default` | 需水印覆盖的区域内容 | diff --git a/packages/components/watermark/index.ts b/packages/components/watermark/index.ts new file mode 100644 index 000000000..7c224915f --- /dev/null +++ b/packages/components/watermark/index.ts @@ -0,0 +1,22 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { WatermarkComponent } from './src/types' + +import Watermark from './src/Watermark' + +const IxWatermark = Watermark as unknown as WatermarkComponent + +export { IxWatermark } + +export type { + WatermarkType, + WatermarkDensityType, + WatermarkInstance, + WatermarkComponent, + WatermarkPublicProps as WatermarkProps, +} from './src/types' diff --git a/packages/components/watermark/src/Watermark.tsx b/packages/components/watermark/src/Watermark.tsx new file mode 100644 index 000000000..56a242439 --- /dev/null +++ b/packages/components/watermark/src/Watermark.tsx @@ -0,0 +1,119 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, defineComponent, ref, watchEffect } from 'vue' + +import { isArray } from 'lodash-es' + +import { useGlobalConfig } from '@idux/components' + +import { watermarkProps } from './types' +import { calcDensity } from './utils' + +export default defineComponent({ + name: 'IxWatermark', + props: watermarkProps, + setup(props, { slots }) { + const common = useGlobalConfig('common') + const mergedPrefixCls = computed(() => `${common.prefixCls}-watermark`) + const base64Ref = ref('') + const rotate = computed(() => (Math.PI / 180) * props.rotate) + + // fix the problem of blurred drawing of canvas in high-definition screen + const ratio = Math.max(window.devicePixelRatio || 1, 2) + + const densityData = computed(() => + calcDensity( + props.density, + props.gapHorizontal, + props.gapVertical, + props.width, + props.height, + props.gapContent, + props.offsetLeft, + props.offsetTop, + ), + ) + const backgroundSize = computed(() => `${densityData.value.gapHorizontal + densityData.value.width}px`) + const canvasStyle = computed(() => { + return { + zIndex: props.zIndex, + backgroundSize: backgroundSize.value, + backgroundImage: `url(${base64Ref.value})`, + } + }) + + watchEffect(() => { + const canvas = document.createElement('canvas') + const { gapHorizontal, gapVertical, width, height, offsetLeft, offsetTop, gapContent } = densityData.value + + const canvasWidth = `${(gapHorizontal + width) * ratio}px` + const canvasHeight = `${(gapVertical + height) * ratio}px` + // default Center + const canvasOffsetLeft = offsetLeft || gapHorizontal / 2 + const canvasOffsetTop = offsetTop || gapVertical / 2 + + canvas.setAttribute('width', canvasWidth) + canvas.setAttribute('height', canvasHeight) + + const ctx = canvas.getContext('2d') + if (ctx) { + ctx.translate(canvasOffsetLeft * ratio, canvasOffsetTop * ratio) + ctx.rotate(rotate.value) + ctx.globalAlpha = props.opacity + + // mark content w&h + const markWidth = width * ratio + const markHeight = height * ratio + + const content = props.content + + if (props.type === 'image') { + const img = new Image() + img.crossOrigin = 'anonymous' + img.referrerPolicy = 'no-referrer' + img.src = content as string + img.onload = () => { + ctx.drawImage(img, 0, 0, markWidth, markHeight) + base64Ref.value = canvas.toDataURL() + } + } else if (content) { + ctx.fillStyle = props.fontColor + // ctx.textBaseline = 'top' + const markSize = props.fontSize * ratio + // pref: reduce too many param + // ctx.font = `${fontStyle} normal ${fontWeight} ${markSize}px/${markHeight}px ${fontFamily}` + ctx.font = props.font.replace(/\d*\.?\d+px/, `${markSize}px/${markHeight}px`) + if (isArray(content)) { + // multiline text + content.forEach((str, index) => { + ctx.fillText(str, 0, index * markSize + gapContent * index) + }) + } else { + // single + ctx.fillText(content, 0, 0) + } + base64Ref.value = canvas.toDataURL() + } + } else { + console.error('The current browser does not support canvas.') + } + }) + + return () => { + const prefixCls = mergedPrefixCls.value + const children = slots.default?.() + + return ( +
+ {children} +
+
+ ) + } + }, +}) diff --git a/packages/components/watermark/src/types.ts b/packages/components/watermark/src/types.ts new file mode 100644 index 000000000..b38ef4ba2 --- /dev/null +++ b/packages/components/watermark/src/types.ts @@ -0,0 +1,80 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils' +import type { DefineComponent, HTMLAttributes, PropType } from 'vue' + +export type WatermarkType = 'text' | 'image' +export type WatermarkDensityType = 'low' | 'mid' | 'high' +export const watermarkProps = { + /** + * 水印类型 + * + * * `text` : 文本水印,支持多行文本,需传入字符串数组 + * * `image`: 图像水印 为保证图片高清且不被拉伸,需同时传入水印图片的宽高 width 和 height,建议使用2倍或3倍图源 + */ + type: { type: String as PropType, default: 'text' }, + /** 水印内容 + * * 多行文本时请以数组形式输入 + */ + content: { type: String as PropType, default: undefined, required: true }, + /** 水印旋转角度 */ + rotate: { type: Number, default: -22 }, + /** 水印`z-index` */ + zIndex: { type: Number, default: 11 }, + /** 水印透明度 */ + opacity: { type: Number, default: 1 }, + /** + * 水印密度 + * * `low` 默认配置 + * * `mid` 中等密度 + * * `high` 高密度 + */ + density: { type: String as PropType, default: 'low' }, + /** 水印宽度 */ + width: { type: Number, default: 120 }, + /** 水印高度 */ + height: { type: Number, default: 64 }, + /** 字体颜色 */ + fontColor: { type: String, default: 'rgba(0,0,0,.15)' }, + /** 字体大小 */ + fontSize: { type: Number, default: 16 }, + /** + * 可控制字体粗细、字体类别等细节 + * + * @example + * font="oblique normal bold 16px sans-serif" + * @see https://developer.mozilla.org/docs/Web/CSS/font + * * 但请注意尽量使用`px`作为字体大小单位,同时fontSize的定义优先级高于font + */ + font: { type: String, default: '16px sans-serif' }, + + /** + * @private + */ + /** 水平方向的水印间距 */ + gapHorizontal: { type: Number, default: 212 }, + /** 垂直方向的水印间距 */ + gapVertical: { type: Number, default: 222 }, + /** 多行文本之间的行间距 */ + gapContent: { type: Number, default: 22 }, + /** 左偏移量 默认根据水印间距居中处理*/ + offsetLeft: { type: Number, default: undefined }, + /** 上偏移量 默认根据水印间距居中处理*/ + offsetTop: { type: Number, default: undefined }, + /** todo 一定程度上的防水印篡改 */ + strict: { type: Boolean, default: false }, +} as const + +export type WatermarkProps = ExtractInnerPropTypes +export type WatermarkPublicProps = ExtractPublicPropTypes< + Omit +> +export type WatermarkComponent = DefineComponent< + Omit & WatermarkPublicProps +> +export type WatermarkInstance = InstanceType> diff --git a/packages/components/watermark/src/utils/index.ts b/packages/components/watermark/src/utils/index.ts new file mode 100644 index 000000000..59f0acbd3 --- /dev/null +++ b/packages/components/watermark/src/utils/index.ts @@ -0,0 +1,60 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { WatermarkDensityType } from './../types' + +/** + * 根据传入密度对渲染所需参数进行计算 + * + * @returns 经密度二次计算的渲染参数 + */ +export function calcDensity( + density: WatermarkDensityType, + gapHorizontal: number, + gapVertical: number, + width: number, + height: number, + gapContent: number, + offsetLeft?: number, + offsetTop?: number, +): { + gapHorizontal: number + gapVertical: number + width: number + height: number + offsetLeft: number | undefined + offsetTop: number | undefined + gapContent: number +} { + switch (density) { + // low is default + + case 'mid': + width /= 2 + height /= 2 + gapVertical /= 1.5 + gapHorizontal /= 1.5 + gapContent /= 1.5 + break + case 'high': + width /= 2 + height /= 2 + gapVertical /= 3 + gapHorizontal /= 2.5 + gapContent /= 2 + break + } + return { + gapHorizontal: gapHorizontal, + gapVertical: gapVertical, + width: width, + height: height, + offsetLeft: offsetLeft, + offsetTop: offsetTop, + gapContent: gapContent, + } +} diff --git a/packages/components/watermark/style/index.less b/packages/components/watermark/style/index.less new file mode 100644 index 000000000..8f5467a45 --- /dev/null +++ b/packages/components/watermark/style/index.less @@ -0,0 +1,15 @@ +@import '../../style/mixins/reset.less'; + +.@{watermark-prefix} { + position: relative; + + &-canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + background-repeat: repeat; + } +} diff --git a/packages/components/watermark/style/themes/default.less b/packages/components/watermark/style/themes/default.less new file mode 100644 index 000000000..a66443560 --- /dev/null +++ b/packages/components/watermark/style/themes/default.less @@ -0,0 +1,4 @@ +@import '../../../style/themes/default.less'; +@import './default.variable.less'; + +@import '../index.less'; diff --git a/packages/components/watermark/style/themes/default.ts b/packages/components/watermark/style/themes/default.ts new file mode 100644 index 000000000..027ca3f89 --- /dev/null +++ b/packages/components/watermark/style/themes/default.ts @@ -0,0 +1,4 @@ +// style dependencies +import '@idux/components/style/core/default' + +import './default.less' diff --git a/packages/components/watermark/style/themes/default.variable.less b/packages/components/watermark/style/themes/default.variable.less new file mode 100644 index 000000000..e69de29bb diff --git a/packages/components/watermark/style/themes/seer.less b/packages/components/watermark/style/themes/seer.less new file mode 100644 index 000000000..5be7463c7 --- /dev/null +++ b/packages/components/watermark/style/themes/seer.less @@ -0,0 +1 @@ +@import '../../../style/themes/default.less'; diff --git a/packages/components/watermark/style/themes/seer.ts b/packages/components/watermark/style/themes/seer.ts new file mode 100644 index 000000000..80634d76c --- /dev/null +++ b/packages/components/watermark/style/themes/seer.ts @@ -0,0 +1,11 @@ +// style dependencies +import '@idux/components/_private/checkable-list/style/themes/seer' +import '@idux/components/style/core/seer' +import '@idux/components/checkbox/style/themes/seer' +import '@idux/components/empty/style/themes/seer' +import '@idux/components/icon/style/themes/seer' +import '@idux/components/pagination/style/themes/seer' +import '@idux/components/spin/style/themes/seer' +import '@idux/components/input/style/themes/seer' + +import './seer.less' diff --git a/packages/components/watermark/style/themes/seer.variable.less b/packages/components/watermark/style/themes/seer.variable.less new file mode 100644 index 000000000..498793af1 --- /dev/null +++ b/packages/components/watermark/style/themes/seer.variable.less @@ -0,0 +1 @@ +@import './default.variable.less';