feat: 🎸 新增二维码组件

This commit is contained in:
chenkl
2021-01-19 11:25:10 +08:00
parent 32b6583099
commit 85555eef7d
11 changed files with 602 additions and 7 deletions

View File

@@ -0,0 +1,274 @@
<template>
<div v-loading="loading" class="qrcode__wrap" :style="wrapStyle">
<component :is="tag" ref="wrapRef" @click="clickCode" />
<div v-if="disabled" class="disabled__wrap" @click="disabledClick">
<div>
<i class="el-icon-refresh-right" />
<div>{{ disabledText }}</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, nextTick, ref, watch, computed, unref } from 'vue'
import type { LogoTypes } from './types'
import QRCode from 'qrcode'
import type { QRCodeRenderersOptions } from 'qrcode'
import { deepClone } from '@/utils'
import { isString } from '@/utils/is'
const { toCanvas, toDataURL } = QRCode
export default defineComponent({
name: 'Qrcode',
props: {
// img 或者 canvas,img不支持logo嵌套
tag: {
type: String as PropType<'canvas' | 'img'>,
default: 'canvas',
validator: (v: string) => ['canvas', 'img'].includes(v)
},
// 二维码内容
text: {
type: [String, Array] as PropType<string | any[]>,
default: null
},
// qrcode.js配置项
options: {
type: Object as PropType<QRCodeRenderersOptions>,
default: null
},
// 宽度
width: {
type: Number as PropType<number>,
default: 200
},
// logo
logo: {
type: [String, Object] as PropType<Partial<LogoTypes> | string>,
default: ''
},
// 是否过期
disabled: {
type: Boolean as PropType<boolean>,
default: false
},
// 过期提示内容
disabledText: {
type: String as PropType<string>,
default: '二维码已失效'
}
},
emits: ['done', 'click', 'disabled-click'],
setup(props, { emit }) {
const loading = ref<boolean>(true)
const wrapRef = ref<HTMLCanvasElement | HTMLImageElement | null>(null)
const renderText = computed(() => String(props.text))
const wrapStyle = computed(() => {
return {
width: props.width + 'px',
height: props.width + 'px'
}
})
watch(
() => renderText.value,
(val) => {
if (!val) return
initQrcode()
},
{
deep: true,
immediate: true
}
)
// 初始化
function initQrcode() {
nextTick(async() => {
const options = deepClone(props.options || {})
if (props.tag === 'canvas') {
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(renderText.value)
getOriginWidth(renderText.value, options).then(async(_width) => {
options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
const canvasRef: any = await toCanvas(unref(wrapRef as any), renderText.value, options)
if (props.logo) {
const url = await createLogoCode(canvasRef)
emit('done', url)
loading.value = false
} else {
emit('done', canvasRef.toDataURL())
loading.value = false
}
})
} else {
const url = await toDataURL(renderText.value, {
errorCorrectionLevel: 'H',
width: props.width,
...options
})
unref(wrapRef as any).src = url
emit('done', url)
loading.value = false
}
})
}
// 生成logo
function createLogoCode(canvasRef: HTMLCanvasElement) {
const canvasWidth = canvasRef.width
const logoOptions: LogoTypes = Object.assign({
logoSize: 0.15,
bgColor: '#ffffff',
borderSize: 0.05,
crossOrigin: 'anonymous',
borderRadius: 8,
logoRadius: 0
}, isString(props.logo) ? {} : props.logo)
const {
logoSize = 0.15,
bgColor = '#ffffff',
borderSize = 0.05,
crossOrigin = 'anonymous',
borderRadius = 8,
logoRadius = 0
} = logoOptions
const logoSrc = isString(props.logo) ? props.logo : props.logo.src
const logoWidth = canvasWidth * logoSize
const logoXY = (canvasWidth * (1 - logoSize)) / 2
const logoBgWidth = canvasWidth * (logoSize + borderSize)
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
const ctx = canvasRef.getContext('2d')
if (!ctx) return
// logo 底色
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
ctx.fillStyle = bgColor
ctx.fill()
// logo
const image = new Image()
if (crossOrigin || logoRadius) {
image.setAttribute('crossOrigin', crossOrigin)
}
(image as any).src = logoSrc
// 使用image绘制可以避免某些跨域情况
const drawLogoWithImage = (image: HTMLImageElement) => {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
}
// 使用canvas绘制以获得更多的功能
const drawLogoWithCanvas = (image: HTMLImageElement) => {
const canvasImage = document.createElement('canvas')
canvasImage.width = logoXY + logoWidth
canvasImage.height = logoXY + logoWidth
const imageCanvas = canvasImage.getContext('2d')
if (!imageCanvas || !ctx) return
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
if (!ctx) return
const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
if (fillStyle) {
ctx.fillStyle = fillStyle
ctx.fill()
}
}
// 将 logo绘制到 canvas上
return new Promise((resolve: any) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
resolve(canvasRef.toDataURL())
}
})
}
// 得到原QrCode的大小以便缩放得到正确的QrCode大小
function getOriginWidth(content: string, options: QRCodeRenderersOptions) {
const _canvas = document.createElement('canvas')
return toCanvas(_canvas, content, options).then(() => _canvas.width)
}
// 对于内容少的QrCode增大容错率
function getErrorCorrectionLevel(content: string) {
if (content.length > 36) {
return 'M'
} else if (content.length > 16) {
return 'Q'
} else {
return 'H'
}
}
// 点击二维码
function clickCode() {
emit('click')
}
// 失效点击事件
function disabledClick() {
emit('disabled-click')
}
// copy来的方法用于绘制圆角
function canvasRoundRect(ctx: CanvasRenderingContext2D) {
return (x: number, y: number, w: number, h: number, r: number) => {
const minSize = Math.min(w, h)
if (r > minSize / 2) {
r = minSize / 2
}
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.arcTo(x + w, y, x + w, y + h, r)
ctx.arcTo(x + w, y + h, x, y + h, r)
ctx.arcTo(x, y + h, x, y, r)
ctx.arcTo(x, y, x + w, y, r)
ctx.closePath()
return ctx
}
}
return {
loading,
wrapRef,
renderText,
wrapStyle,
clickCode,
disabledClick
}
}
})
</script>
<style lang="less" scoped>
.qrcode__wrap {
display: inline-block;
position: relative;
.disabled__wrap {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.95);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&>div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
i {
font-size: 30px;
margin-bottom: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,9 @@
export interface LogoTypes {
src?: string
logoSize?: number
bgColor?: string
borderSize?: number
crossOrigin?: string
borderRadius?: number
logoRadius?: number
}

View File

@@ -197,6 +197,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: {
title: '详情组件'
}
},
{
path: 'qrcode',
component: () => import('_p/index/views/components-demo/qrcode/index.vue'),
name: 'QrcodeDemo',
meta: {
title: '二维码组件'
}
}
]
},

