2
0
mirror of https://github.com/tusen-ai/naive-ui.git synced 2025-04-18 14:50:56 +08:00

feat(n-qrcode): add n-qrcode component ()

* feat(n-qrcode): add `n-qrcode` component

* fix(n-qrcode): value desc when enUS

* fix(n-qrcode): docs error when enUS

* feat(n-qrcode): add n-padding

* fix: remove type as

* feat: switch watch to watchEffect

* feat(n-qrcode): add `color` `bg-color` props

* refactor: change to QR Code generator library

* feat: add size demo

* chore: remove border

* feat: add error-correction-level

* feat: add docs

* feat: add icon

* feat: add download demo

* fix: image scale

* fix: docs  error

* fix: docs error

---------

Co-authored-by: lijiaheng <lijiaheng@semi-tech.com>
Co-authored-by: 07akioni <07akioni2@gmail.com>
This commit is contained in:
jahnli 2023-12-14 01:21:14 +08:00 committed by GitHub
parent ea2603ed4b
commit cdc48c7a7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1906 additions and 0 deletions

@ -47,6 +47,7 @@ module.exports = {
}
},
rules: {
'@typescript-eslint/no-misused-promises': 0,
'@typescript-eslint/strict-boolean-expressions': 0,
'@typescript-eslint/prefer-nullish-coalescing': 0,
'@typescript-eslint/naming-convention': 0,
@ -88,6 +89,14 @@ module.exports = {
'@typescript-eslint/no-floating-promises': 0
}
},
{
files: ['qrcodegen.ts'],
rules: {
'@typescript-eslint/no-namespace': 0,
eqeqeq: 0,
'no-useless-escape': 0
}
},
{
files: '*',
globals: {

@ -37,6 +37,7 @@
- `n-popselect` adds `header` slot.
- `n-tree-select` adds `watch-props` prop.
- Adds `n-split` component, closes [#3557](https://github.com/tusen-ai/naive-ui/issues/3557).
- adds `n-qrcode` component, closes [#2535](https://github.com/tusen-ai/naive-ui/issues/2535)
## 2.35.0

@ -37,6 +37,7 @@
- `n-popselect` 新增 `header` 插槽
- `n-tree-select` 新增 `watch-props` 属性
- 新增 `n-split` 组件,关闭 [#3557](https://github.com/tusen-ai/naive-ui/issues/3557)
- 新增 `n-qrcode` 组件,关闭 [#2535](https://github.com/tusen-ai/naive-ui/issues/2535)
## 2.35.0

@ -535,6 +535,10 @@ export const enComponentRoutes = [
path: 'equation',
component: () => import('../../src/equation/demos/enUS/index.demo-entry.md')
},
{
path: 'qrcode',
component: () => import('../../src/qrcode/demos/enUS/index.demo-entry.md')
},
{
path: 'virtual-list',
component: () =>
@ -921,6 +925,10 @@ export const zhComponentRoutes = [
path: 'equation',
component: () => import('../../src/equation/demos/zhCN/index.demo-entry.md')
},
{
path: 'qrcode',
component: () => import('../../src/qrcode/demos/zhCN/index.demo-entry.md')
},
{
path: 'virtual-list',
component: () =>

@ -461,6 +461,12 @@ export function createComponentMenuOptions ({ lang, theme, mode }) {
enSuffix: true,
path: '/number-animation'
},
{
en: 'QRCode',
zh: '二维码',
enSuffix: true,
path: '/qrcode'
},
{
en: 'Statistic',
zh: '统计数据',

@ -59,6 +59,7 @@ export * from './popconfirm'
export * from './popover'
export * from './popselect'
export * from './progress'
export * from './qrcode'
export * from './radio'
export * from './rate'
export * from './result'

@ -53,6 +53,7 @@ import type { PopconfirmTheme } from '../../popconfirm/styles'
import type { PopoverTheme } from '../../popover/styles'
import type { PopselectTheme } from '../../popselect/styles'
import type { ProgressTheme } from '../../progress/styles'
import type { QrcodeTheme } from '../../qrcode/styles'
import type { RadioTheme } from '../../radio/styles'
import type { RateTheme } from '../../rate/styles'
import type { ResultTheme } from '../../result/styles'
@ -155,6 +156,7 @@ export interface GlobalThemeWithoutCommon {
Popover?: PopoverTheme
Popselect?: PopselectTheme
Progress?: ProgressTheme
Qrcode?: QrcodeTheme
Radio?: RadioTheme
Rate?: RateTheme
Result?: ResultTheme

@ -0,0 +1,25 @@
<markdown>
# Basic
A basic Qrcode.
</markdown>
<template>
<n-space vertical>
<n-qrcode :value="text" />
<n-input v-model:value="text" :maxlength="60" type="text" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('The rain dampened the sky')
return {
text
}
}
})
</script>

@ -0,0 +1,9 @@
<markdown>
# Border
Qrcode can be used without border.
</markdown>
<template>
<n-qrcode :bordered="false" value="https://www.naiveui.com/" />
</template>

@ -0,0 +1,16 @@
<markdown>
# Color
Let the QR code no longer monotonous.
</markdown>
<template>
<n-space>
<n-qrcode value="https://www.naiveui.com/" color="#18a058" />
<n-qrcode
value="https://www.naiveui.com/"
color="#409eff"
background-color="#F5F5F5"
/>
</n-space>
</template>

@ -0,0 +1,41 @@
<markdown>
# Download
Download two-dimensional code code implementation, you can also choose to right-click the picture save as.
</markdown>
<template>
<n-space vertical>
<n-qrcode id="qrcode" value="https://www.naiveui.com/" />
<n-button @click="handleDownloadQRCode">
Download
</n-button>
</n-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
const handleDownloadQRCode = () => {
const canvas = document
.querySelector('#qrcode')
?.querySelector<HTMLCanvasElement>('canvas')
if (canvas) {
const url = canvas.toDataURL()
const a = document.createElement('a')
a.download = 'QRCode.png'
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
}
return {
handleDownloadQRCode
}
}
})
</script>

@ -0,0 +1,42 @@
<markdown>
# Error correction
Use `error-correction-level` to set the error correction level.
</markdown>
<template>
<n-space vertical>
<n-qrcode
value="Like a humorous magician, he skillfully turns tedious information into a mysterious QR code"
:error-correction-level="errorCorrectionLevel"
/>
<n-radio-group v-model:value="errorCorrectionLevel">
<n-radio-button
v-for="errorCorrection in errorCorrectionOptions"
:key="errorCorrection.value"
:value="errorCorrection.value"
:label="errorCorrection.label"
/>
</n-radio-group>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const errorCorrectionLevel = ref('M')
const errorCorrectionOptions = [
{ value: 'L', label: 'L' },
{ value: 'M', label: 'M' },
{ value: 'Q', label: 'Q' },
{ value: 'H', label: 'H' }
]
return {
errorCorrectionLevel,
errorCorrectionOptions
}
}
})
</script>

@ -0,0 +1,37 @@
<markdown>
# Icon
You can put some representative Icon.
</markdown>
<template>
<n-space>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
error-correction-level="H"
/>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
icon-background-color="#ccc"
error-correction-level="H"
/>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
:icon-size="48"
error-correction-level="H"
/>
</n-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
return {}
}
})
</script>

