houzhongjian
2024-08-08 820397e43a0b64d35c6d31d2a55475061438593b
提交 | 用户 | 时间
820397 1 <script lang="ts" setup>
H 2 import { computed, nextTick, PropType, ref, unref, watch } from 'vue'
3 import QRCode, { QRCodeRenderersOptions } from 'qrcode'
4 import { cloneDeep } from 'lodash-es'
5 import { propTypes } from '@/utils/propTypes'
6 import { useDesign } from '@/hooks/web/useDesign'
7 import { isString } from '@/utils/is'
8 import { QrcodeLogo } from '@/types/qrcode'
9
10 defineOptions({ name: 'Qrcode' })
11
12 const props = defineProps({
13   // img 或者 canvas,img不支持logo嵌套
14   tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
15   // 二维码内容
16   text: {
17     type: [String, Array] as PropType<string | Recordable[]>,
18     default: null
19   },
20   // qrcode.js配置项
21   options: {
22     type: Object as PropType<QRCodeRenderersOptions>,
23     default: () => ({})
24   },
25   // 宽度
26   width: propTypes.number.def(200),
27   // logo
28   logo: {
29     type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
30     default: ''
31   },
32   // 是否过期
33   disabled: propTypes.bool.def(false),
34   // 过期提示内容
35   disabledText: propTypes.string.def('')
36 })
37
38 const emit = defineEmits(['done', 'click', 'disabled-click'])
39
40 const { getPrefixCls } = useDesign()
41
42 const prefixCls = getPrefixCls('qrcode')
43
44 const { toCanvas, toDataURL } = QRCode
45
46 const loading = ref(true)
47
48 const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null)
49
50 const renderText = computed(() => String(props.text))
51
52 const wrapStyle = computed(() => {
53   return {
54     width: props.width + 'px',
55     height: props.width + 'px'
56   }
57 })
58
59 const initQrcode = async () => {
60   await nextTick()
61   const options = cloneDeep(props.options || {})
62   if (props.tag === 'canvas') {
63     // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
64     options.errorCorrectionLevel =
65       options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText))
66     const _width: number = await getOriginWidth(unref(renderText), options)
67     options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
68     const canvasRef: HTMLCanvasElement | any = await toCanvas(
69       unref(wrapRef) as HTMLCanvasElement,
70       unref(renderText),
71       options
72     )
73     if (props.logo) {
74       const url = await createLogoCode(canvasRef)
75       emit('done', url)
76       loading.value = false
77     } else {
78       emit('done', canvasRef.toDataURL())
79       loading.value = false
80     }
81   } else {
82     const url = await toDataURL(renderText.value, {
83       errorCorrectionLevel: 'H',
84       width: props.width,
85       ...options
86     })
87     ;(unref(wrapRef) as HTMLImageElement).src = url
88     emit('done', url)
89     loading.value = false
90   }
91 }
92
93 watch(
94   () => renderText.value,
95   (val) => {
96     if (!val) return
97     initQrcode()
98   },
99   {
100     deep: true,
101     immediate: true
102   }
103 )
104
105 const createLogoCode = (canvasRef: HTMLCanvasElement) => {
106   const canvasWidth = canvasRef.width
107   const logoOptions: QrcodeLogo = Object.assign(
108     {
109       logoSize: 0.15,
110       bgColor: '#ffffff',
111       borderSize: 0.05,
112       crossOrigin: 'anonymous',
113       borderRadius: 8,
114       logoRadius: 0
115     },
116     isString(props.logo) ? {} : props.logo
117   )
118   const {
119     logoSize = 0.15,
120     bgColor = '#ffffff',
121     borderSize = 0.05,
122     crossOrigin = 'anonymous',
123     borderRadius = 8,
124     logoRadius = 0
125   } = logoOptions
126   const logoSrc = isString(props.logo) ? props.logo : props.logo.src
127   const logoWidth = canvasWidth * logoSize
128   const logoXY = (canvasWidth * (1 - logoSize)) / 2
129   const logoBgWidth = canvasWidth * (logoSize + borderSize)
130   const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
131
132   const ctx = canvasRef.getContext('2d')
133   if (!ctx) return
134
135   // logo 底色
136   canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
137   ctx.fillStyle = bgColor
138   ctx.fill()
139
140   // logo
141   const image = new Image()
142   if (crossOrigin || logoRadius) {
143     image.setAttribute('crossOrigin', crossOrigin)
144   }
145   ;(image as any).src = logoSrc
146
147   // 使用image绘制可以避免某些跨域情况
148   const drawLogoWithImage = (image: HTMLImageElement) => {
149     ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
150   }
151
152   // 使用canvas绘制以获得更多的功能
153   const drawLogoWithCanvas = (image: HTMLImageElement) => {
154     const canvasImage = document.createElement('canvas')
155     canvasImage.width = logoXY + logoWidth
156     canvasImage.height = logoXY + logoWidth
157     const imageCanvas = canvasImage.getContext('2d')
158     if (!imageCanvas || !ctx) return
159     imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
160
161     canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
162     if (!ctx) return
163     const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
164     if (fillStyle) {
165       ctx.fillStyle = fillStyle
166       ctx.fill()
167     }
168   }
169
170   // 将 logo绘制到 canvas上
171   return new Promise((resolve: any) => {
172     image.onload = () => {
173       logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
174       resolve(canvasRef.toDataURL())
175     }
176   })
177 }
178
179 // 得到原QrCode的大小,以便缩放得到正确的QrCode大小
180 const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
181   const _canvas = document.createElement('canvas')
182   await toCanvas(_canvas, content, options)
183   return _canvas.width
184 }
185
186 // 对于内容少的QrCode,增大容错率
187 const getErrorCorrectionLevel = (content: string) => {
188   if (content.length > 36) {
189     return 'M'
190   } else if (content.length > 16) {
191     return 'Q'
192   } else {
193     return 'H'
194   }
195 }
196
197 // copy来的方法,用于绘制圆角
198 const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
199   return (x: number, y: number, w: number, h: number, r: number) => {
200     const minSize = Math.min(w, h)
201     if (r > minSize / 2) {
202       r = minSize / 2
203     }
204     ctx.beginPath()
205     ctx.moveTo(x + r, y)
206     ctx.arcTo(x + w, y, x + w, y + h, r)
207     ctx.arcTo(x + w, y + h, x, y + h, r)
208     ctx.arcTo(x, y + h, x, y, r)
209     ctx.arcTo(x, y, x + w, y, r)
210     ctx.closePath()
211     return ctx
212   }
213 }
214
215 const clickCode = () => {
216   emit('click')
217 }
218
219 const disabledClick = () => {
220   emit('disabled-click')
221 }
222 </script>
223
224 <template>
225   <div v-loading="loading" :class="[prefixCls, 'relative inline-block']" :style="wrapStyle">
226     <component :is="tag" ref="wrapRef" @click="clickCode" />
227     <div
228       v-if="disabled"
229       :class="`${prefixCls}--disabled`"
230       class="absolute left-0 top-0 h-full w-full flex items-center justify-center"
231       @click="disabledClick"
232     >
233       <div class="absolute left-[50%] top-[50%] font-bold">
234         <Icon :size="30" color="var(--el-color-primary)" icon="ep:refresh-right" />
235         <div>{{ disabledText }}</div>
236       </div>
237     </div>
238   </div>
239 </template>
240
241 <style lang="scss" scoped>
242 $prefix-cls: #{$namespace}-qrcode;
243
244 .#{$prefix-cls} {
245   &--disabled {
246     background: rgb(255 255 255 / 95%);
247
248     & > div {
249       transform: translate(-50%, -50%);
250     }
251   }
252 }
253 </style>