QrCode.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <template>
  2. <view class="qr-container">
  3. <canvas :id="canvasId" :canvas-id="canvasId" class="qr-canvas" :width="width" :height="height" :style="{
  4. width: width + 'px',
  5. height: height + 'px'
  6. }" />
  7. </view>
  8. </template>
  9. <script setup>
  10. import { onMounted, watch } from 'vue'
  11. import QRCode from 'qrcode'
  12. // TextEncoder polyfill for uni-app
  13. if (typeof TextEncoder === 'undefined') {
  14. globalThis.TextEncoder = class TextEncoder {
  15. encode(str) {
  16. const buf = new ArrayBuffer(str.length)
  17. const bufView = new Uint8Array(buf)
  18. for (let i = 0, strLen = str.length; i < strLen; i++) {
  19. bufView[i] = str.charCodeAt(i)
  20. }
  21. return bufView
  22. }
  23. }
  24. }
  25. // TextDecoder polyfill for uni-app
  26. if (typeof TextDecoder === 'undefined') {
  27. globalThis.TextDecoder = class TextDecoder {
  28. decode(uint8arr) {
  29. let encodedString = String.fromCharCode.apply(null, uint8arr)
  30. let decodedString = decodeURIComponent(escape(encodedString))
  31. return decodedString
  32. }
  33. }
  34. }
  35. const props = defineProps({
  36. /** 二维码内容 */
  37. text: {
  38. type: String,
  39. required: true
  40. },
  41. /** 宽高 */
  42. width: {
  43. type: Number,
  44. default: 200
  45. },
  46. height: {
  47. type: Number,
  48. default: 200
  49. },
  50. /** 颜色 */
  51. colorDark: {
  52. type: String,
  53. default: '#000000'
  54. },
  55. colorLight: {
  56. type: String,
  57. default: '#ffffff'
  58. },
  59. /** 中间 logo(可选) */
  60. logo: {
  61. type: String,
  62. default: ''
  63. },
  64. /** logo 占比 */
  65. logoScale: {
  66. type: Number,
  67. default: 0.22
  68. }
  69. })
  70. const canvasId = `qr_${Date.now()}`
  71. /** 绘制二维码 */
  72. async function drawQr() {
  73. console.log('开始绘制二维码:', props.text)
  74. if (!props.text) {
  75. console.warn('二维码文本为空')
  76. return
  77. }
  78. try {
  79. // 延迟确保canvas已初始化
  80. await new Promise(resolve => setTimeout(resolve, 100))
  81. const res = await QRCode.create(props.text, {
  82. errorCorrectionLevel: 'H'
  83. })
  84. // 获取canvas上下文
  85. const ctx = uni.createCanvasContext(canvasId)
  86. if (!ctx) {
  87. console.error('无法获取canvas上下文')
  88. return
  89. }
  90. const size = props.width
  91. const count = res.modules.size
  92. const cellSize = size / count
  93. // 背景
  94. ctx.setFillStyle(props.colorLight)
  95. ctx.fillRect(0, 0, size, size)
  96. // 绘制二维码
  97. for (let row = 0; row < count; row++) {
  98. for (let col = 0; col < count; col++) {
  99. ctx.setFillStyle(
  100. res.modules.get(row, col)
  101. ? props.colorDark
  102. : props.colorLight
  103. )
  104. ctx.fillRect(
  105. col * cellSize,
  106. row * cellSize,
  107. Math.ceil(cellSize),
  108. Math.ceil(cellSize)
  109. )
  110. }
  111. }
  112. // 绘制到canvas
  113. ctx.draw(false, () => {
  114. console.log('二维码绘制成功')
  115. // 绘制 logo
  116. if (props.logo) {
  117. drawLogo(ctx)
  118. }
  119. })
  120. } catch (e) {
  121. console.error('二维码绘制错误:')
  122. console.error('错误类型:', e.constructor.name)
  123. console.error('错误信息:', e.message)
  124. console.error('错误堆栈:', e.stack)
  125. uni.showToast({
  126. title: '二维码生成失败',
  127. icon: 'none',
  128. duration: 2000
  129. })
  130. }
  131. }
  132. /** 绘制中间 logo */
  133. function drawLogo(ctx) {
  134. const size = props.width
  135. const logoSize = size * props.logoScale
  136. const dx = (size - logoSize) / 2
  137. const dy = (size - logoSize) / 2
  138. // 绘制白色背景
  139. ctx.setFillStyle('#ffffff')
  140. ctx.fillRect(dx, dy, logoSize, logoSize)
  141. // 在真机上,确保图片已加载再绘制
  142. const img = new Image()
  143. img.src = props.logo
  144. img.onload = () => {
  145. ctx.drawImage(props.logo, dx, dy, logoSize, logoSize)
  146. ctx.draw(true)
  147. }
  148. img.onerror = () => {
  149. console.error('Logo图片加载失败:', props.logo)
  150. }
  151. }
  152. /** 下载二维码 */
  153. function download() {
  154. uni.canvasToTempFilePath({
  155. canvasId,
  156. success(res) {
  157. console.log('canvas转图片成功:', res.tempFilePath)
  158. uni.saveImageToPhotosAlbum({
  159. filePath: res.tempFilePath,
  160. success() {
  161. uni.showToast({ title: '已保存', icon: 'success' })
  162. },
  163. fail(err) {
  164. console.error('保存图片失败:', err)
  165. uni.showToast({ title: '保存失败', icon: 'error' })
  166. }
  167. })
  168. },
  169. fail(err) {
  170. console.error('canvas转图片失败:', err)
  171. uni.showToast({ title: '生成图片失败', icon: 'error' })
  172. }
  173. })
  174. }
  175. onMounted(() => {
  176. console.log('QrCode组件已挂载,准备绘制二维码')
  177. drawQr()
  178. })
  179. watch(() => props.text, () => {
  180. console.log('二维码文本变化,重新绘制:', props.text)
  181. drawQr()
  182. })
  183. defineExpose({
  184. download
  185. })
  186. </script>
  187. <style scoped lang="scss">
  188. @import "@/uni.scss";
  189. .qr-container {
  190. display: flex;
  191. justify-content: center;
  192. align-items: center;
  193. }
  194. .qr-canvas {
  195. background: transparent;
  196. }
  197. </style>