@ -0,0 +1,49 @@
# QRCode
Like a humorous magician, he skillfully turns tedious information into a mysterious QR code
## 演示
```demo
basic.vue
icon.vue
border.vue
size.vue
color.vue
error-correction.vue
download.vue
```
## API
### QRCode Props
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| background-color | `string` | `#FFF` | Qr code background color, Values need to be in `hex` format. | NEXT |
| bordered | `boolean` | `true` | Whether to show the qrcode border. | NEXT |
| color | `string` | `#000` | Qr code color, Values need to be in `hex` format. | NEXT |
| error-correction-level | `L` \| `M` \| `Q` \| `H` | `M` | Qr code error correction level. | NEXT |
| icon | `string` | `undefined` | Icon's URL. | NEXT |
| icon-size | `number` | `40` | Icon's size. | NEXT |
| icon-background-color | `string` | `#FFF` | Icon's background color. | NEXT |
| value | `string` | `-` | Text information. | NEXT |
| size | `number` | `160` | Size of the qrcode. | NEXT |
## Q & A
### About QR code error correction level
The error correction level of the two-dimensional code refers to the error correction capability used in the generation of the two-dimensional code, which determines the ability of the two-dimensional code to be correctly decoded when it is damaged or partially invisible.
Two-dimensional code standards (such as QR codes) define four error correction levels: L, M, Q, and H. Each level provides different error correction capabilities and tolerance.
- 'L (Low)' : Provides approximately `7%` recovery capability
- `M (Middle)`Provides approximately `15%` recovery capacity
- `Q (High)`Provides approximately `25%` recovery capacity
- `H (Max)`Provides approximately `30%` recovery capacity
Selecting a higher error correction level can improve the fault tolerance of the two-dimensional code, that is, it can still be correctly decoded under a certain degree of damage or deformation, but at the same time, it will increase the density of the two-dimensional code, making the two-dimensional code occupy a larger space.

