feat: add qrcode (#6315)

* feat: add qrcode

* fix: qrcode bug

* fix: qrcode value required

* refactor: props  deconstruct
pull/6348/head
selicens 2023-03-02 10:46:16 +08:00 committed by GitHub
parent eda7247c2c
commit 41a455f881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 702 additions and 2 deletions

View File

@ -250,3 +250,6 @@ export type { WatermarkProps } from './watermark';
export type { SegmentedProps } from './segmented';
export { default as Segmented } from './segmented';
export type { QRCodeProps } from './qrcode';
export { default as QRCode } from './qrcode';

View File

@ -131,6 +131,10 @@ const localeValues: Locale = {
Image: {
preview: 'Preview',
},
QRCode: {
expired: 'QR code expired',
refresh: 'Refresh',
},
};
export default localeValues;

View File

@ -42,6 +42,10 @@ export interface Locale {
copied?: any;
expand?: any;
};
QRCode: {
expired?: string;
refresh?: string;
};
}
export interface LocaleProviderProps {

View File

@ -130,6 +130,10 @@ const localeValues: Locale = {
Image: {
preview: '预览',
},
QRCode: {
expired: '二维码已过期',
refresh: '点击刷新',
},
};
export default localeValues;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('qrcode');

View File

@ -0,0 +1,29 @@
<docs>
---
order: 0
title:
zh-CN: 基本使用
en-US: Base
---
## zh-CN
基本用法
## en-US
Basic Usage.
</docs>
<template>
<a-qrcode value="https://www.antdv.com/" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>

View File

@ -0,0 +1,32 @@
<docs>
---
order: 5
title:
zh-CN: 自定义颜色
en-US: Custom Color
---
## zh-CN
通过设置 `color` 自定义二维码颜色通过设置 `style` 自定义背景颜色
## en-US
Custom Color.
</docs>
<template>
<a-space>
<div><a-qrcode value="http://www.antv.com" color="#73d13d" /></div>
<div><a-qrcode value="http://www.antv.com" color="#1677ff" /></div>
</a-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>

View File

@ -0,0 +1,56 @@
<docs>
---
order: 4
title:
zh-CN: 自定义尺寸
en-US: Custom Size
---
## zh-CN
自定义尺寸
## en-US
Custom Size.
</docs>
<template>
<a-button-group>
<a-button @click="decline">
<template #icon><MinusOutlined /></template>
samll
</a-button>
<a-button @click="increase">
<template #icon><PlusOutlined /></template>
large
</a-button>
</a-button-group>
<br />
<br />
<a-qrcode
:size="size"
:icon-size="size / 4"
error-level="H"
value="https://www.antv.com"
icon="https://www.antdv.com/assets/logo.1ef800a8.svg"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons-vue';
const size = ref(160);
const decline = () => {
size.value = size.value - 10;
if (size.value < 48) {
size.value = 48;
}
};
const increase = () => {
size.value = size.value + 10;
if (size.value > 300) {
size.value = 300;
}
};
</script>

View File

@ -0,0 +1,45 @@
<docs>
---
order: 6
title:
zh-CN: 下载二维码
en-US: Download QRCode
---
## zh-CN
下载二维码的简单实现
## en-US
A way to download QRCode.
</docs>
<template>
<a-qrcode ref="qrcodeCanvasRef" value="http://www.antv.com" />
<br />
<br />
<a-button type="primary" @click="dowloadChange">Downlaod</a-button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const qrcodeCanvasRef = ref();
const dowloadChange = async () => {
const url = await qrcodeCanvasRef.value.toDataUrl();
const a = document.createElement('a');
a.download = 'QRCode.png';
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
return {
dowloadChange,
qrcodeCanvasRef,
};
},
});
</script>

View File

@ -0,0 +1,37 @@
<docs>
---
order: 7
title:
zh-CN: 纠错比例
en-US: Error Level
---
## zh-CN
通过设置 errorLevel 调整不同的容错等级
## en-US
set Error Level.
</docs>
<template>
<a-qrcode v-model:error-level="level" value="http://www.antv.com" />
<br />
<br />
<a-segmented v-model:value="level" :options="segmentedData" />
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from 'vue';
export default defineComponent({
setup() {
const segmentedData = reactive(['L', 'M', 'Q', 'H']);
const level = ref(segmentedData[0]);
return {
segmentedData,
level,
};
},
});
</script>

View File

@ -0,0 +1,33 @@
<docs>
---
order: 1
title:
zh-CN: Icon 的例子
en-US: With Icon
---
## zh-CN
Icon 的二维码
## en-US
QRCode with Icon.
</docs>
<template>
<a-qrcode
error-level="H"
value="https://www.antv.com"
icon="https://www.antdv.com/assets/logo.1ef800a8.svg"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>

