Qrcode.vue 6.8 KB

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