You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

477 lines
10 KiB

  1. <template>
  2. <view class="invite-page">
  3. <view class="hero">
  4. <view class="hero-copy">
  5. <text class="eyebrow">INVITE FRIENDS</text>
  6. <text class="hero-title">{{ config.share_title || '邀请好友一起用 AI ONLINE' }}</text>
  7. <text class="hero-desc">{{ config.share_desc || '好友注册成功后,你和好友都能获得点数奖励。' }}</text>
  8. </view>
  9. <view class="reward-box">
  10. <view class="reward-item">
  11. <text class="reward-num">{{ config.inviter_points || 0 }}</text>
  12. <text class="reward-label">你获得</text>
  13. </view>
  14. <view class="reward-divider"></view>
  15. <view class="reward-item">
  16. <text class="reward-num">{{ config.invitee_points || 0 }}</text>
  17. <text class="reward-label">好友获得</text>
  18. </view>
  19. </view>
  20. </view>
  21. <view class="stats-row">
  22. <view class="stat-card">
  23. <text class="stat-num">{{ stats.success || 0 }}</text>
  24. <text class="stat-label">成功邀请</text>
  25. </view>
  26. <view class="stat-card">
  27. <text class="stat-num">{{ stats.inviter_points || 0 }}</text>
  28. <text class="stat-label">累计奖励</text>
  29. </view>
  30. </view>
  31. <view class="action-panel">
  32. <button class="primary-action" open-type="share">
  33. <up-icon class="action-icon" name="share" size="28rpx" color="#ffffff"></up-icon>
  34. <text>分享小程序</text>
  35. </button>
  36. <view class="secondary-actions">
  37. <view class="secondary-action" @click="openQr">
  38. <up-icon class="action-icon" name="scan" size="28rpx" color="#0b8cff"></up-icon>
  39. <text>二维码分享</text>
  40. </view>
  41. <view class="secondary-action" @click="goList">
  42. <up-icon class="action-icon" name="order" size="28rpx" color="#0b8cff"></up-icon>
  43. <text>邀请记录</text>
  44. </view>
  45. </view>
  46. </view>
  47. <view class="rule-card">
  48. <view class="rule-title">活动规则</view>
  49. <text class="rule-text">{{ config.rule_text || '好友通过你的邀请登录注册成功后,你和好友都可获得点数奖励。' }}</text>
  50. <view class="rule-steps">
  51. <view class="step">
  52. <text class="step-no">01</text>
  53. <text>分享给好友</text>
  54. </view>
  55. <view class="step">
  56. <text class="step-no">02</text>
  57. <text>好友登录注册</text>
  58. </view>
  59. <view class="step">
  60. <text class="step-no">03</text>
  61. <text>点数自动到账</text>
  62. </view>
  63. </view>
  64. </view>
  65. <view v-if="qrVisible" class="qr-mask" @click="closeQr">
  66. <view class="qr-sheet" @click.stop>
  67. <view class="sheet-head">
  68. <text class="sheet-title">二维码分享</text>
  69. <text class="sheet-close" @click="closeQr">×</text>
  70. </view>
  71. <view class="qr-card">
  72. <image v-if="qrcodeUrl" class="qr-img" :src="qrcodeUrl" mode="aspectFit" />
  73. <view v-else class="qr-loading">{{ qrLoading ? '生成中...' : '暂无二维码' }}</view>
  74. <text class="qr-tip">好友微信扫码即可进入邀请页</text>
  75. </view>
  76. <view class="sheet-actions">
  77. <view class="ghost-btn" @click="copyPath">复制路径</view>
  78. <view class="save-btn" @click="saveQr">保存图片</view>
  79. </view>
  80. </view>
  81. </view>
  82. </view>
  83. </template>
  84. <script setup>
  85. import { computed, ref } from 'vue'
  86. import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
  87. import { inviteApi } from '@/api/index.js'
  88. import { useUserStore } from '@/store/user.js'
  89. import { useApp } from '@/utils/useApp.js'
  90. import { buildShareOptions } from '@/utils/share.js'
  91. const userStore = useUserStore()
  92. const { toast } = useApp()
  93. const config = ref({})
  94. const stats = ref({})
  95. const share = ref({})
  96. const qrcodeUrl = ref('')
  97. const qrVisible = ref(false)
  98. const qrLoading = ref(false)
  99. const sharePath = computed(() => (share.value && share.value.path) || `/pages/invite/landing?invite_code=${userStore.userInfo.open_id || ''}`)
  100. onShow(() => {
  101. userStore.restore()
  102. if (!userStore.isLogin) {
  103. uni.navigateTo({ url: '/pages/login/login' })
  104. return
  105. }
  106. loadData()
  107. })
  108. onShareAppMessage(() => buildShareOptions({
  109. title: (share.value && share.value.title) || config.value.share_title || '邀请你体验 AI ONLINE',
  110. path: sharePath.value,
  111. imageUrl: (share.value && share.value.image) || config.value.share_image || '',
  112. source: 'share_invite'
  113. }))
  114. async function loadData() {
  115. try {
  116. const res = await inviteApi.getMy()
  117. config.value = res.data.config || {}
  118. stats.value = res.data.stats || {}
  119. share.value = res.data.share || {}
  120. } catch (e) {
  121. toast(e.msg || '邀请信息加载失败')
  122. }
  123. }
  124. async function openQr() {
  125. qrVisible.value = true
  126. if (qrcodeUrl.value || qrLoading.value) return
  127. qrLoading.value = true
  128. try {
  129. const res = await inviteApi.getQrcode()
  130. qrcodeUrl.value = res.data.qrcode_url || ''
  131. } catch (e) {
  132. toast(e.msg || '二维码生成失败')
  133. } finally {
  134. qrLoading.value = false
  135. }
  136. }
  137. function closeQr() {
  138. qrVisible.value = false
  139. }
  140. function copyPath() {
  141. uni.setClipboardData({ data: sharePath.value })
  142. }
  143. function saveQr() {
  144. if (!qrcodeUrl.value) return toast('二维码还没有生成')
  145. uni.downloadFile({
  146. url: qrcodeUrl.value,
  147. success: (res) => {
  148. if (res.statusCode !== 200) return toast('图片下载失败')
  149. uni.saveImageToPhotosAlbum({
  150. filePath: res.tempFilePath,
  151. success: () => toast('已保存到相册'),
  152. fail: () => toast('保存失败,请检查相册权限')
  153. })
  154. },
  155. fail: () => toast('图片下载失败')
  156. })
  157. }
  158. function goList() {
  159. uni.navigateTo({ url: '/pages/invite/list' })
  160. }
  161. </script>
  162. <style lang="scss" scoped>
  163. .invite-page {
  164. min-height: 100vh;
  165. padding: 24rpx 24rpx calc(46rpx + env(safe-area-inset-bottom));
  166. background: #f4f7fb;
  167. box-sizing: border-box;
  168. }
  169. .hero {
  170. border-radius: 30rpx;
  171. padding: 34rpx 30rpx 30rpx;
  172. color: #ffffff;
  173. background: linear-gradient(135deg, #0b8cff 0%, #18c2ff 100%);
  174. overflow: hidden;
  175. }
  176. .hero-copy {
  177. display: flex;
  178. flex-direction: column;
  179. }
  180. .eyebrow {
  181. opacity: 0.72;
  182. font-size: 22rpx;
  183. font-weight: 800;
  184. line-height: 1;
  185. }
  186. .hero-title {
  187. margin-top: 18rpx;
  188. font-size: 42rpx;
  189. font-weight: 900;
  190. line-height: 1.22;
  191. }
  192. .hero-desc {
  193. width: 92%;
  194. margin-top: 16rpx;
  195. opacity: 0.86;
  196. font-size: 26rpx;
  197. line-height: 1.45;
  198. }
  199. .reward-box {
  200. margin-top: 30rpx;
  201. border-radius: 24rpx;
  202. background: rgba(255, 255, 255, 0.16);
  203. display: flex;
  204. align-items: center;
  205. padding: 24rpx 0;
  206. }
  207. .reward-item {
  208. flex: 1;
  209. display: flex;
  210. flex-direction: column;
  211. align-items: center;
  212. }
  213. .reward-num {
  214. font-size: 50rpx;
  215. font-weight: 900;
  216. line-height: 1;
  217. }
  218. .reward-label {
  219. margin-top: 10rpx;
  220. font-size: 23rpx;
  221. opacity: 0.82;
  222. }
  223. .reward-divider {
  224. width: 1rpx;
  225. height: 58rpx;
  226. background: rgba(255, 255, 255, 0.35);
  227. }
  228. .stats-row {
  229. display: grid;
  230. grid-template-columns: repeat(2, minmax(0, 1fr));
  231. gap: 18rpx;
  232. margin-top: 20rpx;
  233. }
  234. .stat-card,
  235. .action-panel,
  236. .rule-card {
  237. border-radius: 24rpx;
  238. background: #ffffff;
  239. box-shadow: 0 12rpx 30rpx rgba(30, 66, 120, 0.06);
  240. }
  241. .stat-card {
  242. padding: 24rpx;
  243. }
  244. .stat-num {
  245. color: #172033;
  246. font-size: 38rpx;
  247. font-weight: 900;
  248. line-height: 1;
  249. }
  250. .stat-label {
  251. display: block;
  252. margin-top: 12rpx;
  253. color: #7f8896;
  254. font-size: 24rpx;
  255. }
  256. .action-panel {
  257. margin-top: 20rpx;
  258. padding: 24rpx;
  259. }
  260. button {
  261. margin: 0;
  262. padding: 0;
  263. border: 0;
  264. background: transparent;
  265. line-height: normal;
  266. }
  267. button::after {
  268. border: 0;
  269. }
  270. .primary-action {
  271. height: 88rpx;
  272. border-radius: 999rpx;
  273. color: #ffffff;
  274. background: #0b8cff;
  275. display: flex;
  276. align-items: center;
  277. justify-content: center;
  278. gap: 12rpx;
  279. font-size: 29rpx;
  280. font-weight: 900;
  281. }
  282. .secondary-actions {
  283. display: grid;
  284. grid-template-columns: repeat(2, minmax(0, 1fr));
  285. gap: 16rpx;
  286. margin-top: 16rpx;
  287. }
  288. .secondary-action {
  289. height: 78rpx;
  290. border-radius: 999rpx;
  291. color: #0b8cff;
  292. background: #edf7ff;
  293. display: flex;
  294. align-items: center;
  295. justify-content: center;
  296. gap: 10rpx;
  297. font-size: 26rpx;
  298. font-weight: 800;
  299. }
  300. .action-icon {
  301. flex-shrink: 0;
  302. }
  303. .rule-card {
  304. margin-top: 20rpx;
  305. padding: 28rpx;
  306. }
  307. .rule-title {
  308. color: #172033;
  309. font-size: 30rpx;
  310. font-weight: 900;
  311. }
  312. .rule-text {
  313. display: block;
  314. margin-top: 14rpx;
  315. color: #687386;
  316. font-size: 25rpx;
  317. line-height: 1.5;
  318. }
  319. .rule-steps {
  320. margin-top: 22rpx;
  321. display: flex;
  322. flex-direction: column;
  323. gap: 14rpx;
  324. }
  325. .step {
  326. height: 58rpx;
  327. border-radius: 16rpx;
  328. background: #f5f9ff;
  329. display: flex;
  330. align-items: center;
  331. padding: 0 18rpx;
  332. color: #39445a;
  333. font-size: 25rpx;
  334. font-weight: 700;
  335. }
  336. .step-no {
  337. margin-right: 16rpx;
  338. color: #0b8cff;
  339. font-size: 21rpx;
  340. font-weight: 900;
  341. }
  342. .qr-mask {
  343. position: fixed;
  344. inset: 0;
  345. z-index: 99;
  346. background: rgba(9, 16, 28, 0.45);
  347. display: flex;
  348. align-items: flex-end;
  349. }
  350. .qr-sheet {
  351. width: 100%;
  352. padding: 28rpx 28rpx calc(30rpx + env(safe-area-inset-bottom));
  353. border-radius: 34rpx 34rpx 0 0;
  354. background: #ffffff;
  355. box-sizing: border-box;
  356. }
  357. .sheet-head,
  358. .sheet-actions {
  359. display: flex;
  360. align-items: center;
  361. justify-content: space-between;
  362. }
  363. .sheet-title {
  364. color: #172033;
  365. font-size: 32rpx;
  366. font-weight: 900;
  367. }
  368. .sheet-close {
  369. width: 58rpx;
  370. height: 58rpx;
  371. border-radius: 50%;
  372. color: #7f8896;
  373. background: #f4f7fb;
  374. font-size: 38rpx;
  375. line-height: 54rpx;
  376. text-align: center;
  377. }
  378. .qr-card {
  379. margin: 26rpx auto 24rpx;
  380. width: 430rpx;
  381. min-height: 500rpx;
  382. border-radius: 28rpx;
  383. background: #f6f9fd;
  384. display: flex;
  385. flex-direction: column;
  386. align-items: center;
  387. justify-content: center;
  388. }
  389. .qr-img {
  390. width: 360rpx;
  391. height: 360rpx;
  392. }
  393. .qr-loading,
  394. .qr-tip {
  395. color: #7f8896;
  396. font-size: 24rpx;
  397. }
  398. .qr-tip {
  399. margin-top: 18rpx;
  400. }
  401. .ghost-btn,
  402. .save-btn {
  403. flex: 1;
  404. height: 82rpx;
  405. border-radius: 999rpx;
  406. font-size: 27rpx;
  407. font-weight: 900;
  408. line-height: 82rpx;
  409. text-align: center;
  410. }
  411. .ghost-btn {
  412. margin-right: 18rpx;
  413. color: #0b8cff;
  414. background: #edf7ff;
  415. }
  416. .save-btn {
  417. color: #ffffff;
  418. background: #0b8cff;
  419. }
  420. </style>