View File

@ -0,0 +1,36 @@
<template>
<demo-sort>
<Base />
<Icon />
<Status />
<CustomSize />
<CustomColor />
<Download />
<ErrorLevel />
<Popover />
</demo-sort>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import Base from './base.vue';
import Icon from './icon.vue';
import Status from './status.vue';
import CustomSize from './customSize.vue';
import CustomColor from './customColor.vue';
import Download from './download.vue';
import ErrorLevel from './errorLevel.vue';
import Popover from './popover.vue';
export default defineComponent({
components: { Base, Icon, Status, CustomSize, CustomColor, Download, ErrorLevel, Popover },
category: 'Components',
subtitle: '二维码',
type: 'Data Display',
title: 'QRCode',
CN,
US,
});
</script>

View File

@ -0,0 +1,34 @@
<docs>
---
order: 8
title:
zh-CN: 高级用法
en-US: Advanced Usage
---
## zh-CN
带气泡卡片的例子
## en-US
With Popover.
</docs>
<template>
<a-popover>
<template #content>
<a-qrcode value="http://www.antv.com" />
</template>
<img width="100" height="100" src="https://aliyuncdn.antdv.com/logo.png" />
</a-popover>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>

View File

@ -0,0 +1,35 @@
<docs>
---
order: 3
title:
zh-CN: 不同的状态
en-US: other status
---
## zh-CN
可以通过 status 的值控制二维码的状态
## en-US
The status can be controlled by the value `status`.
</docs>
<template>
<a-space>
<div><a-qrcode value="http://www.antv.com" status="loading" /></div>
<div><a-qrcode value="http://www.antv.com" status="expired" @refresh="refreshChange" /></div>
</a-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
const refreshChange = () => alert('updated');
return {
refreshChange,
};
},
});
</script>

View File

