SharePoster.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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. familyName: { type: String, default: '' },
  21. qrCodeBase64: { type: String, default: '' }
  22. },
  23. watch: {
  24. show(val) {
  25. if (val) {
  26. this.$nextTick(function() {
  27. setTimeout(function() {
  28. this.drawPoster()
  29. }.bind(this), 300)
  30. }.bind(this))
  31. }
  32. }
  33. },
  34. methods: {
  35. // ============================================================
  36. // ENTRY POINT
  37. // ============================================================
  38. drawPoster() {
  39. var ctx = uni.createCanvasContext('sharePosterCanvas', this)
  40. var w = 500
  41. var h = 750
  42. // 1. Background gradient — warm orange
  43. var gradient = ctx.createLinearGradient(0, 0, w * 0.3, h)
  44. gradient.addColorStop(0, '#F97316')
  45. gradient.addColorStop(0.55, '#EA580C')
  46. gradient.addColorStop(1, '#D94F0A')
  47. ctx.setFillStyle(gradient)
  48. ctx.fillRect(0, 0, w, h)
  49. // 2. Top decorative area (brand, tagline, dimension dots, divider)
  50. this._drawTopDecoration(ctx, w)
  51. // 3. White/cream content card
  52. var cardX = 28
  53. var cardY = 112
  54. var cardW = 444
  55. var cardH = 528
  56. this._drawContentCard(ctx, cardX, cardY, cardW, cardH)
  57. // 4. Avatar decorative frame (drawn before async avatar download)
  58. var avatarX = w / 2
  59. var avatarY = cardY + 62
  60. var avatarR = 48
  61. this._drawAvatarFrame(ctx, avatarX, avatarY, avatarR)
  62. // 5. Download avatar (async) then draw remaining content
  63. var self = this
  64. if (this.userAvatar) {
  65. uni.downloadFile({
  66. url: this.userAvatar,
  67. success: function(res) {
  68. if (res.statusCode === 200) {
  69. ctx.save()
  70. ctx.beginPath()
  71. ctx.arc(avatarX, avatarY, avatarR, 0, 2 * Math.PI)
  72. ctx.clip()
  73. ctx.drawImage(res.tempFilePath, avatarX - avatarR, avatarY - avatarR, avatarR * 2, avatarR * 2)
  74. ctx.restore()
  75. } else {
  76. self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  77. }
  78. self._drawContentAfterAvatar(ctx, w, h, cardX, cardW, avatarX, avatarY)
  79. },
  80. fail: function() {
  81. self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  82. self._drawContentAfterAvatar(ctx, w, h, cardX, cardW, avatarX, avatarY)
  83. }
  84. })
  85. } else {
  86. this._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
  87. this._drawContentAfterAvatar(ctx, w, h, cardX, cardW, avatarX, avatarY)
  88. }
  89. },
  90. // ============================================================
  91. // TOP DECORATION — brand, tagline, dimension dots, divider
  92. // ============================================================
  93. _drawTopDecoration(ctx, w) {
  94. var centerX = w / 2
  95. var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
  96. var dotY = 28
  97. var dotR = 5
  98. var dotSpacing = 26
  99. var i, offsetX, offsetY
  100. // 5 dimension dots in a gentle arc
  101. for (i = 0; i < 5; i++) {
  102. offsetX = (i - 2) * dotSpacing
  103. offsetY = Math.abs(i - 2) * 3
  104. ctx.setFillStyle(dotColors[i])
  105. ctx.beginPath()
  106. ctx.arc(centerX + offsetX, dotY + offsetY, dotR, 0, 2 * Math.PI)
  107. ctx.fill()
  108. }
  109. // Brand name
  110. ctx.setFillStyle('#ffffff')
  111. ctx.setFontSize(18)
  112. ctx.setTextAlign('center')
  113. ctx.setTextBaseline('top')
  114. ctx.fillText('浠艾福', centerX, 52)
  115. // Tagline
  116. ctx.setFontSize(12)
  117. ctx.setFillStyle('rgba(255,255,255,0.8)')
  118. ctx.fillText('给全家的一站式幸福提案', centerX, 76)
  119. // Dotted divider
  120. ctx.setStrokeStyle('rgba(255,255,255,0.25)')
  121. ctx.setLineWidth(1)
  122. ctx.beginPath()
  123. var dashX = 80
  124. var dashY = 100
  125. ctx.moveTo(dashX, dashY)
  126. ctx.lineTo(w - dashX, dashY)
  127. ctx.stroke()
  128. },
  129. // ============================================================
  130. // CONTENT CARD — warm white rounded rect with bracket corners
  131. // ============================================================
  132. _drawContentCard(ctx, x, y, w, h) {
  133. // Shadow/3D offset
  134. ctx.setFillStyle('rgba(0,0,0,0.04)')
  135. this._roundRect(ctx, x + 2, y + 3, w, h, 16)
  136. ctx.fill()
  137. // Main white card
  138. ctx.setFillStyle('#FFFCF5')
  139. this._roundRect(ctx, x, y, w, h, 16)
  140. ctx.fill()
  141. // Corner decorative brackets
  142. var br = 10
  143. var bl = 22
  144. ctx.setStrokeStyle('#F97316')
  145. ctx.setLineWidth(1.5)
  146. // Top-left
  147. ctx.beginPath()
  148. ctx.moveTo(x + br, y + br + bl)
  149. ctx.lineTo(x + br, y + br)
  150. ctx.lineTo(x + br + bl, y + br)
  151. ctx.stroke()
  152. // Top-right
  153. ctx.beginPath()
  154. ctx.moveTo(x + w - br - bl, y + br)
  155. ctx.lineTo(x + w - br, y + br)
  156. ctx.lineTo(x + w - br, y + br + bl)
  157. ctx.stroke()
  158. // Bottom-left
  159. ctx.beginPath()
  160. ctx.moveTo(x + br, y + h - br - bl)
  161. ctx.lineTo(x + br, y + h - br)
  162. ctx.lineTo(x + br + bl, y + h - br)
  163. ctx.stroke()
  164. // Bottom-right
  165. ctx.beginPath()
  166. ctx.moveTo(x + w - br - bl, y + h - br)
  167. ctx.lineTo(x + w - br, y + h - br)
  168. ctx.lineTo(x + w - br, y + h - br - bl)
  169. ctx.stroke()
  170. },
  171. // ============================================================
  172. // AVATAR FRAME — dotted outer ring + solid inner ring
  173. // ============================================================
  174. _drawAvatarFrame(ctx, x, y, r) {
  175. // Outer dotted ring
  176. ctx.setStrokeStyle('rgba(249,115,22,0.2)')
  177. ctx.setLineWidth(1.5)
  178. ctx.beginPath()
  179. ctx.arc(x, y, r + 12, 0, 2 * Math.PI)
  180. ctx.stroke()
  181. // Solid ring
  182. ctx.setStrokeStyle('#F97316')
  183. ctx.setLineWidth(2.5)
  184. ctx.beginPath()
  185. ctx.arc(x, y, r + 4, 0, 2 * Math.PI)
  186. ctx.stroke()
  187. },
  188. // ============================================================
  189. // ROUNDED RECTANGLE HELPER
  190. // ============================================================
  191. _roundRect(ctx, x, y, w, h, r) {
  192. ctx.beginPath()
  193. ctx.moveTo(x + r, y)
  194. ctx.arcTo(x + w, y, x + w, y + r, r)
  195. ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
  196. ctx.arcTo(x, y + h, x, y + h - r, r)
  197. ctx.arcTo(x, y, x + r, y, r)
  198. ctx.closePath()
  199. },
  200. // ============================================================
  201. // DEFAULT AVATAR (fallback when image download fails)
  202. // ============================================================
  203. _drawDefaultAvatar(ctx, x, y, r) {
  204. // Warm cream circle background
  205. ctx.setFillStyle('#FFF0E0')
  206. ctx.beginPath()
  207. ctx.arc(x, y, r, 0, 2 * Math.PI)
  208. ctx.fill()
  209. // Draw a simple person silhouette with Canvas primitives
  210. ctx.setFillStyle('#F97316')
  211. // Head (smaller circle, upper portion)
  212. var headR = r * 0.32
  213. var headY = y - r * 0.12
  214. ctx.beginPath()
  215. ctx.arc(x, headY, headR, 0, 2 * Math.PI)
  216. ctx.fill()
  217. // Body (trapezoid-like shape: shoulders + torso)
  218. ctx.beginPath()
  219. var shoulderW = r * 0.55
  220. var bodyTop = headY + headR * 0.5
  221. var bodyBottom = y + r * 0.55
  222. var hipW = r * 0.35
  223. ctx.moveTo(x - shoulderW, bodyTop)
  224. ctx.quadraticCurveTo(x - shoulderW, bodyTop + 10, x - hipW, bodyBottom)
  225. ctx.lineTo(x + hipW, bodyBottom)
  226. ctx.quadraticCurveTo(x + shoulderW, bodyTop + 10, x + shoulderW, bodyTop)
  227. ctx.closePath()
  228. ctx.fill()
  229. },
  230. // ============================================================
  231. // CONTENT AFTER AVATAR — nickname, referral code, QR code, CTA
  232. // ============================================================
  233. _drawContentAfterAvatar(ctx, w, h, _cardX, _cardW, _avatarX, avatarY) {
  234. var self = this
  235. var base64Data, commaIdx, fs, filePath
  236. // --- Nickname ---
  237. var nameY = avatarY + 70
  238. ctx.setFillStyle('#1C1917')
  239. ctx.setFontSize(22)
  240. ctx.setTextAlign('center')
  241. ctx.setTextBaseline('top')
  242. ctx.fillText(this.nickname || '好友', w / 2, nameY)
  243. // --- "我的邀请码" label ---
  244. var labelY = nameY + 42
  245. ctx.setFontSize(15)
  246. ctx.setFillStyle('#A8A29E')
  247. ctx.fillText('我的邀请码', w / 2, labelY)
  248. // --- Referral code (prominent, warm orange) ---
  249. var codeY = labelY + 38
  250. var codeText = this.referralCode || '------'
  251. ctx.setFontSize(44)
  252. ctx.setFillStyle('#F97316')
  253. ctx.setTextAlign('center')
  254. ctx.setTextBaseline('top')
  255. // Gentle underline accent
  256. var codeWidth = 0
  257. if (ctx.measureText) {
  258. try { codeWidth = ctx.measureText(codeText).width } catch (e) { codeWidth = codeText.length * 26 }
  259. } else {
  260. codeWidth = codeText.length * 26
  261. }
  262. var underlineY = codeY + 54
  263. ctx.setStrokeStyle('rgba(249,115,22,0.12)')
  264. ctx.setLineWidth(2)
  265. ctx.beginPath()
  266. ctx.moveTo(w / 2 - codeWidth / 2 - 8, underlineY)
  267. ctx.lineTo(w / 2 + codeWidth / 2 + 8, underlineY)
  268. ctx.stroke()
  269. ctx.fillText(codeText, w / 2, codeY)
  270. // --- 邀请文案(二维码上方)---
  271. var hintY = codeY + 80
  272. ctx.setFontSize(12)
  273. ctx.setFillStyle('#D6D3D1')
  274. ctx.setTextAlign('center')
  275. ctx.setTextBaseline('top')
  276. var inviteText = ''
  277. if (this.nickname && this.familyName) {
  278. inviteText = this.nickname + '邀请你来' + this.familyName
  279. } else if (this.nickname) {
  280. inviteText = this.nickname + '邀请你加入浠艾福'
  281. } else {
  282. inviteText = '长按识别二维码 · 一起加入浠艾福'
  283. }
  284. ctx.fillText(inviteText, w / 2, hintY)
  285. // --- QR code ---
  286. var qrSize = 180
  287. var qrX = (w - qrSize) / 2
  288. var qrY = hintY + 26
  289. if (this.qrCodeBase64) {
  290. base64Data = this.qrCodeBase64
  291. if (base64Data.indexOf('data:image') === 0) {
  292. commaIdx = base64Data.indexOf(',')
  293. if (commaIdx > -1) {
  294. base64Data = base64Data.substring(commaIdx + 1)
  295. }
  296. }
  297. fs = uni.getFileSystemManager()
  298. filePath = wx.env.USER_DATA_PATH + '/share_qr_tmp.png'
  299. fs.writeFile({
  300. filePath: filePath,
  301. data: base64Data,
  302. encoding: 'base64',
  303. success: function() {
  304. // White background behind QR
  305. ctx.setFillStyle('#ffffff')
  306. self._roundRect(ctx, qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 8)
  307. ctx.fill()
  308. // Subtle border
  309. ctx.setStrokeStyle('#E7E5E4')
  310. ctx.setLineWidth(0.5)
  311. self._roundRect(ctx, qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 8)
  312. ctx.stroke()
  313. ctx.drawImage(filePath, qrX, qrY, qrSize, qrSize)
  314. self._drawBottomDecoration(ctx, w, h, qrY, qrSize)
  315. },
  316. fail: function() {
  317. self._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
  318. self._drawBottomDecoration(ctx, w, h, qrY, qrSize)
  319. }
  320. })
  321. } else {
  322. this._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
  323. this._drawBottomDecoration(ctx, w, h, qrY, qrSize)
  324. }
  325. },
  326. // ============================================================
  327. // QR PLACEHOLDER (fallback when QR write fails)
  328. // ============================================================
  329. _drawQrPlaceholder(ctx, x, y, size) {
  330. ctx.setFillStyle('#ffffff')
  331. this._roundRect(ctx, x - 10, y - 10, size + 20, size + 20, 8)
  332. ctx.fill()
  333. ctx.setStrokeStyle('#E7E5E4')
  334. ctx.setLineWidth(0.5)
  335. this._roundRect(ctx, x - 10, y - 10, size + 20, size + 20, 8)
  336. ctx.stroke()
  337. ctx.setFillStyle('#A8A29E')
  338. ctx.setFontSize(14)
  339. ctx.setTextAlign('center')
  340. ctx.setTextBaseline('middle')
  341. ctx.fillText('二维码加载中', x + size / 2, y + size / 2)
  342. },
  343. // ============================================================
  344. // BOTTOM DECORATION — CTA, dots, brand footer, ctx.draw()
  345. // ============================================================
  346. _drawBottomDecoration(ctx, w, h, qrY, qrSize) {
  347. var centerX = w / 2
  348. var bottomY = qrY + qrSize + 16
  349. // Decorative small diamonds on sides
  350. var diamondY = bottomY + 10
  351. ctx.setFillStyle('rgba(249,115,22,0.15)')
  352. // Left diamond
  353. ctx.beginPath()
  354. ctx.moveTo(centerX - 76, diamondY)
  355. ctx.lineTo(centerX - 72, diamondY - 4)
  356. ctx.lineTo(centerX - 68, diamondY)
  357. ctx.lineTo(centerX - 72, diamondY + 4)
  358. ctx.closePath()
  359. ctx.fill()
  360. // Right diamond
  361. ctx.beginPath()
  362. ctx.moveTo(centerX + 68, diamondY)
  363. ctx.lineTo(centerX + 72, diamondY - 4)
  364. ctx.lineTo(centerX + 76, diamondY)
  365. ctx.lineTo(centerX + 72, diamondY + 4)
  366. ctx.closePath()
  367. ctx.fill()
  368. // CTA text
  369. ctx.setFillStyle('#F97316')
  370. ctx.setFontSize(17)
  371. ctx.setTextAlign('center')
  372. ctx.setTextBaseline('top')
  373. ctx.fillText('扫码邀请好友加入', centerX, bottomY)
  374. // 5 dimension dots in the bottom area (outside card)
  375. var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
  376. var dotY = h - 22
  377. var dotR = 4
  378. var dotSpacing = 20
  379. var i_b, offsetX_b
  380. for (i_b = 0; i_b < 5; i_b++) {
  381. offsetX_b = (i_b - 2) * dotSpacing
  382. ctx.setFillStyle(dotColors[i_b])
  383. ctx.beginPath()
  384. ctx.arc(centerX + offsetX_b, dotY, dotR, 0, 2 * Math.PI)
  385. ctx.fill()
  386. }
  387. // Footer brand text
  388. ctx.setFillStyle('rgba(255,255,255,0.45)')
  389. ctx.setFontSize(10)
  390. ctx.setTextAlign('center')
  391. ctx.setTextBaseline('top')
  392. ctx.fillText('浠艾福 · 家庭健康俱乐部', centerX, dotY + 7)
  393. // FINAL RENDER — flush all queued canvas commands
  394. ctx.draw()
  395. },
  396. // ============================================================
  397. // SAVE POSTER TO PHOTO ALBUM (preserved as-is)
  398. // ============================================================
  399. savePoster() {
  400. var self = this
  401. uni.canvasToTempFilePath({
  402. canvasId: 'sharePosterCanvas',
  403. success: function(res) {
  404. uni.saveImageToPhotosAlbum({
  405. filePath: res.tempFilePath,
  406. success: function() {
  407. uni.showToast({ title: '已保存到相册', icon: 'success' })
  408. self.$emit('close')
  409. },
  410. fail: function(err) {
  411. if (err && err.errMsg && (err.errMsg.indexOf('deny') > -1 || err.errMsg.indexOf('auth denied') > -1 || err.errMsg.indexOf('authorize') > -1)) {
  412. uni.showModal({
  413. title: '提示',
  414. content: '需要相册权限才能保存图片',
  415. confirmText: '去设置',
  416. success: function(modalRes) {
  417. if (modalRes.confirm) {
  418. uni.openSetting()
  419. }
  420. }
  421. })
  422. } else {
  423. uni.showToast({ title: '保存失败,请重试', icon: 'none' })
  424. }
  425. }
  426. })
  427. },
  428. fail: function() {
  429. uni.showToast({ title: '生成图片失败', icon: 'none' })
  430. }
  431. }, this)
  432. }
  433. }
  434. }
  435. </script>
  436. <style scoped>
  437. .poster-overlay {
  438. position: fixed;
  439. top: 0;
  440. left: 0;
  441. right: 0;
  442. bottom: 0;
  443. background: rgba(0, 0, 0, 0.6);
  444. display: flex;
  445. align-items: center;
  446. justify-content: center;
  447. z-index: 999;
  448. }
  449. .poster-container {
  450. display: flex;
  451. flex-direction: column;
  452. align-items: center;
  453. }
  454. .poster-canvas {
  455. width: 500px;
  456. height: 750px;
  457. border-radius: 16rpx;
  458. }
  459. .poster-actions {
  460. display: flex;
  461. flex-direction: column;
  462. width: 500px;
  463. margin-top: 30rpx;
  464. }
  465. .poster-btn {
  466. height: 80rpx;
  467. line-height: 80rpx;
  468. font-size: 30rpx;
  469. border-radius: 40rpx;
  470. text-align: center;
  471. border: none;
  472. }
  473. .poster-btn::after { border: none; }
  474. .poster-btn.save {
  475. background: #ffffff;
  476. color: #F97316;
  477. font-weight: 600;
  478. margin-bottom: 16rpx;
  479. }
  480. .poster-btn.save:active { opacity: 0.85; }
  481. .poster-btn.cancel {
  482. background: rgba(255, 255, 255, 0.15);
  483. color: #ffffff;
  484. }
  485. .poster-btn.cancel:active { opacity: 0.85; }
  486. </style>