invite.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <template>
  2. <view class="container" :style="{ '--font-scale': fontScale }">
  3. <!-- 邀请信息 -->
  4. <view class="invite-card" v-if="guideInfo">
  5. <view class="guide-avatar">{{ guideInfo.guideName ? guideInfo.guideName[0] : '师' }}</view>
  6. <text class="guide-name">{{ guideInfo.guideName }}</text>
  7. <text class="invite-desc">邀请您绑定家庭关系</text>
  8. <text class="expire-text">邀请有效期至 {{ formatDate(guideInfo.expireTime) }}</text>
  9. </view>
  10. <!-- 绑定表单 -->
  11. <view class="bind-form" v-if="!bound">
  12. <view class="form-item">
  13. <text class="label">选择家庭</text>
  14. <picker :range="families" range-key="name" @change="onFamilyChange">
  15. <view class="picker">
  16. {{ selectedFamily ? selectedFamily.name : '请选择家庭' }}
  17. </view>
  18. </picker>
  19. </view>
  20. <view class="form-item">
  21. <text class="label">服务类型</text>
  22. <picker :range="serviceTypes" range-key="label" @change="onServiceTypeChange">
  23. <view class="picker">
  24. {{ serviceType.label }}
  25. </view>
  26. </picker>
  27. </view>
  28. <view class="form-item">
  29. <text class="label">服务价格</text>
  30. <input class="input" type="digit" v-model="servicePrice" placeholder="请输入服务价格" />
  31. </view>
  32. <button class="btn-confirm" @click="confirmBind">确认绑定</button>
  33. </view>
  34. <!-- 已绑定提示 -->
  35. <view class="bound-card" v-else>
  36. <text class="success-icon">✓</text>
  37. <text class="success-text">绑定成功</text>
  38. <text class="success-desc">您已成功绑定成长规划师</text>
  39. <button class="btn-go" @click="goHome">返回首页</button>
  40. </view>
  41. <!-- 分享邀请 -->
  42. <view class="share-section" v-if="isGuide">
  43. <text class="section-title">邀请家人绑定</text>
  44. <view class="invite-url">
  45. <text selectable>{{ inviteUrl }}</text>
  46. </view>
  47. <button class="btn-copy" @click="copyUrl">复制邀请链接</button>
  48. <button class="btn-qrcode" @click="showQrcode">生成二维码</button>
  49. </view>
  50. <!-- 二维码弹窗 -->
  51. <uni-popup ref="qrPopup" type="center">
  52. <view class="qr-popup">
  53. <text class="qr-title">扫码绑定</text>
  54. <image class="qr-image" :src="qrcodeUrl" mode="widthFix" />
  55. <text class="qr-desc">微信扫一扫绑定</text>
  56. <text class="close-btn" @click="closeQrcode">关闭</text>
  57. </view>
  58. </uni-popup>
  59. </view>
  60. </template>
  61. <script>
  62. import { generateBindInvite, generateQrCodeFromText } from '@/utils/api.js'
  63. import config from '@/config.js'
  64. import { parseDate } from '@/utils/format.js'
  65. import fontScaleMixin from '@/mixins/font-scale.js'
  66. export default {
  67. mixins: [fontScaleMixin],
  68. data() {
  69. return {
  70. inviteToken: '',
  71. guideId: null,
  72. guideInfo: null,
  73. families: [],
  74. selectedFamily: null,
  75. serviceTypes: [
  76. { label: '咨询', value: 'consulting' },
  77. { label: '月度服务', value: 'monthly' },
  78. { label: '年度服务', value: 'yearly' }
  79. ],
  80. serviceType: { label: '咨询', value: 'consulting' },
  81. servicePrice: 0,
  82. bound: false,
  83. isGuide: false,
  84. inviteUrl: '',
  85. qrcodeUrl: ''
  86. }
  87. },
  88. onLoad(options) {
  89. if (options.token) {
  90. this.inviteToken = options.token
  91. this.guideId = options.guideId
  92. this.loadGuideInfo()
  93. }
  94. // 判断是否为成长规划师
  95. const roles = uni.getStorageSync('roles') || ''
  96. this.isGuide = roles.includes('teacher')
  97. if (this.isGuide) {
  98. this.generateInvite()
  99. }
  100. this.loadFamilies()
  101. },
  102. methods: {
  103. loadGuideInfo() {
  104. // 从服务器获取成长规划师信息
  105. uni.request({
  106. url: `${config.API_BASE_URL}/api/guide/bind/validate`,
  107. method: 'POST',
  108. data: { token: this.inviteToken },
  109. header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
  110. success: (res) => {
  111. if (res.data.code === 200) {
  112. this.guideInfo = res.data.data
  113. }
  114. }
  115. })
  116. },
  117. loadFamilies() {
  118. // 获取家庭列表
  119. uni.request({
  120. url: `${config.API_BASE_URL}/api/family/user/family-members`,
  121. method: 'POST',
  122. data: {},
  123. header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
  124. success: (res) => {
  125. if (res.data.code === 200) {
  126. this.families = res.data.data || []
  127. }
  128. }
  129. })
  130. },
  131. onFamilyChange(e) {
  132. this.selectedFamily = this.families[e.detail.value]
  133. },
  134. onServiceTypeChange(e) {
  135. this.serviceType = this.serviceTypes[e.detail.value]
  136. },
  137. async confirmBind() {
  138. if (!this.selectedFamily) {
  139. uni.showToast({ title: '请选择家庭', icon: 'none' })
  140. return
  141. }
  142. uni.showLoading({ title: '绑定中...' })
  143. try {
  144. await uni.request({
  145. url: `${config.API_BASE_URL}/api/bind/accept`,
  146. method: 'POST',
  147. header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
  148. data: {
  149. guideId: this.guideId,
  150. bindToken: this.inviteToken,
  151. serviceType: this.serviceType.value,
  152. servicePrice: this.servicePrice
  153. }
  154. })
  155. uni.showToast({ title: '绑定成功' })
  156. this.bound = true
  157. } catch (e) {
  158. uni.showToast({ title: '绑定失败', icon: 'none' })
  159. } finally {
  160. uni.hideLoading()
  161. }
  162. },
  163. async generateInvite() {
  164. try {
  165. const res = await generateBindInvite()
  166. this.inviteUrl = res.data.inviteUrl
  167. this.guideInfo = res.data
  168. } catch (e) {
  169. console.error(e)
  170. }
  171. },
  172. copyUrl() {
  173. uni.setClipboardData({ data: this.inviteUrl })
  174. uni.showToast({ title: '已复制' })
  175. },
  176. async showQrcode() {
  177. if (!this.inviteUrl) {
  178. uni.showToast({ title: '请先生成邀请', icon: 'none' })
  179. return
  180. }
  181. try {
  182. const res = await generateQrCodeFromText(this.inviteUrl)
  183. if (res.code === 200 && res.data) {
  184. this.qrcodeUrl = res.data.qrCodeBase64
  185. } else {
  186. uni.showToast({ title: res.message || '生成二维码失败', icon: 'none' })
  187. return
  188. }
  189. } catch (e) {
  190. uni.showToast({ title: '生成二维码失败', icon: 'none' })
  191. return
  192. }
  193. this.$refs.qrPopup.open()
  194. },
  195. closeQrcode() {
  196. this.$refs.qrPopup.close()
  197. },
  198. formatDate(date) {
  199. if (!date) return ''
  200. const d = parseDate(date)
  201. if (!d) return ''
  202. return `${d.getMonth() + 1}-${d.getDate()}`
  203. },
  204. goHome() {
  205. uni.navigateTo({ url: '/pages/home-pages/parent-index' })
  206. }
  207. }
  208. }
  209. </script>
  210. <style scoped lang="scss">
  211. .container { min-height: 100vh; background: $facade-fill-2; padding: $uni-space-page; }
  212. .invite-card, .bound-card {
  213. background: $app-surface;
  214. border-radius: 16rpx;
  215. padding: 60rpx $uni-space-page;
  216. text-align: center;
  217. margin-bottom: $uni-space-lg;
  218. }
  219. .guide-avatar {
  220. width: 120rpx;
  221. height: 120rpx;
  222. background: $mat-green;
  223. border-radius: 50%;
  224. margin: 0 auto 20rpx;
  225. display: flex;
  226. align-items: center;
  227. justify-content: center;
  228. font-size: calc(48rpx * var(--font-scale, 1));
  229. color: $app-surface;
  230. }
  231. .guide-name { font-size: calc(36rpx * var(--font-scale, 1)); font-weight: bold; display: block; margin-bottom: 10rpx; }
  232. .invite-desc { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-body; display: block; margin-bottom: 20rpx; }
  233. .expire-text { font-size: calc(24rpx * var(--font-scale, 1)); color: $facade-text-muted; }
  234. .bind-form { background: $app-surface; border-radius: 16rpx; padding: $uni-space-page; }
  235. .form-item { margin-bottom: $uni-space-lg; }
  236. .label { display: block; font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-strong; margin-bottom: 16rpx; }
  237. .input, .picker { padding: 20rpx; background: $facade-fill-2; border-radius: 16rpx; font-size: calc(28rpx * var(--font-scale, 1)); }
  238. .btn-confirm { background: $mat-green; color: $app-surface; border-radius: 24rpx; margin-top: 40rpx; }
  239. .success-icon { font-size: calc(80rpx * var(--font-scale, 1)); color: $mat-green; display: block; margin-bottom: 20rpx; }
  240. .success-text { font-size: calc(36rpx * var(--font-scale, 1)); font-weight: bold; display: block; margin-bottom: 10rpx; }
  241. .success-desc { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-body; display: block; margin-bottom: 40rpx; }
  242. .btn-go { background: $mat-green; color: $app-surface; border-radius: 24rpx; }
  243. .share-section { background: $app-surface; border-radius: 16rpx; padding: $uni-space-page; margin-top: $uni-space-lg; }
  244. .section-title { font-size: calc(30rpx * var(--font-scale, 1)); font-weight: bold; display: block; margin-bottom: 20rpx; }
  245. .invite-url { background: $facade-fill-2; padding: 20rpx; border-radius: 8rpx; margin-bottom: 20rpx; font-size: calc(24rpx * var(--font-scale, 1)); word-break: break-all; }
  246. .btn-copy, .btn-qrcode { background: $mat-green; color: $app-surface; margin-bottom: 20rpx; }
  247. .qr-popup { background: $app-surface; border-radius: 16rpx; padding: 40rpx; text-align: center; width: 500rpx; }
  248. .qr-title { font-size: calc(32rpx * var(--font-scale, 1)); font-weight: bold; display: block; margin-bottom: $uni-space-lg; }
  249. .qr-image { width: 300rpx; height: 300rpx; margin-bottom: 20rpx; }
  250. .qr-desc { font-size: calc(26rpx * var(--font-scale, 1)); color: $facade-text-body; display: block; margin-bottom: $uni-space-lg; }
  251. .close-btn { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-muted; }
  252. </style>