@ -0,0 +1,52 @@
<markdown>
# Size
It is like a crazy puff pastry, no matter how big or small, it will lead you into another mysterious information space.
</markdown>
<template>
<n-space vertical>
<n-space>
<n-button @click="minus">
Minus 10
</n-button>
<n-button @click="add">
Add 10
</n-button>
</n-space>
<n-qrcode :value="text" :size="size" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('The rain dampened the sky')
const size = ref(110)
const add = () => {
size.value += 10
if (size.value > 200) {
size.value = 110
}
}
const minus = () => {
size.value -= 10
if (size.value < 20) {
size.value = 110
}
}
return {
size,
text,
add,
minus
}
}
})
</script>

@ -0,0 +1,25 @@
<markdown>
# 基础用法
基础二维码
</markdown>
<template>
<n-space vertical>
<n-qrcode :value="text" />
<n-input v-model:value="text" :maxlength="60" type="text" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('雨淋湿了天空')
return {
text
}
}
})
</script>

@ -0,0 +1,9 @@
<markdown>
# 边框
二维码可以没有边框
</markdown>
<template>
<n-qrcode :bordered="false" value="https://www.naiveui.com/" />
</template>

@ -0,0 +1,16 @@
<markdown>
# 颜色
让二维码不再单调乏味
</markdown>
<template>
<n-space>
<n-qrcode value="https://www.naiveui.com/" color="#18a058" />
<n-qrcode
value="https://www.naiveui.com/"
color="#409eff"
background-color="#F5F5F5"
/>
</n-space>
</template>

@ -0,0 +1,40 @@
<markdown>
# 下载
下载二维码的代码实现你也可以选择右键图片另存为
</markdown>
<template>
<n-space vertical>
<n-qrcode id="qrcode" value="https://www.naiveui.com/" />
<n-button @click="handleDownloadQRCode">
下载
</n-button>
</n-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
const handleDownloadQRCode = () => {
const canvas = document
.querySelector('#qrcode')
?.querySelector<HTMLCanvasElement>('canvas')
if (canvas) {
const url = canvas.toDataURL()
const a = document.createElement('a')
a.download = 'QRCode.png'
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
}
return {
handleDownloadQRCode
}
}
})
</script>

@ -0,0 +1,42 @@
<markdown>
# 纠错
使用 `error-correction-level` 来设定纠错级别
</markdown>
<template>
<n-space vertical>
<n-qrcode
value="犹如一位幽默风趣的魔术师,巧妙地将繁琐的信息变成了一个神秘的二维码"
:error-correction-level="errorCorrectionLevel"
/>
<n-radio-group v-model:value="errorCorrectionLevel">
<n-radio-button
v-for="errorCorrection in errorCorrectionOptions"
:key="errorCorrection.value"
:value="errorCorrection.value"
:label="errorCorrection.label"
/>
</n-radio-group>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const errorCorrectionLevel = ref('M')
const errorCorrectionOptions = [
{ value: 'L', label: 'L' },
{ value: 'M', label: 'M' },
{ value: 'Q', label: 'Q' },
{ value: 'H', label: 'H' }
]
return {
errorCorrectionLevel,
errorCorrectionOptions
}
}
})
</script>

@ -0,0 +1,37 @@
<markdown>
# 图标
可以放一些代表性的 Icon
</markdown>
<template>
<n-space>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
error-correction-level="H"
/>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
icon-background-color="#ccc"
error-correction-level="H"
/>
<n-qrcode
value="https://www.naiveui.com/"
icon="https://www.naiveui.com/assets/naivelogo-93278402.svg"
:icon-size="48"
error-correction-level="H"
/>
</n-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
return {}
}
})
</script>

