提交 | 用户 | 时间
|
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> |