SharePoster.vue 16 KB

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