feat(watermark): new component (#2341)

* feat(watermark): new component

Signed-off-by: Sepush <sepush@outlook.com>

* changelog

* fix(watermark): render

* feat(watermark): add selectable prop&test case

* refactor(watermark): use useThemeVars
This commit is contained in:
Sepush 2022-02-14 00:58:34 +08:00 committed by GitHub
parent 3351152634
commit 2af9ccb047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 440 additions and 2 deletions

View File

@ -32,6 +32,10 @@
- `n-progress` adds `gap-offset-degree` prop.
- `n-select` adds `clear-filter-after-select` prop, closes [#2352](https://github.com/TuSimple/naive-ui/issues/2352).
### Feats
- `n-watermark` add new component, closes [#1745](https://github.com/TuSimple/naive-ui/issues/1745).
### i18n
- Add plPL locale, closes [#2354](https://github.com/TuSimple/naive-ui/issues/2354).

View File

@ -32,6 +32,10 @@
- `n-progress` 新增 `gap-offset-degree` 属性
- `n-select` 新增 `clear-filter-after-select` 属性,关闭 [#2352](https://github.com/TuSimple/naive-ui/issues/2352)
### Feats
- `n-watermark` 新增组件,关闭 [#1745](https://github.com/TuSimple/naive-ui/issues/1745)
### i18n
- 新增 plPL locale关闭 [#2354](https://github.com/TuSimple/naive-ui/issues/2354)

View File

@ -498,6 +498,11 @@ export const enComponentRoutes = [
path: 'number-animation',
component: () =>
import('../../src/number-animation/demos/enUS/index.demo-entry.md')
},
{
path: 'watermark',
component: () =>
import('../../src/watermark/demos/enUS/index.demo-entry.md')
}
]
@ -857,6 +862,11 @@ export const zhComponentRoutes = [
path: 'number-animation',
component: () =>
import('../../src/number-animation/demos/zhCN/index.demo-entry.md')
},
{
path: 'watermark',
component: () =>
import('../../src/watermark/demos/zhCN/index.demo-entry.md')
}
]

View File

@ -260,6 +260,12 @@ export function createComponentMenuOptions ({ lang, theme, mode }) {
zh: '排印',
enSuffix: true,
path: '/typography'
},
{
en: 'Watermark',
zh: '水印',
enSuffix: true,
path: '/watermark'
}
]
}),

View File

@ -81,3 +81,4 @@ export * from './tree'
export * from './tree-select'
export * from './typography'
export * from './upload'
export * from './watermark'

View File

@ -74,6 +74,7 @@ import type { TreeTheme } from '../../tree/styles'
import type { TreeSelectTheme } from '../../tree-select/styles'
import type { TypographyTheme } from '../../typography/styles'
import type { UploadTheme } from '../../upload/styles'
import type { WatermarkTheme } from '../../watermark/styles'
import type { InternalSelectMenuTheme } from '../../_internal/select-menu/styles'
import type { InternalSelectionTheme } from '../../_internal/selection/styles'
import type { NDateLocale, NLocale } from '../../locales'
@ -164,6 +165,7 @@ export interface GlobalThemeWithoutCommon {
TreeSelect?: TreeSelectTheme
Typography?: TypographyTheme
Upload?: UploadTheme
Watermark?: WatermarkTheme
// internal
InternalSelectMenu?: InternalSelectMenuTheme
InternalSelection?: InternalSelectionTheme

View File

@ -65,6 +65,7 @@ export { treeDark } from './tree/styles'
export { treeSelectDark } from './tree-select/styles'
export { typographyDark } from './typography/styles'
export { uploadDark } from './upload/styles'
export { watermarkDark } from './watermark/styles'
// danger zone, internal styles
export { scrollbarDark } from './_internal/scrollbar/styles'

View File

@ -74,6 +74,7 @@ import { treeSelectDark } from '../tree-select/styles'
import { typographyDark } from '../typography/styles'
import { treeDark } from '../tree/styles'
import { uploadDark } from '../upload/styles'
import { watermarkDark } from '../watermark/styles'
import type { BuiltInGlobalTheme } from './interface'
export const darkTheme: BuiltInGlobalTheme = {
@ -153,5 +154,6 @@ export const darkTheme: BuiltInGlobalTheme = {
Tree: treeDark,
TreeSelect: treeSelectDark,
Typography: typographyDark,
Upload: uploadDark
Upload: uploadDark,
Watermark: watermarkDark
}

View File

@ -76,6 +76,7 @@ import { typographyLight } from '../typography/styles'
import { treeLight } from '../tree/styles'
import { treeSelectLight } from '../tree-select/styles'
import { uploadLight } from '../upload/styles'
import { watermarkLight } from '../watermark/styles'
import type { BuiltInGlobalTheme } from './interface'
export const lightTheme: BuiltInGlobalTheme = {
@ -155,5 +156,6 @@ export const lightTheme: BuiltInGlobalTheme = {
Tree: treeLight,
TreeSelect: treeSelectLight,
Typography: typographyLight,
Upload: uploadLight
Upload: uploadLight,
Watermark: watermarkLight
}

View File

@ -0,0 +1,56 @@
<markdown>
# Basic
</markdown>
<template>
<n-watermark content="watermark" :gap-x="5" :gap-y="70">
<n-table :bordered="false" :single-line="false">
<thead>
<tr>
<th>...</th>
<th>...</th>
<th>...</th>
<th>...</th>
<th>......</th>
</tr>
</thead>
<tbody>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>......</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</n-table>
</n-watermark>
</template>

View File

@ -0,0 +1,35 @@
# Watermark
Watermark
## Demos
```demo
basic.vue
```
## API
### Watermark Props
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| width | `number` | `120` | width | NEXT_VERSION |
| height | `number` | `64` | height | NEXT_VERSION |
| z-index | `number` | `10` | z-index | NEXT_VERSION |
| gap-x | `number` | `212` | gap of x | NEXT_VERSION |
| gap-y | `number` | `222` | gap of y | NEXT_VERSION |
| offset-top | `number` | - | offset top | NEXT_VERSION |
| offset-left | `number` | - | offset left | NEXT_VERSION |
| rotate | `number` | `-22` | rotation angle | NEXT_VERSION |
| image | `string` | - | src of a watermark image | NEXT_VERSION |
| content | `string` | - | content of watermark | NEXT_VERSION |
| font-color | string | `rgba(0,0,0,.15)` | font color | NEXT_VERSION |
| font-style | `'normal' \| 'italic' \| 'oblique' \| number` | `normal` | font style | NEXT_VERSION |
| selectable | `boolean` | `true` | whether the watermark is selectable | NEXT_VERSION |
### Watermark Slots
| Name | Parameters | Description | Version |
| ------- | ---------- | ------------------------ | ------------ |
| default | `()` | something with watermark | NEXT_VERSION |

View File

@ -0,0 +1,35 @@
<markdown>
# 基本用法
</markdown>
<template>
<n-watermark content="水印" :gap-x="10" :gap-y="50">
<n-table :bordered="false" :single-line="false">
<thead>
<tr>
<th>不问</th>
<th>不能</th>
<th>不明白</th>
<th>...</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>机密</td>
<td>禁止</td>
<td>外传</td>
<td>...</td>
<td>给司外势力递刀子</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</n-table>
</n-watermark>
</template>

View File

@ -0,0 +1,35 @@
# 水印 Watermark
留下一点痕迹
## 演示
```demo
basic.vue
```
## API
### Watermark Props
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| width | `number` | `120` | 宽度 | NEXT_VERSION |
| height | `number` | `64` | 高度 | NEXT_VERSION |
| z-index | `number` | `10` | z 轴 | NEXT_VERSION |
| gap-x | `number` | `212` | x 轴间隔 | NEXT_VERSION |
| gap-y | `number` | `222` | y 轴间隔 | NEXT_VERSION |
| offset-top | `number` | - | 上边距 | NEXT_VERSION |
| offset-left | `number` | - | 左边距 | NEXT_VERSION |
| rotate | `number` | `-22` | 旋转角度 | NEXT_VERSION |
| image | `string` | - | 图片路径 | NEXT_VERSION |
| content | `string` | - | 文字内容 | NEXT_VERSION |
| font-color | string | `rgba(0,0,0,.15)` | 字体颜色 | NEXT_VERSION |
| font-style | `'normal' \| 'italic' \| 'oblique' \| number` | `normal` | 字体风格 | NEXT_VERSION |
| selectable | `boolean` | `true` | 水印覆盖的内容是否可选中 | NEXT_VERSION |
### Watermark Slots
| 名称 | 参数 | 说明 | 版本 |
| ------- | ---- | ---- | ------------ |
| default | `()` | 内容 | NEXT_VERSION |

2
src/watermark/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { default as NWatermark } from './src/Watermark'
export type { WatermarkProps } from './src/Watermark'

View File

@ -0,0 +1,147 @@
import { h, defineComponent, PropType, ref } from 'vue'
import { useConfig, useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { ExtractPublicPropTypes, warnOnce } from '../../_utils'
import { useThemeVars } from '../../composables/index'
import { watermarkLight, WatermarkTheme } from '../styles'
import style from './styles/index.cssr'
function getRatio (context: any): number {
if (!context) {
return 1
}
const backingStore =
context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio ||
1
return (window.devicePixelRatio || 1) / backingStore
}
const watermarkProps = {
...(useTheme.props as ThemeProps<WatermarkTheme>),
width: {
type: Number,
default: 120
},
height: {
type: Number,
default: 64
},
zIndex: {
type: Number,
default: 10
},
gapX: {
type: Number,
default: 212
},
gapY: {
type: Number,
default: 222
},
offsetTop: Number,
offsetLeft: Number,
rotate: {
type: Number,
default: -22
},
image: String,
content: String,
fontColor: {
type: String,
default: 'rgba(0,0,0,.15)'
},
fontStyle: {
type: [String, Number] as PropType<
'normal' | 'italic' | 'oblique' | number
>,
default: 'normal'
},
selectable: {
type: Boolean,
default: true
}
} as const
export type WatermarkProps = ExtractPublicPropTypes<typeof watermarkProps>
export default defineComponent({
name: 'Watermark',
props: watermarkProps,
setup (props, { slots }) {
const { mergedClsPrefixRef } = useConfig(props)
const {
gapX,
gapY,
zIndex,
width,
height,
offsetTop,
offsetLeft,
rotate,
image,
content,
fontColor,
fontStyle,
selectable
} = props
const base64UrlRef = ref('')
const themeVars = useThemeVars().value
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const ratio = getRatio(ctx)
const canvasWidth = (gapX + width) * ratio
const canvasHeight = (gapY + height) * ratio
const canvasOffsetLeft = offsetLeft || gapX / 2
const canvasOffsetTop = offsetTop || gapY / 2
canvas.width = canvasWidth
canvas.height = canvasHeight
if (ctx) {
ctx.translate(canvasOffsetLeft * ratio, canvasOffsetTop * ratio)
ctx.rotate(rotate * (Math.PI / 180))
const markWidth = width * ratio
const markHeight = height * ratio
if (image) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.referrerPolicy = 'no-referrer'
img.src = image
img.onload = () => {
ctx.drawImage(img, 0, 0, markWidth, markHeight)
base64UrlRef.value = canvas.toDataURL()
}
} else if (content) {
const markSize = parseInt(themeVars.fontSizeHuge, 10) * ratio
ctx.font = `${fontStyle} normal ${themeVars.fontWeight} ${markSize}px/${markHeight} ${themeVars.fontFamily}`
ctx.fillStyle = fontColor
ctx.fillText(content, 0, 0)
base64UrlRef.value = canvas.toDataURL()
}
} else {
warnOnce('Watermark:', 'Canvas is not supported in this browser.')
}
useTheme('Watermark', '-watermark', style, watermarkLight, props)
return () => (
<div
class={[
`${mergedClsPrefixRef.value}-watermark-container`,
!selectable && `${mergedClsPrefixRef.value}-watermark--selectable`
]}
>
{slots.default?.()}
<div
class={`${mergedClsPrefixRef.value}-watermark`}
style={{
zIndex: zIndex,
backgroundSize: `${gapX + width}px`,
backgroundImage: `url(${base64UrlRef.value})`
}}
></div>
</div>
)
}
})

View File

@ -0,0 +1,18 @@
import { c, cB, cM } from '../../../_utils/cssr'
export default c([
cB('watermark-container', `
position: relative;
`, [
cB('watermark', `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-repeat: repeat;
`)
]),
cM('watermark--selectable', 'user-select: none;')
])

View File

@ -0,0 +1,9 @@
import { commonDark } from '../../_styles/common'
import type { WatermarkTheme } from './light'
const watermarkDark: WatermarkTheme = {
name: 'Watermark',
common: commonDark
}
export default watermarkDark

View File

@ -0,0 +1,3 @@
export { default as watermarkDark } from './dark'
export { default as watermarkLight } from './light'
export type { WatermarkTheme } from './light'

View File

@ -0,0 +1,10 @@
import { commonLight } from '../../_styles/common'
import { createTheme } from '../../_mixins'
const watermarkLight = createTheme({
name: 'Watermark',
common: commonLight
})
export default watermarkLight
export type WatermarkTheme = typeof watermarkLight

View File

@ -0,0 +1,39 @@
import { mount } from '@vue/test-utils'
import { NWatermark } from '../index'
import 'jest-canvas-mock'
describe('NWatermark', () => {
it('should work with import on demand', () => {
mount(NWatermark)
})
it('should work with `z-index` prop', () => {
const wrapper = mount(NWatermark, {
props: {
zIndex: 9
}
})
expect(wrapper.find('.n-watermark').exists()).toBe(true)
expect(wrapper.find('.n-watermark').attributes('style')).toContain(
'z-index: 9'
)
})
it('should work with `gap-x` & `width` props', () => {
const wrapper = mount(NWatermark, {
props: {
gapX: 10,
width: 100
}
})
expect(wrapper.find('.n-watermark').attributes('style')).toContain(
'background-size: 110px'
)
})
it('should work with `selectable` prop', () => {
const wrapper = mount(NWatermark, {
props: {
selectable: false
}
})
expect(wrapper.find('.n-watermark--selectable').exists()).toBe(true)
})
})

View File

@ -0,0 +1,16 @@
import { h, createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { setup } from '@css-render/vue3-ssr'
import { NWatermark } from '../..'
describe('SSR', () => {
it('works', async () => {
const app = createSSRApp(() => <NWatermark />)
setup(app)
try {
await renderToString(app)
} catch (e) {
expect(e).not.toBeTruthy()
}
})
})

1
volar.d.ts vendored
View File

@ -139,6 +139,7 @@ declare module 'vue' {
NCol: typeof import('naive-ui')['NCol']
NRow: typeof import('naive-ui')['NRow']
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
NWatermark: typeof import('naive-ui')['NWatermark']
[key: string]: any
}
}