@ -0,0 +1,49 @@
# 二维码 QRCode
犹如一位幽默风趣的魔术师,巧妙地将繁琐的信息变成了一个神秘的二维码
## 演示
```demo
basic.vue
icon.vue
border.vue
size.vue
color.vue
error-correction.vue
download.vue
```
## API
### QRCode Props
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| background-color | `string` | `#FFF` | 二维码背景颜色,值需要采用 `hex` 格式 | NEXT |
| bordered | `boolean` | `true` | 是否显示二维码边框 | NEXT |
| color | `string` | `#000` | 二维码颜色,值需要采用 `hex` 格式 | NEXT |
| error-correction-level | `L` \| `M` \| `Q` \| `H` | `M` | 二维码纠错级别 | NEXT |
| icon | `string` | `undefined` | 图标地址 | NEXT |
| icon-size | `number` | `40` | 图标大小 | NEXT |
| icon-background-color | `string` | `#FFF` | 图标背景颜色 | NEXT |
| value | `string` | `-` | 文本信息 | NEXT |
| size | `number` | `160` | 二维码大小 | NEXT |
## Q & A
### 关于二维码纠错级别
二维码纠错级别是指在生成二维码时所使用的错误纠正能力,它决定了二维码在受到损坏或部分不可见时,仍然可以正确解码的能力。
二维码标准(如 QR 码定义了四个纠错级别L、M、Q 和 H。每个级别提供不同的纠错能力和容错性。
- `L`:提供约 `7%` 的恢复能力
- `M`:提供约 `15%` 的恢复能力
- `Q`:提供约 `25%` 的恢复能力
- `H最高`:提供约 `30%` 的恢复能力
选择更高的纠错级别可以提高二维码的容错性,即在一定程度的损坏或变形下仍然能够正确解码,但同时会增加二维码的密度,使得二维码所占空间更大。

@ -0,0 +1,52 @@
<markdown>
# 尺寸
它就像疯狂千层饼无论大小都会引领你进入另一个神秘的信息空间
</markdown>
<template>
<n-space vertical>
<n-space>
<n-button @click="minus">
10
</n-button>
<n-button @click="add">
10
</n-button>
</n-space>
<n-qrcode :value="text" :size="size" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('The rain dampened the sky')
const size = ref(110)
const add = () => {
size.value += 10
if (size.value > 200) {
size.value = 110
}
}
const minus = () => {
size.value -= 10
if (size.value < 20) {
size.value = 110
}
}
return {
size,
text,
add,
minus
}
}
})
</script>

2
src/qrcode/index.ts Normal file

@ -0,0 +1,2 @@
export { default as NQrcode, qrcodeProps } from './src/Qrcode'
export type { QrcodeProps } from './src/Qrcode'

141
src/qrcode/src/Qrcode.tsx Normal file

@ -0,0 +1,141 @@
import { h, ref, defineComponent, watchEffect } from 'vue'
import { useConfig, useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import type { ExtractPublicPropTypes } from '../../_utils'
import style from './styles/index.cssr'
import { type QrcodeTheme, qrcodeLight } from '../styles'
import qrcodegen from './qrcodegen'
const ERROR_CORRECTION_LEVEL: Record<string, qrcodegen.QrCode.Ecc> = {
L: qrcodegen.QrCode.Ecc.LOW,
M: qrcodegen.QrCode.Ecc.MEDIUM,
Q: qrcodegen.QrCode.Ecc.QUARTILE,
H: qrcodegen.QrCode.Ecc.HIGH
}
export const qrcodeProps = {
...(useTheme.props as ThemeProps<QrcodeTheme>),
value: String,
color: {
type: String,
default: '#000'
},
backgroundColor: {
type: String,
default: '#FFF'
},
bordered: {
type: Boolean,
default: true
},
icon: String,
iconSize: {
type: Number,
default: 40
},
iconBackgroundColor: {
type: String,
default: '#FFF'
},
size: {
type: Number,
default: 110
},
errorCorrectionLevel: {
type: String,
default: 'M'
}
} as const
export type QrcodeProps = ExtractPublicPropTypes<typeof qrcodeProps>
export default defineComponent({
name: 'Qrcode',
props: qrcodeProps,
setup (props) {
const { mergedClsPrefixRef } = useConfig(props)
useTheme('Qrcode', '-qrcode', style, qrcodeLight, props, mergedClsPrefixRef)
const canvasRef = ref<HTMLCanvasElement>()
watchEffect(() => {
const errorCorrectionLevel =
ERROR_CORRECTION_LEVEL[props.errorCorrectionLevel]
const qr = qrcodegen.QrCode.encodeText(
props.value ?? '-',
errorCorrectionLevel
)
drawCanvas(qr, props.size, props.color, props.backgroundColor)
})
function drawCanvas (
qr: qrcodegen.QrCode,
size: number,
darkColor: string,
lightColor: string
): void {
const canvas = canvasRef.value
if (!canvas) return
const canvasWidth = size
const width = qr.size
const scale = canvasWidth / width
canvas.width = canvasWidth
canvas.height = canvasWidth
const ctx = canvas.getContext('2d')
if (!ctx) return
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
ctx.fillStyle = qr.getModule(x, y) ? darkColor : lightColor
const startX = Math.floor(x * scale)
const endX = Math.ceil((x + 1) * scale)
const startY = Math.floor(y * scale)
const endY = Math.ceil((y + 1) * scale)
ctx.fillRect(startX, startY, endX - startX, endY - startY)
}
}
if (props.icon) {
const img = new Image()
img.src = props.icon
img.onload = () => {
const iconSize = props.iconSize
const centerX = (canvas.width - iconSize) / 2
const centerY = (canvas.height - iconSize) / 2
ctx.fillStyle = props.iconBackgroundColor
ctx.fillRect(centerX, centerY, iconSize, iconSize)
const aspectRatio = img.width / img.height
const scaledWidth =
aspectRatio >= 1 ? iconSize : iconSize * aspectRatio
const scaledHeight =
aspectRatio <= 1 ? iconSize : iconSize / aspectRatio
const left = centerX + (iconSize - scaledWidth) / 2
const top = centerY + (iconSize - scaledHeight) / 2
ctx.drawImage(img, left, top, scaledWidth, scaledHeight)
}
}
}
return {
canvasRef,
mergedClsPrefix: mergedClsPrefixRef
}
},
render () {
const { mergedClsPrefix, bordered, backgroundColor } = this
return (
<div
class={[
`${mergedClsPrefix}-qrcode`,
{ [`${mergedClsPrefix}-qrcode--bordered`]: bordered }
]}
>
<div
class={[`${mergedClsPrefix}-qrcode-wrapper`]}
style={{ backgroundColor }}
>
<canvas ref="canvasRef"></canvas>
</div>
</div>
)
}
})

