index.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template>
  2. <view class="container">
  3. <view class="header">
  4. <text class="title">邀请家人加入</text>
  5. <text class="desc">分享给好友,好友注册后自动成为家庭成员</text>
  6. </view>
  7. <!-- 邀请码展示 -->
  8. <view class="invite-card" v-if="inviteCode">
  9. <view class="invite-code">{{ inviteCode }}</view>
  10. <text class="invite-tip">好友也可输入邀请码加入</text>
  11. </view>
  12. <!-- 分享按钮区域 -->
  13. <view class="share-section">
  14. <button class="btn-share-large" open-type="share">
  15. <text class="share-icon">📤</text>
  16. <text class="share-text">发送给好友</text>
  17. </button>
  18. <text class="share-hint">点击上方按钮,分享到微信好友</text>
  19. </view>
  20. <!-- 其他方式 -->
  21. <view class="other-methods">
  22. <view class="method-title">其他方式</view>
  23. <view class="method-item" @click="copyCode">
  24. <text class="method-icon">📋</text>
  25. <text class="method-text">复制邀请码</text>
  26. </view>
  27. <view class="method-item" @click="generatePoster">
  28. <text class="method-icon">🖼️</text>
  29. <text class="method-text">生成邀请海报</text>
  30. </view>
  31. </view>
  32. <!-- 成长规划师邀请说明 -->
  33. <view class="guide-tip" v-if="role === 'teacher'">
  34. <text class="tip-title">成长规划师权限说明</text>
  35. <text class="tip-text">通过邀请码绑定家庭后,您将获得:</text>
  36. <text class="tip-item">• 查看家庭成员任务完成情况</text>
  37. <text class="tip-item">• 审核任务执行</text>
  38. <text class="tip-item">• 制定任务</text>
  39. </view>
  40. <!-- 成长规划师绑定家庭 -->
  41. <view class="section" v-if="role === 'teacher'">
  42. <view class="section-title">绑定已有家庭</view>
  43. <input
  44. class="input"
  45. v-model="familyCode"
  46. placeholder="输入家庭邀请码"
  47. />
  48. <button class="btn-primary" @click="bindFamily">绑定家庭</button>
  49. </view>
  50. <!-- 海报弹窗 -->
  51. <view class="poster-mask" v-if="showPoster" @click="showPoster = false">
  52. <view class="poster-content" @click.stop>
  53. <canvas canvas-id="posterCanvas" class="poster-canvas"></canvas>
  54. <button class="btn-save" @click="savePoster">保存到相册</button>
  55. <text class="poster-hint">保存图片后发送给好友</text>
  56. </view>
  57. </view>
  58. </view>
  59. </template>
  60. <script>
  61. import { getInviteCode, generateFamilyInviteCode, bindGuideFamily, generateQrCodeFromText } from '@/utils/api.js'
  62. export default {
  63. data() {
  64. return {
  65. inviteCode: '',
  66. familyCode: '',
  67. role: 'parent',
  68. showPoster: false,
  69. familyName: ''
  70. }
  71. },
  72. onLoad() {
  73. this.role = uni.getStorageSync('role') || 'parent'
  74. this.loadInviteCode()
  75. this.loadFamilyName()
  76. },
  77. onShareAppMessage() {
  78. var code = this.inviteCode || ''
  79. var path = '/pages/invite/join'
  80. if (code) {
  81. path = path + '?inviteCode=' + code
  82. }
  83. return {
  84. title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
  85. path: path,
  86. imageUrl: ''
  87. }
  88. },
  89. methods: {
  90. async loadInviteCode() {
  91. try {
  92. const res = await getInviteCode()
  93. if (res.data) {
  94. this.inviteCode = res.data
  95. } else {
  96. const genRes = await generateFamilyInviteCode()
  97. this.inviteCode = genRes.data
  98. }
  99. } catch (e) {
  100. console.error('获取邀请码失败', e)
  101. }
  102. },
  103. async loadFamilyName() {
  104. try {
  105. const userInfo = uni.getStorageSync('userInfo')
  106. if (userInfo && userInfo.nickname) {
  107. this.familyName = userInfo.nickname + '的家庭'
  108. } else {
  109. this.familyName = '我的家庭'
  110. }
  111. } catch (e) {
  112. this.familyName = '我的家庭'
  113. }
  114. },
  115. copyCode() {
  116. uni.setClipboardData({
  117. data: this.inviteCode,
  118. success: () => {
  119. uni.showToast({ title: '已复制邀请码', icon: 'success' })
  120. }
  121. })
  122. },
  123. async generatePoster() {
  124. this.showPoster = true
  125. // 先获取二维码
  126. var qrTempPath = ''
  127. if (this.inviteCode) {
  128. var invitePath = '/pages/invite/join?inviteCode=' + this.inviteCode
  129. try {
  130. var res = await generateQrCodeFromText(invitePath)
  131. if (res.code === 200 && res.data) {
  132. var rawBase64 = res.data.qrCodeBase64
  133. if (rawBase64.indexOf('base64,') >= 0) {
  134. rawBase64 = rawBase64.split('base64,')[1]
  135. }
  136. var fsm = uni.getFileSystemManager()
  137. var path = wx.env.USER_DATA_PATH + '/qr_poster_' + Date.now() + '.png'
  138. fsm.writeFileSync(path, rawBase64, 'base64')
  139. qrTempPath = path
  140. }
  141. } catch (e) {
  142. console.error('生成二维码失败', e)
  143. }
  144. }
  145. this.$nextTick(function() {
  146. this.drawPoster(qrTempPath)
  147. }.bind(this))
  148. },
  149. drawPoster(qrTempPath) {
  150. const ctx = uni.createCanvasContext('posterCanvas')
  151. const width = 300
  152. const height = 450
  153. var self = this
  154. // 背景
  155. ctx.setFillStyle('#667eea')
  156. ctx.fillRect(0, 0, width, height)
  157. // 标题
  158. ctx.setFillStyle('#ffffff')
  159. ctx.setFontSize(20)
  160. ctx.setTextAlign('center')
  161. ctx.fillText('邀请加入家庭', width / 2, 40)
  162. // 家庭名
  163. ctx.setFontSize(14)
  164. ctx.fillText(self.familyName, width / 2, 70)
  165. // 邀请码
  166. ctx.setFillStyle('#ffffff')
  167. ctx.setFontSize(32)
  168. ctx.setTextAlign('center')
  169. ctx.fillText(self.inviteCode, width / 2, 130)
  170. // 提示
  171. ctx.setFontSize(12)
  172. ctx.setFillStyle('rgba(255,255,255,0.8)')
  173. ctx.setTextAlign('center')
  174. ctx.fillText('输入邀请码加入家庭', width / 2, 170)
  175. // 二维码区域
  176. if (qrTempPath) {
  177. ctx.drawImage(qrTempPath, 75, 210, 150, 150)
  178. } else {
  179. ctx.setFillStyle('#ffffff')
  180. ctx.fillRect(75, 210, 150, 150)
  181. ctx.setFillStyle('#667eea')
  182. ctx.setFontSize(12)
  183. ctx.setTextAlign('center')
  184. ctx.fillText('扫码注册', width / 2, 300)
  185. }
  186. ctx.draw()
  187. },
  188. savePoster() {
  189. uni.canvasToTempFilePath({
  190. canvasId: 'posterCanvas',
  191. success: (res) => {
  192. uni.saveImageToPhotosAlbum({
  193. filePath: res.tempFilePath,
  194. success: () => {
  195. uni.showToast({ title: '已保存到相册', icon: 'success' })
  196. this.showPoster = false
  197. },
  198. fail: () => {
  199. uni.showToast({ title: '保存失败', icon: 'none' })
  200. }
  201. })
  202. },
  203. fail: () => {
  204. uni.showToast({ title: '生成失败', icon: 'none' })
  205. }
  206. })
  207. },
  208. async bindFamily() {
  209. if (!this.familyCode) {
  210. uni.showToast({ title: '请输入家庭邀请码', icon: 'none' })
  211. return
  212. }
  213. try {
  214. await bindGuideFamily(this.familyCode)
  215. uni.showToast({ title: '绑定成功', icon: 'success' })
  216. } catch (e) {
  217. uni.showToast({ title: e.message || '绑定失败', icon: 'none' })
  218. }
  219. }
  220. }
  221. }
  222. </script>
  223. <style scoped>
  224. .container { padding: 30rpx; min-height: 100vh; background: #f5f5f5; }
  225. .header { text-align: center; padding: 40rpx 0; }
  226. .title { font-size: 40rpx; font-weight: bold; display: block; margin-bottom: 16rpx; color: #333; }
  227. .desc { font-size: 28rpx; color: #666; }
  228. .invite-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20rpx; padding: 50rpx; text-align: center; margin: 30rpx 0; }
  229. .invite-code { font-size: 56rpx; font-weight: bold; color: #fff; letter-spacing: 8rpx; margin-bottom: 16rpx; }
  230. .invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.8); }
  231. .share-section { background: #fff; border-radius: 20rpx; padding: 40rpx; margin: 30rpx 0; text-align: center; }
  232. .btn-share-large { background: #07c160; color: #fff; border-radius: 60rpx; padding: 24rpx 60rpx; display: inline-flex; align-items: center; justify-content: center; }
  233. .btn-share-large::after { border: none; }
  234. .share-icon { font-size: 36rpx; margin-right: 16rpx; }
  235. .share-text { font-size: 32rpx; font-weight: bold; }
  236. .share-hint { display: block; margin-top: 20rpx; font-size: 24rpx; color: #999; }
  237. .other-methods { background: #fff; border-radius: 20rpx; padding: 30rpx; margin: 30rpx 0; }
  238. .method-title { font-size: 28rpx; color: #999; margin-bottom: 20rpx; }
  239. .method-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; }
  240. .method-item:last-child { border-bottom: none; }
  241. .method-icon { font-size: 40rpx; margin-right: 20rpx; }
  242. .method-text { font-size: 28rpx; color: #333; }
  243. .guide-tip { background: #fff3e0; border-radius: 16rpx; padding: 30rpx; margin: 30rpx 0; }
  244. .tip-title { font-size: 28rpx; font-weight: bold; color: #ff9800; display: block; margin-bottom: 16rpx; }
  245. .tip-text { font-size: 24rpx; color: #666; display: block; margin-bottom: 12rpx; }
  246. .tip-item { font-size: 24rpx; color: #666; display: block; margin-left: 20rpx; line-height: 1.8; }
  247. .section { background: #fff; border-radius: 20rpx; padding: 30rpx; margin: 30rpx 0; }
  248. .section-title { font-size: 28rpx; font-weight: bold; margin-bottom: 20rpx; }
  249. .input { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; margin-bottom: 20rpx; font-size: 28rpx; }
  250. .poster-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 999; }
  251. .poster-content { background: #fff; border-radius: 20rpx; padding: 30rpx; text-align: center; }
  252. .poster-canvas { width: 300px; height: 450px; margin: 0 auto; }
  253. .btn-save { background: #07c160; color: #fff; border-radius: 40rpx; padding: 20rpx; margin: 20rpx 0; }
  254. .poster-hint { font-size: 24rpx; color: #999; display: block; }
  255. </style>