index.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <view class="share-page">
  3. <!-- 生产:后端已生成带参小程序码海报,直接展示图片 -->
  4. <view class="poster-card" v-if="posterUrl">
  5. <image class="poster-image" :src="posterUrl" mode="widthFix"></image>
  6. </view>
  7. <!-- test-mode/降级:本地 canvas 渲染带邀请码海报 -->
  8. <view class="poster-card" v-else>
  9. <canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas"></canvas>
  10. </view>
  11. <view class="invite-row">
  12. <text class="invite-label">我的邀请码</text>
  13. <text class="invite-code">{{ inviteCode || '加载中…' }}</text>
  14. <button class="copy-btn" @click="copyCode" :disabled="!inviteCode">复制</button>
  15. </view>
  16. <view class="invite-row share-text-row" v-if="shareText">
  17. <text class="invite-label">分享文案</text>
  18. <text class="share-text-preview" @click="copyShareText">点击复制带邀请码的分享文案 ›</text>
  19. </view>
  20. <button class="save-btn" @click="savePoster" :loading="saving" :disabled="saving || !inviteCode">保存海报图片</button>
  21. <view class="stats-link" @click="goStats">
  22. <text>查看我的转介绍统计</text>
  23. <text class="arrow">›</text>
  24. </view>
  25. </view>
  26. </template>
  27. <script>
  28. import { getMyPoster } from '@/utils/api.js'
  29. import { guardPage } from '@/utils/guard.js'
  30. export default {
  31. data() {
  32. return {
  33. inviteCode: '',
  34. posterUrl: '',
  35. shareText: '',
  36. saving: false
  37. }
  38. },
  39. onLoad() {
  40. if (!guardPage()) return
  41. this.loadPoster()
  42. },
  43. onReady() {
  44. // canvas 就绪后绘制海报
  45. this.drawPoster()
  46. },
  47. methods: {
  48. loadPoster() {
  49. var self = this
  50. getMyPoster().then(function(resp) {
  51. var data = resp.data || {}
  52. self.inviteCode = data.inviteCode || ''
  53. self.posterUrl = data.posterUrl || ''
  54. self.shareText = data.shareText || ''
  55. if (self.inviteCode) {
  56. // 重新绘制带邀请码的海报
  57. setTimeout(function() {
  58. self.drawPoster()
  59. }, 300)
  60. }
  61. }).catch(function () {})
  62. },
  63. drawPoster() {
  64. var self = this
  65. var ctx = uni.createCanvasContext('posterCanvas', self)
  66. var width = 300
  67. var height = 450
  68. // 背景
  69. var gradient = ctx.createLinearGradient(0, 0, 0, height)
  70. gradient.addColorStop(0, '#FFF7ED')
  71. gradient.addColorStop(1, '#FED7AA')
  72. ctx.setFillStyle(gradient)
  73. ctx.fillRect(0, 0, width, height)
  74. // 顶部品牌区
  75. ctx.setFillStyle('#EA580C')
  76. ctx.fillRect(0, 0, width, 8)
  77. ctx.setFillStyle('#1E293B')
  78. ctx.setFontSize(22)
  79. ctx.setTextAlign('center')
  80. ctx.fillText('爱伴·AI之旅', width / 2, 60)
  81. ctx.setFillStyle('#64748B')
  82. ctx.setFontSize(13)
  83. ctx.fillText('我的专属邀请码', width / 2, 90)
  84. // 中央邀请码卡片
  85. ctx.setFillStyle('#FFFFFF')
  86. this.roundRect(ctx, 40, 130, width - 80, 120, 12)
  87. ctx.setFillStyle('#F97316')
  88. ctx.setFontSize(15)
  89. ctx.fillText('我的邀请码', width / 2, 165)
  90. ctx.setFillStyle('#1E293B')
  91. ctx.setFontSize(34)
  92. ctx.fillText(self.inviteCode || '------', width / 2, 215)
  93. // 底部提示
  94. ctx.setFillStyle('#EA580C')
  95. ctx.setFontSize(14)
  96. ctx.fillText('扫码加入训练营', width / 2, 320)
  97. // 二维码占位区(生产环境接入 getwxacodeunlimit 后绘制小程序码)
  98. ctx.setFillStyle('#FFFFFF')
  99. this.roundRect(ctx, width / 2 - 45, 340, 90, 90, 8)
  100. ctx.setStrokeStyle('#E2E8F0')
  101. ctx.setLineWidth(1)
  102. this.roundRect(ctx, width / 2 - 45, 340, 90, 90, 8)
  103. ctx.stroke()
  104. ctx.setFillStyle('#CBD5E1')
  105. ctx.setFontSize(11)
  106. ctx.fillText('小程序码', width / 2, 390)
  107. ctx.setFillStyle('#94A3B8')
  108. ctx.setFontSize(12)
  109. ctx.fillText('爱伴·AI之旅训练营', width / 2, 420)
  110. ctx.draw(false)
  111. },
  112. roundRect(ctx, x, y, w, h, r) {
  113. ctx.beginPath()
  114. ctx.moveTo(x + r, y)
  115. ctx.lineTo(x + w - r, y)
  116. ctx.arcTo(x + w, y, x + w, y + r, r)
  117. ctx.lineTo(x + w, y + h - r)
  118. ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
  119. ctx.lineTo(x + r, y + h)
  120. ctx.arcTo(x, y + h, x, y + h - r, r)
  121. ctx.lineTo(x, y + r)
  122. ctx.arcTo(x, y, x + r, y, r)
  123. ctx.closePath()
  124. ctx.fill()
  125. },
  126. copyCode() {
  127. if (!this.inviteCode) return
  128. uni.setClipboardData({
  129. data: this.inviteCode,
  130. success: function() {
  131. uni.showToast({ title: '邀请码已复制', icon: 'success' })
  132. }
  133. })
  134. },
  135. copyShareText() {
  136. if (!this.shareText) {
  137. uni.showToast({ title: '分享文案暂未生成', icon: 'none' })
  138. return
  139. }
  140. uni.setClipboardData({
  141. data: this.shareText,
  142. success: function() {
  143. uni.showToast({ title: '分享文案已复制', icon: 'success' })
  144. }
  145. })
  146. },
  147. savePoster() {
  148. var self = this
  149. if (this.saving) return
  150. this.saving = true
  151. // 后端已生成海报:保存远程图片到相册
  152. if (this.posterUrl) {
  153. uni.downloadFile({
  154. url: this.posterUrl,
  155. success: function(res) {
  156. if (res.statusCode !== 200) {
  157. self.saving = false
  158. uni.showToast({ title: '海报下载失败', icon: 'none' })
  159. return
  160. }
  161. uni.saveImageToPhotosAlbum({
  162. filePath: res.tempFilePath,
  163. success: function() {
  164. uni.showToast({ title: '海报已保存到相册', icon: 'success' })
  165. },
  166. fail: function() {
  167. self.saving = false
  168. uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
  169. },
  170. complete: function() {
  171. self.saving = false
  172. }
  173. })
  174. },
  175. fail: function() {
  176. self.saving = false
  177. uni.showToast({ title: '海报下载失败', icon: 'none' })
  178. }
  179. })
  180. return
  181. }
  182. // 本地 canvas 渲染海报
  183. uni.canvasToTempFilePath({
  184. canvasId: 'posterCanvas',
  185. success: function(res) {
  186. uni.saveImageToPhotosAlbum({
  187. filePath: res.tempFilePath,
  188. success: function() {
  189. uni.showToast({ title: '海报已保存到相册', icon: 'success' })
  190. },
  191. fail: function(err) {
  192. if (err && (err.errMsg || '').indexOf('auth') > -1) {
  193. uni.showModal({
  194. title: '需要相册权限',
  195. content: '请在设置中开启「保存到相册」权限',
  196. confirmText: '去设置',
  197. success: function(r) {
  198. if (r.confirm) {
  199. uni.openSetting({})
  200. }
  201. }
  202. })
  203. } else {
  204. uni.showToast({ title: '保存失败,请稍后重试', icon: 'none' })
  205. }
  206. },
  207. complete: function() {
  208. self.saving = false
  209. }
  210. })
  211. },
  212. fail: function() {
  213. self.saving = false
  214. uni.showToast({ title: '海报生成失败', icon: 'none' })
  215. }
  216. })
  217. },
  218. goStats() {
  219. uni.navigateTo({ url: '/pages/share/stats' })
  220. }
  221. }
  222. }
  223. </script>
  224. <style scoped>
  225. .share-page { min-height: 100vh; background: #F5F5F5; padding: 24rpx 32rpx; }
  226. .poster-card { background: #FFF; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; display: flex; justify-content: center; }
  227. .poster-canvas { width: 300px; height: 450px; border-radius: 12rpx; }
  228. .poster-image { width: 100%; border-radius: 12rpx; }
  229. .invite-row { background: #FFF; border-radius: 16rpx; padding: 32rpx; margin-bottom: 24rpx; display: flex; align-items: center; }
  230. .share-text-row { padding: 20rpx 32rpx; }
  231. .share-text-preview { flex: 1; font-size: 24rpx; color: #F97316; margin-left: 24rpx; line-height: 1.5; }
  232. .share-text-preview:active { opacity: 0.7; }
  233. .invite-label { font-size: 28rpx; color: #1E293B; font-weight: 600; flex-shrink: 0; }
  234. .invite-code { flex: 1; font-size: 32rpx; color: #F97316; font-weight: 700; font-family: monospace; margin-left: 24rpx; letter-spacing: 4rpx; }
  235. .copy-btn { flex-shrink: 0; height: 60rpx; line-height: 60rpx; padding: 0 32rpx; background: #F97316; color: #FFF; font-size: 26rpx; border-radius: 30rpx; border: none; }
  236. .copy-btn[disabled] { background: #FDBA74; color: #FFF; }
  237. .save-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: #F97316; color: #FFF; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; border: none; margin-top: 8rpx; }
  238. .save-btn:active { opacity: 0.85; }
  239. .stats-link { display: flex; align-items: center; justify-content: center; padding: 32rpx 0; color: #64748B; font-size: 28rpx; }
  240. .stats-link .arrow { color: #94A3B8; font-size: 32rpx; margin-left: 8rpx; }
  241. </style>