SharePoster.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <template>
  2. <view class="poster-overlay" v-if="show" @click.self="$emit('close')">
  3. <view class="poster-container">
  4. <canvas canvas-id="sharePosterCanvas" class="poster-canvas" id="sharePosterCanvas"></canvas>
  5. <view class="poster-actions">
  6. <button class="poster-btn save" @click="savePoster">保存到相册</button>
  7. <button class="poster-btn cancel" @click="$emit('close')">取消</button>
  8. </view>
  9. </view>
  10. </view>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'SharePoster',
  15. props: {
  16. show: { type: Boolean, default: false },
  17. userAvatar: { type: String, default: '' },
  18. nickname: { type: String, default: '' },
  19. referralCode: { type: String, default: '' },
  20. qrCodeBase64: { type: String, default: '' }
  21. },
  22. watch: {
  23. show(val) {
  24. if (val) {
  25. this.$nextTick(function() {
  26. setTimeout(function() {
  27. this.drawPoster()
  28. }.bind(this), 300)
  29. }.bind(this))
  30. }
  31. }
  32. },
  33. methods: {
  34. drawPoster() {
  35. var ctx = uni.createCanvasContext('sharePosterCanvas', this)
  36. var w = 500
  37. var h = 700
  38. // 1. Orange gradient background
  39. var gradient = ctx.createLinearGradient(0, 0, w, h)
  40. gradient.addColorStop(0, '#F97316')
  41. gradient.addColorStop(1, '#EA580C')
  42. ctx.setFillStyle(gradient)
  43. ctx.fillRect(0, 0, w, h)
  44. // 2. White circle border for avatar
  45. var avatarX = w / 2
  46. var avatarY = 120
  47. var avatarR = 50
  48. ctx.setStrokeStyle('#ffffff')
  49. ctx.setLineWidth(3)
  50. ctx.beginPath()
  51. ctx.arc(avatarX, avatarY, avatarR + 3, 0, 2 * Math.PI)
  52. ctx.stroke()
  53. // 3. Draw avatar or default icon
  54. if (this.userAvatar) {
  55. var self = this
  56. uni.downloadFile({
  57. url: this.userAvatar,
  58. success: function(res) {
  59. if (res.statusCode === 200) {
  60. ctx.save()
  61. ctx.beginPath()
  62. ctx.arc(avatarX, avatarY, avatarR, 0, 2 * Math.PI)
  63. ctx.clip()
  64. ctx.drawImage(res.tempFilePath, avatarX - avatarR, avatarY - avatarR, avatarR * 2, avatarR * 2)
  65. ctx.restore()
  66. self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
  67. } else {
  68. self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  69. self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
  70. }
  71. },
  72. fail: function() {
  73. self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  74. self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
  75. }
  76. })
  77. } else {
  78. this._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  79. this._drawTextAndQr(ctx, w, h, avatarX, avatarY)
  80. }
  81. },
  82. _drawDefaultAvatar(ctx, x, y, r) {
  83. ctx.setFillStyle('#ffffff')
  84. ctx.beginPath()
  85. ctx.arc(x, y, r, 0, 2 * Math.PI)
  86. ctx.fill()
  87. ctx.setFillStyle('#F97316')
  88. ctx.setFontSize(40)
  89. ctx.setTextAlign('center')
  90. ctx.setTextBaseline('middle')
  91. ctx.fillText('\u{1F464}', x, y)
  92. },
  93. _drawTextAndQr(ctx, w, h, avatarX, avatarY) {
  94. var self = this
  95. // 4. Nickname
  96. var nameY = avatarY + 80
  97. ctx.setFillStyle('#ffffff')
  98. ctx.setFontSize(28)
  99. ctx.setTextAlign('center')
  100. ctx.setTextBaseline('top')
  101. ctx.fillText(this.nickname || '\u597d\u53cb', w / 2, nameY)
  102. // 5. "我的邀请码" label
  103. var labelY = nameY + 50
  104. ctx.setFontSize(20)
  105. ctx.fillText('\u6211\u7684\u9080\u8bf7\u7801', w / 2, labelY)
  106. // 6. Referral code in large bold text
  107. var codeY = labelY + 36
  108. ctx.setFontSize(48)
  109. ctx.fillText(this.referralCode || '', w / 2, codeY)
  110. // 7. QR code
  111. var qrSize = 220
  112. var qrX = (w - qrSize) / 2
  113. var qrY = codeY + 70
  114. if (this.qrCodeBase64) {
  115. // Build a file path from base64 data
  116. var base64Data = this.qrCodeBase64
  117. // Handle data URI prefix
  118. if (base64Data.indexOf('data:image') === 0) {
  119. var commaIdx = base64Data.indexOf(',')
  120. if (commaIdx > -1) {
  121. base64Data = base64Data.substring(commaIdx + 1)
  122. }
  123. }
  124. var fs = uni.getFileSystemManager()
  125. var filePath = wx.env.USER_DATA_PATH + '/share_qr_tmp.png'
  126. fs.writeFile({
  127. filePath: filePath,
  128. data: base64Data,
  129. encoding: 'base64',
  130. success: function() {
  131. // White background behind QR
  132. ctx.setFillStyle('#ffffff')
  133. ctx.fillRect(qrX - 10, qrY - 10, qrSize + 20, qrSize + 20)
  134. ctx.drawImage(filePath, qrX, qrY, qrSize, qrSize)
  135. self._drawHintAndFinish(ctx, w, h, qrY, qrSize)
  136. },
  137. fail: function() {
  138. self._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
  139. self._drawHintAndFinish(ctx, w, h, qrY, qrSize)
  140. }
  141. })
  142. } else {
  143. this._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
  144. this._drawHintAndFinish(ctx, w, h, qrY, qrSize)
  145. }
  146. },
  147. _drawQrPlaceholder(ctx, x, y, size) {
  148. ctx.setFillStyle('#ffffff')
  149. ctx.fillRect(x - 10, y - 10, size + 20, size + 20)
  150. ctx.setFillStyle('#F97316')
  151. ctx.setFontSize(18)
  152. ctx.setTextAlign('center')
  153. ctx.setTextBaseline('middle')
  154. ctx.fillText('\u4e8c\u7ef4\u7801\u52a0\u8f7d\u4e2d', x + size / 2, y + size / 2)
  155. },
  156. _drawHintAndFinish(ctx, w, h, qrY, qrSize) {
  157. // 8. Hint text below QR
  158. var hintY = qrY + qrSize + 20
  159. ctx.setFillStyle('#ffffff')
  160. ctx.setFontSize(22)
  161. ctx.setTextAlign('center')
  162. ctx.setTextBaseline('top')
  163. ctx.fillText('\u626b\u7801\u9080\u8bf7\u597d\u53cb\u52a0\u5165', w / 2, hintY)
  164. ctx.draw()
  165. },
  166. savePoster() {
  167. var self = this
  168. uni.canvasToTempFilePath({
  169. canvasId: 'sharePosterCanvas',
  170. success: function(res) {
  171. uni.saveImageToPhotosAlbum({
  172. filePath: res.tempFilePath,
  173. success: function() {
  174. uni.showToast({ title: '\u5df2\u4fdd\u5b58\u5230\u76f8\u518c', icon: 'success' })
  175. self.$emit('close')
  176. },
  177. fail: function(err) {
  178. if (err && err.errMsg && (err.errMsg.indexOf('deny') > -1 || err.errMsg.indexOf('auth denied') > -1 || err.errMsg.indexOf('authorize') > -1)) {
  179. uni.showModal({
  180. title: '\u63d0\u793a',
  181. content: '\u9700\u8981\u76f8\u518c\u6743\u9650\u624d\u80fd\u4fdd\u5b58\u56fe\u7247',
  182. confirmText: '\u53bb\u8bbe\u7f6e',
  183. success: function(modalRes) {
  184. if (modalRes.confirm) {
  185. uni.openSetting()
  186. }
  187. }
  188. })
  189. } else {
  190. uni.showToast({ title: '\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5', icon: 'none' })
  191. }
  192. }
  193. })
  194. },
  195. fail: function() {
  196. uni.showToast({ title: '\u751f\u6210\u56fe\u7247\u5931\u8d25', icon: 'none' })
  197. }
  198. }, this)
  199. }
  200. }
  201. }
  202. </script>
  203. <style scoped>
  204. .poster-overlay {
  205. position: fixed;
  206. top: 0;
  207. left: 0;
  208. right: 0;
  209. bottom: 0;
  210. background: rgba(0, 0, 0, 0.6);
  211. display: flex;
  212. align-items: center;
  213. justify-content: center;
  214. z-index: 999;
  215. }
  216. .poster-container {
  217. display: flex;
  218. flex-direction: column;
  219. align-items: center;
  220. }
  221. .poster-canvas {
  222. width: 500px;
  223. height: 700px;
  224. border-radius: 16rpx;
  225. }
  226. .poster-actions {
  227. display: flex;
  228. flex-direction: column;
  229. width: 500px;
  230. margin-top: 30rpx;
  231. }
  232. .poster-btn {
  233. height: 80rpx;
  234. line-height: 80rpx;
  235. font-size: 30rpx;
  236. border-radius: 40rpx;
  237. text-align: center;
  238. border: none;
  239. }
  240. .poster-btn::after { border: none; }
  241. .poster-btn.save {
  242. background: #ffffff;
  243. color: #F97316;
  244. font-weight: 600;
  245. margin-bottom: 16rpx;
  246. }
  247. .poster-btn.save:active { opacity: 0.85; }
  248. .poster-btn.cancel {
  249. background: rgba(255, 255, 255, 0.15);
  250. color: #ffffff;
  251. }
  252. .poster-btn.cancel:active { opacity: 0.85; }
  253. </style>