QrCode.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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()}_${Math.floor(Math.random() * 10000000000).toString().padStart(10, '0')}`
  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. // #ifdef H5
  142. // H5端可直接用图片url
  143. ctx.drawImage(props.logo, dx, dy, logoSize, logoSize)
  144. ctx.draw(true)
  145. // #endif
  146. // #ifndef H5
  147. // 小程序/APP端需先下载图片到本地再绘制
  148. uni.getImageInfo({
  149. src: props.logo,
  150. success(res) {
  151. ctx.drawImage(res.path, dx, dy, logoSize, logoSize)
  152. ctx.draw(true)
  153. },
  154. fail() {
  155. console.error('Logo图片加载失败:', props.logo)
  156. }
  157. })
  158. // #endif
  159. }
  160. /** 下载二维码 */
  161. function download() {
  162. // 延迟一点时间确保画布渲染完成
  163. setTimeout(() => {
  164. uni.canvasToTempFilePath({
  165. canvasId: canvasId,
  166. success(res) {
  167. console.log('canvas转图片成功:', res.tempFilePath)
  168. // #ifdef H5
  169. // H5端下载处理
  170. const a = document.createElement('a')
  171. a.href = res.tempFilePath
  172. a.download = 'qrcode.png'
  173. document.body.appendChild(a)
  174. a.click()
  175. document.body.removeChild(a)
  176. uni.showToast({ title: '已保存', icon: 'success' })
  177. // #endif
  178. // #ifndef H5
  179. // 移动端下载处理
  180. uni.saveImageToPhotosAlbum({
  181. filePath: res.tempFilePath,
  182. success() {
  183. uni.showToast({ title: '已保存', icon: 'success' })
  184. },
  185. fail(err) {
  186. console.error('保存图片失败:', err)
  187. // 如果是没有权限,提示用户授权
  188. if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny' || err.errMsg === 'saveImageToPhotosAlbum:fail:auth denied') {
  189. uni.showModal({
  190. title: '提示',
  191. content: '需要您授权保存相册',
  192. success: (modalRes) => {
  193. if (modalRes.confirm) {
  194. uni.openSetting({
  195. success(settingdata) {
  196. if (settingdata.authSetting['scope.writePhotosAlbum']) {
  197. uni.showToast({ title: '获取权限成功,再次点击图片即可保存', icon: 'none' })
  198. } else {
  199. uni.showToast({ title: '获取权限失败', icon: 'none' })
  200. }
  201. }
  202. })
  203. }
  204. }
  205. })
  206. } else {
  207. uni.showToast({ title: '保存失败', icon: 'error' })
  208. }
  209. }
  210. })
  211. // #endif
  212. },
  213. fail(err) {
  214. console.error('canvas转图片失败:', err)
  215. uni.showToast({ title: '生成图片失败', icon: 'error' })
  216. }
  217. })
  218. }, 200)
  219. }
  220. /** 清空二维码 */
  221. function clear() {
  222. const ctx = uni.createCanvasContext(canvasId)
  223. if (!ctx) return
  224. // 清除画布区域
  225. ctx.clearRect(0, 0, props.width, props.height)
  226. // 绘制透明/白色背景
  227. ctx.setFillStyle(props.colorLight)
  228. ctx.fillRect(0, 0, props.width, props.height)
  229. ctx.draw()
  230. console.log('二维码已清空')
  231. }
  232. onMounted(() => {
  233. console.log('QrCode组件已挂载,准备绘制二维码')
  234. drawQr()
  235. })
  236. watch(() => props.text, (newVal) => {
  237. console.log('二维码文本变化:', newVal)
  238. if (!newVal) {
  239. clear()
  240. } else {
  241. drawQr()
  242. }
  243. })
  244. defineExpose({
  245. download,
  246. clear,
  247. drawQr
  248. })
  249. </script>
  250. <style scoped lang="scss">
  251. @import "@/uni.scss";
  252. .qr-container {
  253. display: flex;
  254. justify-content: center;
  255. align-items: center;
  256. }
  257. .qr-canvas {
  258. background: transparent;
  259. }
  260. </style>