1130
src/qrcode/src/qrcodegen.ts Normal file

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
import { c, cB, cM } from '../../../_utils/cssr'
// vars:
// --n-border-color
// --n-border-radius
export default c([
cB('qrcode', [
cB('qrcode-wrapper', `
width: fit-content;
line-height: 0;
`),
cM('bordered', [
c('>',
[
cB('qrcode-wrapper', `
border: 1px solid var(--n-border-color);
border-radius: var(--n-border-radius) var(--n-border-radius) 0 0;
padding:
var(--n-padding-top)
var(--n-padding-left)
var(--n-padding-bottom)
var(--n-padding-left);
`)
]
)
])
]
)
])

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

@ -0,0 +1,3 @@
export { default as qrcodeDark } from './dark'
export { default as qrcodeLight } from './light'
export type { QrcodeTheme, QrcodeThemeVars } from './light'

@ -0,0 +1,18 @@
import { commonLight } from '../../_styles/common'
import type { ThemeCommonVars } from '../../_styles/common'
import { type Theme } from '../../_mixins'
const self = (vars: ThemeCommonVars) => {
return {}
}
export type QrcodeThemeVars = ReturnType<typeof self>
const themeLight: Theme<'Qrcode', QrcodeThemeVars> = {
name: 'Qrcode',
common: commonLight,
self
}
export default themeLight
export type QrcodeTheme = typeof themeLight

@ -54,6 +54,7 @@ import { popconfirmDark } from '../popconfirm/styles'
import { popoverDark } from '../popover/styles'
import { popselectDark } from '../popselect/styles'
import { progressDark } from '../progress/styles'
import { qrcodeDark } from '../qrcode/styles'
import { radioDark } from '../radio/styles'
import { rateDark } from '../rate/styles'
import { resultDark } from '../result/styles'
@ -141,6 +142,7 @@ export const darkTheme: BuiltInGlobalTheme = {
Popover: popoverDark,
Popselect: popselectDark,
Progress: progressDark,
Qrcode: qrcodeDark,
Radio: radioDark,
Rate: rateDark,
Result: resultDark,

@ -56,6 +56,7 @@ import { popconfirmLight } from '../popconfirm/styles'
import { popoverLight } from '../popover/styles'
import { popselectLight } from '../popselect/styles'
import { progressLight } from '../progress/styles'
import { qrcodeLight } from '../qrcode/styles'
import { radioLight } from '../radio/styles'
import { rateLight } from '../rate/styles'
import { resultLight } from '../result/styles'
@ -143,6 +144,7 @@ export const lightTheme: BuiltInGlobalTheme = {
Popover: popoverLight,
Popselect: popselectLight,
Progress: progressLight,
Qrcode: qrcodeLight,
Radio: radioLight,
Rate: rateLight,
Row: rowLight,