mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-02-11 13:10:26 +08:00
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:
parent
3351152634
commit
2af9ccb047
@ -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).
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -260,6 +260,12 @@ export function createComponentMenuOptions ({ lang, theme, mode }) {
|
||||
zh: '排印',
|
||||
enSuffix: true,
|
||||
path: '/typography'
|
||||
},
|
||||
{
|
||||
en: 'Watermark',
|
||||
zh: '水印',
|
||||
enSuffix: true,
|
||||
path: '/watermark'
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
@ -81,3 +81,4 @@ export * from './tree'
|
||||
export * from './tree-select'
|
||||
export * from './typography'
|
||||
export * from './upload'
|
||||
export * from './watermark'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
56
src/watermark/demos/enUS/basic.demo.vue
Normal file
56
src/watermark/demos/enUS/basic.demo.vue
Normal 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>
|
35
src/watermark/demos/enUS/index.demo-entry.md
Normal file
35
src/watermark/demos/enUS/index.demo-entry.md
Normal 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 |
|
35
src/watermark/demos/zhCN/basic.demo.vue
Normal file
35
src/watermark/demos/zhCN/basic.demo.vue
Normal 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>
|
35
src/watermark/demos/zhCN/index.demo-entry.md
Normal file
35
src/watermark/demos/zhCN/index.demo-entry.md
Normal 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
2
src/watermark/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as NWatermark } from './src/Watermark'
|
||||
export type { WatermarkProps } from './src/Watermark'
|
147
src/watermark/src/Watermark.tsx
Normal file
147
src/watermark/src/Watermark.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
})
|
18
src/watermark/src/styles/index.cssr.ts
Normal file
18
src/watermark/src/styles/index.cssr.ts
Normal 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;')
|
||||
])
|
9
src/watermark/styles/dark.ts
Normal file
9
src/watermark/styles/dark.ts
Normal 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
|
3
src/watermark/styles/index.ts
Normal file
3
src/watermark/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as watermarkDark } from './dark'
|
||||
export { default as watermarkLight } from './light'
|
||||
export type { WatermarkTheme } from './light'
|
10
src/watermark/styles/light.ts
Normal file
10
src/watermark/styles/light.ts
Normal 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
|
39
src/watermark/tests/Watermark.spec.ts
Normal file
39
src/watermark/tests/Watermark.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
16
src/watermark/tests/server.spec.tsx
Normal file
16
src/watermark/tests/server.spec.tsx
Normal 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
1
volar.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user