index.vue 6.2 KB

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