@ -0,0 +1,39 @@
---
category: Components
title: QRCode
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cJopQrf0ncwAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*M4PBTZ_n9OgAAAAAAAAAAAAADrJ8AQ/original
---
## When To Use
Used when the link needs to be converted into a QR Code.
## API
| Property | Description | Type | Default |
| :-- | :-- | :-- | :-- |
| value | scanned link | string | - |
| icon | include image url (only image link are supported) | string | - |
| size | QRCode size | number | 128 |
| iconSize | include image size | number | 32 |
| color | QRCode Color | string | `#000` |
| bordered | Whether has border style | boolean | `true` |
| errorLevel | Error Code Level | `'L'` \| `'M'` \| `'Q'` \| `'H'` | `'M'` |
| status | QRCode status | `active` \| `expired` \| `loading ` | `active` |
### events
| Events Name | Description | Arguments | Version |
| :---------- | :---------- | :----------- | :------ |
| refresh | callback | `() => void` | - |
## FAQ
### About QRCode ErrorLevel
The ErrorLevel means that the QR code can be scanned normally after being blocked, and the maximum area that can be blocked is the error correction rate.
Generally, the QR code is divided into 4 error correction levels: Level `L` can correct about `7%` errors, Level `M` can correct about `15%` errors, Level `Q` can correct about `25%` errors, and Level `H` can correct about `30%` errors. When the content encoding of the QR code carries less information, in other words, when the value link is short, set different error correction levels, and the generated image will not change.
> For more information, see the: [https://www.qrcode.com/en/about/error_correction](https://www.qrcode.com/en/about/error_correction.html)

165
components/qrcode/index.tsx Normal file
View File

@ -0,0 +1,165 @@
import { defineComponent, onMounted, ref, watch } from 'vue';
import type { ExtractPropTypes } from 'vue';
import classNames from '../_util/classNames';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import { initDefaultProps } from '../_util/props-util';
import useStyle from './style';
import { useLocaleReceiver } from '../locale/LocaleReceiver';
import defaultLocale from '../locale/en_US';
import { toCanvas, toDataURL } from 'qrcode';
import { withInstall } from '../_util/type';
import Spin from '../spin';
import Button from '../button';
import { ReloadOutlined } from '@ant-design/icons-vue';
import { useToken } from '../theme/internal';
interface QRCodeCanvasColor {
dark?: string; // #000000ff
light?: string; // #ffffffff
}
interface QRCodeCanvasOptions {
version?: number;
errorCorrectionLevel?: string; // "M"
maskPattern?: number; //
toSJISFunc?: Function; // Shift JIS
margin?: number;
scale?: number;
small?: boolean;
width: number;
color?: QRCodeCanvasColor;
}
const qrcodeProps = () => {
return {
value: { type: String, required: true },
errorLevel: String,
size: { type: Number, default: 160 },
icon: String,
iconSize: { type: Number, default: 40 },
color: String,
status: { type: String, default: 'active' },
bordered: { type: Boolean, default: true },
};
};
export type QRCodeProps = Partial<ExtractPropTypes<ReturnType<typeof qrcodeProps>>>;
const canvasProps = () => {
return {
value: String,
errorLevel: { type: String, default: 'M' },
size: Number,
icon: String,
iconSize: { type: Number, default: 40 },
color: { type: String, default: '#000000ff' },
};
};
const QRCodeCanvas = defineComponent({
name: 'QRCodeCanvas',
props: initDefaultProps(canvasProps(), {}),
setup(props) {
const qrcodeCanvasRef = ref();
watch(
() => props.size,
newSize => {
createQRCode(newSize);
},
);
watch(
() => props.errorLevel,
newLevel => {
createQRCode(props.size, newLevel);
},
);
const createQRCode = (width = props.size, level = props.errorLevel) => {
const options: QRCodeCanvasOptions = {
errorCorrectionLevel: level || getErrorCorrectionLevel(props.value),
margin: 0,
width,
color: { dark: props.color },
};
toCanvas(qrcodeCanvasRef.value, props.value, options);
if (props.icon) {
const ctx = qrcodeCanvasRef.value.getContext('2d');
const image = new Image(props.iconSize, props.iconSize);
image.src = props.icon;
image.onload = () => {
/*
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
sx,sy 在画布指定位置绘制
sw,sh 被剪切的部分
dx,dy 在目标画布的起点位置
dw,dh 在目标画布绘制的宽高
*/
ctx.drawImage(qrcodeCanvasRef.value, 0, 0, width, width);
const center = (width - props.iconSize) / 2;
ctx.drawImage(image, center, center, props.iconSize, props.iconSize);
};
}
};
function getErrorCorrectionLevel(content) {
if (content.length > 36) {
return 'M';
} else if (content.length > 16) {
return 'Q';
} else {
return 'H';
}
}
onMounted(() => {
createQRCode();
});
return () => (
<>
<canvas ref={qrcodeCanvasRef} />
</>
);
},
});
const QRCode = defineComponent({
name: 'AQrcode',
props: initDefaultProps(qrcodeProps(), {}),
emits: ['refresh'],
setup(props, { emit, expose }) {
const [locale] = useLocaleReceiver('QRCode', defaultLocale.QRCode);
const { prefixCls } = useConfigInject('qrcode', props);
const [wrapSSR, hashId] = useStyle(prefixCls);
const [, token] = useToken();
const pre = prefixCls.value;
const toDataUrl = async () => {
return await toDataURL(props.value);
};
expose({ toDataUrl });
return () => {
return wrapSSR(
<div
style={{ width: props.size + 'px', height: props.size + 'px' }}
class={classNames(hashId.value, pre, {
[`${prefixCls}-borderless`]: !props.bordered,
})}
>
{props.status !== 'active' && (
<div class={classNames(`${pre}-mask`)}>
{props.status === 'loading' && <Spin />}
{props.status === 'expired' && (
<>
<p class={classNames(`${pre}-expired`)}>{locale.value.expired}</p>
<Button type="link" onClick={() => emit('refresh')}>
<ReloadOutlined />
{locale.value.refresh}
</Button>
</>
)}
</div>
)}
<QRCodeCanvas
value={props.value}
errorLevel={props.errorLevel}
size={props.size - (token.value.paddingSM + token.value.lineWidth) * 2}
icon={props.icon}
iconSize={props.iconSize}
color={props.color}
/>
</div>,
);
};
},
});
export default withInstall(QRCode);

View File

@ -0,0 +1,40 @@
---
category: Components
subtitle: 二维码
title: QRCode
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cJopQrf0ncwAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*M4PBTZ_n9OgAAAAAAAAAAAAADrJ8AQ/original
---
## 何时使用
当需要将链接转换成为二维码时使用。
## API
| 参数 | 说明 | 类型 | 默认值 |
| :-- | :-- | :-- | :-- |
| value | 扫描后的地址 | string | - |
| icon | 二维码中图片的地址(目前只支持图片地址) | string | - |
| size | 二维码大小 | number | 160 |
| iconSize | 二维码中图片的大小 | number | 40 |
| color | 二维码颜色 | string | `#000` |
| bordered | 是否有边框 | boolean | `true` |
| errorLevel | 二维码纠错等级 | `'L'` \| `'M'` \| `'Q'` \| `'H'` | `'M'` |
| status | 二维码状态 | `active` \| `expired` \| `loading ` | `active` |
### 事件
| 事件名称 | 说明 | 回调参数 | 版本 |
| :------- | :------------------- | :----------- | :--- |
| refresh | 点击"点击刷新"的回调 | `() => void` | - |
## FAQ
### 关于二维码纠错等级
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
通常情况下二维码分为 4 个纠错级别:`L级` 可纠正约 `7%` 错误、`M级` 可纠正约 `15%` 错误、`Q级` 可纠正约 `25%` 错误、`H级` 可纠正约`30%` 错误。并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
> 有关更多信息,可参阅相关资料:[https://www.qrcode.com/zh/about/error_correction](https://www.qrcode.com/zh/about/error_correction.html)

View File

@ -0,0 +1,33 @@
import type { CSSProperties } from 'vue';
interface ImageSettings {
src: string;
height: number;
width: number;
excavate: boolean;
x?: number;
y?: number;
}
interface QRProps {
value: string;
size?: number;
color?: string;
style?: CSSProperties;
includeMargin?: boolean;
imageSettings?: ImageSettings;
}
export type QRPropsCanvas = QRProps;
export interface QRCodeProps extends QRProps {
className?: string;
rootClassName?: string;
prefixCls?: string;
icon?: string;
iconSize?: number;
bordered?: boolean;
errorLevel?: 'L' | 'M' | 'Q' | 'H';
status?: 'active' | 'expired' | 'loading';
onRefresh?: () => void;
}

View File

@ -0,0 +1,65 @@
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { mergeToken, genComponentStyleHook } from '../../theme/internal';
import { resetComponent } from '../../_style';
export interface ComponentToken {}
interface QRCodeToken extends FullToken<'QRCode'> {
QRCodeExpiredTextColor: string;
QRCodeMaskBackgroundColor: string;
}
const genQRCodeStyle: GenerateStyle<QRCodeToken> = token => {
const { componentCls } = token;
return {
[componentCls]: {
...resetComponent(token),
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: token.paddingSM,
backgroundColor: token.colorWhite,
borderRadius: token.borderRadiusLG,
border: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden',
[`& > ${componentCls}-mask`]: {
position: 'absolute',
insetBlockStart: 0,
insetInlineStart: 0,
zIndex: 10,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
color: token.colorText,
lineHeight: token.lineHeight,
background: token.QRCodeMaskBackgroundColor,
textAlign: 'center',
[`& > ${componentCls}-expired`]: {
color: token.QRCodeExpiredTextColor,
},
},
'&-icon': {
marginBlockEnd: token.marginXS,
fontSize: token.controlHeight,
},
},
[`${componentCls}-borderless`]: {
borderColor: 'transparent',
},
};
};
export default genComponentStyleHook<'QRCode'>('QRCode', token =>
genQRCodeStyle(
mergeToken<QRCodeToken>(token, {
QRCodeExpiredTextColor: 'rgba(0, 0, 0, 0.88)',
QRCodeMaskBackgroundColor: 'rgba(255, 255, 255, 0.96)',
}),
),
);

View File

@ -46,7 +46,7 @@ import type { ComponentToken as TransferComponentToken } from '../../transfer/st
import type { ComponentToken as TypographyComponentToken } from '../../typography/style';
import type { ComponentToken as UploadComponentToken } from '../../upload/style';
// import type { ComponentToken as TourComponentToken } from '../../tour/style';
// import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
// import type { ComponentToken as AppComponentToken } from '../../app/style';
// import type { ComponentToken as WaveToken } from '../../_util/wave/style';
@ -113,7 +113,7 @@ export interface ComponentTokenMap {
Space?: SpaceComponentToken;
Progress?: ProgressComponentToken;
// Tour?: TourComponentToken;
// QRCode?: QRCodeComponentToken;
QRCode?: QRCodeComponentToken;
// App?: AppComponentToken;
// /** @private Internal TS definition. Do not use. */

View File

@ -293,6 +293,7 @@
"dom-scroll-into-view": "^2.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.15",
"qrcode": "^1.5.1",
"resize-observer-polyfill": "^1.5.1",
"scroll-into-view-if-needed": "^2.2.25",
"shallow-equal": "^1.0.0",

2
typings/global.d.ts vendored
View File

@ -252,6 +252,8 @@ declare module 'vue' {
AUploadDragger: typeof import('ant-design-vue')['UploadDragger'];
AWeekPicker: typeof import('ant-design-vue')['WeekPicker'];
AQRCode: typeof import('ant-design-vue')['QRCode'];
}
}
export {};