View File

@@ -0,0 +1,106 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="6">
<div class="title-item">基础用法默认canvas</div>
<qrcode text="vue-element-admin2.0" />
</el-col>
<el-col :span="6">
<div class="title-item">img标签</div>
<qrcode text="vue-element-admin2.0" tag="img" />
</el-col>
<el-col :span="6">
<div class="title-item">样式配置</div>
<qrcode
text="vue-element-admin2.0"
:options="{
color: {
dark: '#55D187',
light: '#2d8cf0'
}
}"
/>
</el-col>
<el-col :span="6">
<div class="title-item">点击</div>
<qrcode
text="vue-element-admin2.0"
@click="codeClick"
/>
</el-col>
<el-col :span="6">
<div class="title-item">异步内容</div>
<qrcode :text="text" />
</el-col>
<el-col :span="6">
<div class="title-item">二维码失效</div>
<qrcode text="vue-element-admin2.0" :disabled="true" @disabled-click="disabledClick" />
</el-col>
<el-col :span="6">
<div class="title-item">logo配置</div>
<qrcode
text="vue-element-admin2.0"
:logo="require('@/assets/img/logo.png')"
/>
</el-col>
<el-col :span="6">
<div class="title-item">logo样式配置</div>
<qrcode
text="vue-element-admin2.0"
:logo="{
src: require('@/assets/img/logo.png'),
logoSize: 0.2,
borderSize: 0.05,
borderRadius: 50,
bgColor: 'blue'
}"
/>
</el-col>
<el-col :span="6">
<div class="title-item">大小配置</div>
<qrcode text="vue-element-admin2.0" :width="300" />
</el-col>
</el-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import Qrcode from '_c/Qrcode/index.vue'
import { Message } from '_c/Message'
export default defineComponent({
// name: 'QrcodeDemo',
components: {
Qrcode
},
setup() {
const text = ref<string>('')
setTimeout(() => {
text.value = '我是异步生成的内容'
}, 3000)
function codeClick() {
Message.info('我被点击了。')
}
function disabledClick() {
Message.info('我失效被点击了。')
}
return {
text,
codeClick,
disabledClick
}
}
})
</script>
<style lang="less" scoped>
.el-col {
text-align: center;
margin-bottom: 20px;
.title-item {
font-weight: bold;
margin-bottom: 10px;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="login-wrap">
<div class="login-wrap" @keydown.enter="login">
<div class="login-con">
<el-card class="box-card">
<template #header>