From 41a455f881467c185339d5edc844611673a77bf1 Mon Sep 17 00:00:00 2001 From: selicens <1244620067@qq.com> Date: Thu, 2 Mar 2023 10:46:16 +0800 Subject: [PATCH] feat: add qrcode (#6315) * feat: add qrcode * fix: qrcode bug * fix: qrcode value required * refactor: props deconstruct --- components/components.ts | 3 + components/locale/en_US.ts | 4 + components/locale/index.tsx | 4 + components/locale/zh_CN.ts | 4 + components/qrcode/__tests__/demo.test.js | 3 + components/qrcode/demo/base.vue | 29 ++++ components/qrcode/demo/customColor.vue | 32 +++++ components/qrcode/demo/customSize.vue | 56 ++++++++ components/qrcode/demo/download.vue | 45 +++++++ components/qrcode/demo/errorLevel.vue | 37 +++++ components/qrcode/demo/icon.vue | 33 +++++ components/qrcode/demo/index.vue | 36 +++++ components/qrcode/demo/popover.vue | 34 +++++ components/qrcode/demo/status.vue | 35 +++++ components/qrcode/index.en-US.md | 39 ++++++ components/qrcode/index.tsx | 165 +++++++++++++++++++++++ components/qrcode/index.zh-CN.md | 40 ++++++ components/qrcode/interface.ts | 33 +++++ components/qrcode/style/index.ts | 65 +++++++++ components/theme/interface/components.ts | 4 +- package.json | 1 + typings/global.d.ts | 2 + 22 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 components/qrcode/__tests__/demo.test.js create mode 100644 components/qrcode/demo/base.vue create mode 100644 components/qrcode/demo/customColor.vue create mode 100644 components/qrcode/demo/customSize.vue create mode 100644 components/qrcode/demo/download.vue create mode 100644 components/qrcode/demo/errorLevel.vue create mode 100644 components/qrcode/demo/icon.vue create mode 100644 components/qrcode/demo/index.vue create mode 100644 components/qrcode/demo/popover.vue create mode 100644 components/qrcode/demo/status.vue create mode 100644 components/qrcode/index.en-US.md create mode 100644 components/qrcode/index.tsx create mode 100644 components/qrcode/index.zh-CN.md create mode 100644 components/qrcode/interface.ts create mode 100644 components/qrcode/style/index.ts diff --git a/components/components.ts b/components/components.ts index 83a933dcb..286904c7b 100644 --- a/components/components.ts +++ b/components/components.ts @@ -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'; diff --git a/components/locale/en_US.ts b/components/locale/en_US.ts index 1de8b02af..1038f45df 100644 --- a/components/locale/en_US.ts +++ b/components/locale/en_US.ts @@ -131,6 +131,10 @@ const localeValues: Locale = { Image: { preview: 'Preview', }, + QRCode: { + expired: 'QR code expired', + refresh: 'Refresh', + }, }; export default localeValues; diff --git a/components/locale/index.tsx b/components/locale/index.tsx index ce85caf46..8a9b5e210 100644 --- a/components/locale/index.tsx +++ b/components/locale/index.tsx @@ -42,6 +42,10 @@ export interface Locale { copied?: any; expand?: any; }; + QRCode: { + expired?: string; + refresh?: string; + }; } export interface LocaleProviderProps { diff --git a/components/locale/zh_CN.ts b/components/locale/zh_CN.ts index b6da7ded0..65e601980 100644 --- a/components/locale/zh_CN.ts +++ b/components/locale/zh_CN.ts @@ -130,6 +130,10 @@ const localeValues: Locale = { Image: { preview: '预览', }, + QRCode: { + expired: '二维码已过期', + refresh: '点击刷新', + }, }; export default localeValues; diff --git a/components/qrcode/__tests__/demo.test.js b/components/qrcode/__tests__/demo.test.js new file mode 100644 index 000000000..1cb1d77dd --- /dev/null +++ b/components/qrcode/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('qrcode'); diff --git a/components/qrcode/demo/base.vue b/components/qrcode/demo/base.vue new file mode 100644 index 000000000..280560d61 --- /dev/null +++ b/components/qrcode/demo/base.vue @@ -0,0 +1,29 @@ + +--- +order: 0 +title: + zh-CN: 基本使用 + en-US: Base +--- + +## zh-CN + +基本用法。 + +## en-US +Basic Usage. + + + + + + + diff --git a/components/qrcode/demo/customColor.vue b/components/qrcode/demo/customColor.vue new file mode 100644 index 000000000..372dd8eb6 --- /dev/null +++ b/components/qrcode/demo/customColor.vue @@ -0,0 +1,32 @@ + +--- +order: 5 +title: + zh-CN: 自定义颜色 + en-US: Custom Color +--- + +## zh-CN + +通过设置 `color` 自定义二维码颜色,通过设置 `style` 自定义背景颜色。 + +## en-US +Custom Color. + + + + + + + + + + diff --git a/components/qrcode/demo/customSize.vue b/components/qrcode/demo/customSize.vue new file mode 100644 index 000000000..bc3ddc1be --- /dev/null +++ b/components/qrcode/demo/customSize.vue @@ -0,0 +1,56 @@ + +--- +order: 4 +title: + zh-CN: 自定义尺寸 + en-US: Custom Size +--- + +## zh-CN + +自定义尺寸 + +## en-US +Custom Size. + + + + + + + samll + + + + large + + + + + + + + diff --git a/components/qrcode/demo/download.vue b/components/qrcode/demo/download.vue new file mode 100644 index 000000000..ee7e5bd01 --- /dev/null +++ b/components/qrcode/demo/download.vue @@ -0,0 +1,45 @@ + +--- +order: 6 +title: + zh-CN: 下载二维码 + en-US: Download QRCode +--- + +## zh-CN + +下载二维码的简单实现。 + +## en-US +A way to download QRCode. + + + + + + + Downlaod + + + diff --git a/components/qrcode/demo/errorLevel.vue b/components/qrcode/demo/errorLevel.vue new file mode 100644 index 000000000..9b7142cc4 --- /dev/null +++ b/components/qrcode/demo/errorLevel.vue @@ -0,0 +1,37 @@ + +--- +order: 7 +title: + zh-CN: 纠错比例 + en-US: Error Level +--- + +## zh-CN + +通过设置 errorLevel 调整不同的容错等级。 + +## en-US +set Error Level. + + + + + + + + + + diff --git a/components/qrcode/demo/icon.vue b/components/qrcode/demo/icon.vue new file mode 100644 index 000000000..8d63bda0e --- /dev/null +++ b/components/qrcode/demo/icon.vue @@ -0,0 +1,33 @@ + +--- +order: 1 +title: + zh-CN: 带 Icon 的例子 + en-US: With Icon +--- + +## zh-CN + +带 Icon 的二维码。 + +## en-US +QRCode with Icon. + + + + + + + diff --git a/components/qrcode/demo/index.vue b/components/qrcode/demo/index.vue new file mode 100644 index 000000000..41d7536ac --- /dev/null +++ b/components/qrcode/demo/index.vue @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/components/qrcode/demo/popover.vue b/components/qrcode/demo/popover.vue new file mode 100644 index 000000000..f4a69c9f1 --- /dev/null +++ b/components/qrcode/demo/popover.vue @@ -0,0 +1,34 @@ + +--- +order: 8 +title: + zh-CN: 高级用法 + en-US: Advanced Usage +--- + +## zh-CN + +带气泡卡片的例子。 + +## en-US +With Popover. + + + + + + + + + + + + diff --git a/components/qrcode/demo/status.vue b/components/qrcode/demo/status.vue new file mode 100644 index 000000000..283d10491 --- /dev/null +++ b/components/qrcode/demo/status.vue @@ -0,0 +1,35 @@ + +--- +order: 3 +title: + zh-CN: 不同的状态 + en-US: other status +--- + +## zh-CN + +可以通过 status 的值控制二维码的状态。 + +## en-US +The status can be controlled by the value `status`. + + + + + + + + + + diff --git a/components/qrcode/index.en-US.md b/components/qrcode/index.en-US.md new file mode 100644 index 000000000..fa5baa94c --- /dev/null +++ b/components/qrcode/index.en-US.md @@ -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) diff --git a/components/qrcode/index.tsx b/components/qrcode/index.tsx new file mode 100644 index 000000000..93a4cd6fb --- /dev/null +++ b/components/qrcode/index.tsx @@ -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>>; +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 () => ( + <> + + > + ); + }, +}); +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( + + {props.status !== 'active' && ( + + {props.status === 'loading' && } + {props.status === 'expired' && ( + <> + {locale.value.expired} + emit('refresh')}> + + {locale.value.refresh} + + > + )} + + )} + + , + ); + }; + }, +}); +export default withInstall(QRCode); diff --git a/components/qrcode/index.zh-CN.md b/components/qrcode/index.zh-CN.md new file mode 100644 index 000000000..01dac5c13 --- /dev/null +++ b/components/qrcode/index.zh-CN.md @@ -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) diff --git a/components/qrcode/interface.ts b/components/qrcode/interface.ts new file mode 100644 index 000000000..9fb51311b --- /dev/null +++ b/components/qrcode/interface.ts @@ -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; +} diff --git a/components/qrcode/style/index.ts b/components/qrcode/style/index.ts new file mode 100644 index 000000000..90ab300b4 --- /dev/null +++ b/components/qrcode/style/index.ts @@ -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 = 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(token, { + QRCodeExpiredTextColor: 'rgba(0, 0, 0, 0.88)', + QRCodeMaskBackgroundColor: 'rgba(255, 255, 255, 0.96)', + }), + ), +); diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index 50de8bdc2..f682f43d0 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -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. */ diff --git a/package.json b/package.json index 4595209a5..e5c6fdf0e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/typings/global.d.ts b/typings/global.d.ts index ea1102e25..b94bba26a 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -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 {};
{locale.value.expired}