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.
 
 
 

475 regels
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. const userStore = useUserStore()
  91. const { toast } = useApp()
  92. const config = ref({})
  93. const stats = ref({})
  94. const share = ref({})
  95. const qrcodeUrl = ref('')
  96. const qrVisible = ref(false)
  97. const qrLoading = ref(false)
  98. const sharePath = computed(() => (share.value && share.value.path) || `/pages/invite/landing?invite_code=${userStore.userInfo.open_id || ''}`)
  99. onShow(() => {
  100. userStore.restore()
  101. if (!userStore.isLogin) {
  102. uni.navigateTo({ url: '/pages/login/login' })
  103. return
  104. }
  105. loadData()
  106. })
  107. onShareAppMessage(() => ({
  108. title: (share.value && share.value.title) || config.value.share_title || '邀请你体验 AI ONLINE',
  109. path: sharePath.value,
  110. imageUrl: (share.value && share.value.image) || config.value.share_image || ''
  111. }))
  112. async function loadData() {
  113. try {
  114. const res = await inviteApi.getMy()
  115. config.value = res.data.config || {}
  116. stats.value = res.data.stats || {}
  117. share.value = res.data.share || {}
  118. } catch (e) {
  119. toast(e.msg || '邀请信息加载失败')
  120. }
  121. }
  122. async function openQr() {
  123. qrVisible.value = true
  124. if (qrcodeUrl.value || qrLoading.value) return
  125. qrLoading.value = true
  126. try {
  127. const res = await inviteApi.getQrcode()
  128. qrcodeUrl.value = res.data.qrcode_url || ''
  129. } catch (e) {
  130. toast(e.msg || '二维码生成失败')
  131. } finally {
  132. qrLoading.value = false
  133. }
  134. }
  135. function closeQr() {
  136. qrVisible.value = false
  137. }
  138. function copyPath() {
  139. uni.setClipboardData({ data: sharePath.value })
  140. }
  141. function saveQr() {
  142. if (!qrcodeUrl.value) return toast('二维码还没有生成')
  143. uni.downloadFile({
  144. url: qrcodeUrl.value,
  145. success: (res) => {
  146. if (res.statusCode !== 200) return toast('图片下载失败')
  147. uni.saveImageToPhotosAlbum({
  148. filePath: res.tempFilePath,
  149. success: () => toast('已保存到相册'),
  150. fail: () => toast('保存失败,请检查相册权限')
  151. })
  152. },
  153. fail: () => toast('图片下载失败')
  154. })
  155. }
  156. function goList() {
  157. uni.navigateTo({ url: '/pages/invite/list' })
  158. }
  159. </script>
  160. <style lang="scss" scoped>
  161. .invite-page {
  162. min-height: 100vh;
  163. padding: 24rpx 24rpx calc(46rpx + env(safe-area-inset-bottom));
  164. background: #f4f7fb;
  165. box-sizing: border-box;
  166. }
  167. .hero {
  168. border-radius: 30rpx;
  169. padding: 34rpx 30rpx 30rpx;
  170. color: #ffffff;
  171. background: linear-gradient(135deg, #0b8cff 0%, #18c2ff 100%);
  172. overflow: hidden;
  173. }
  174. .hero-copy {
  175. display: flex;
  176. flex-direction: column;
  177. }
  178. .eyebrow {
  179. opacity: 0.72;
  180. font-size: 22rpx;
  181. font-weight: 800;
  182. line-height: 1;
  183. }
  184. .hero-title {
  185. margin-top: 18rpx;
  186. font-size: 42rpx;
  187. font-weight: 900;
  188. line-height: 1.22;
  189. }
  190. .hero-desc {
  191. width: 92%;
  192. margin-top: 16rpx;
  193. opacity: 0.86;
  194. font-size: 26rpx;
  195. line-height: 1.45;
  196. }
  197. .reward-box {
  198. margin-top: 30rpx;
  199. border-radius: 24rpx;
  200. background: rgba(255, 255, 255, 0.16);
  201. display: flex;
  202. align-items: center;
  203. padding: 24rpx 0;
  204. }
  205. .reward-item {
  206. flex: 1;
  207. display: flex;
  208. flex-direction: column;
  209. align-items: center;
  210. }
  211. .reward-num {
  212. font-size: 50rpx;
  213. font-weight: 900;
  214. line-height: 1;
  215. }
  216. .reward-label {
  217. margin-top: 10rpx;
  218. font-size: 23rpx;
  219. opacity: 0.82;
  220. }
  221. .reward-divider {
  222. width: 1rpx;
  223. height: 58rpx;
  224. background: rgba(255, 255, 255, 0.35);
  225. }
  226. .stats-row {
  227. display: grid;
  228. grid-template-columns: repeat(2, minmax(0, 1fr));
  229. gap: 18rpx;
  230. margin-top: 20rpx;
  231. }
  232. .stat-card,
  233. .action-panel,
  234. .rule-card {
  235. border-radius: 24rpx;
  236. background: #ffffff;
  237. box-shadow: 0 12rpx 30rpx rgba(30, 66, 120, 0.06);
  238. }
  239. .stat-card {
  240. padding: 24rpx;
  241. }
  242. .stat-num {
  243. color: #172033;
  244. font-size: 38rpx;
  245. font-weight: 900;
  246. line-height: 1;
  247. }
  248. .stat-label {
  249. display: block;
  250. margin-top: 12rpx;
  251. color: #7f8896;
  252. font-size: 24rpx;
  253. }
  254. .action-panel {
  255. margin-top: 20rpx;
  256. padding: 24rpx;
  257. }
  258. button {
  259. margin: 0;
  260. padding: 0;
  261. border: 0;
  262. background: transparent;
  263. line-height: normal;
  264. }
  265. button::after {
  266. border: 0;
  267. }
  268. .primary-action {
  269. height: 88rpx;
  270. border-radius: 999rpx;
  271. color: #ffffff;
  272. background: #0b8cff;
  273. display: flex;
  274. align-items: center;
  275. justify-content: center;
  276. gap: 12rpx;
  277. font-size: 29rpx;
  278. font-weight: 900;
  279. }
  280. .secondary-actions {
  281. display: grid;
  282. grid-template-columns: repeat(2, minmax(0, 1fr));
  283. gap: 16rpx;
  284. margin-top: 16rpx;
  285. }
  286. .secondary-action {
  287. height: 78rpx;
  288. border-radius: 999rpx;
  289. color: #0b8cff;
  290. background: #edf7ff;
  291. display: flex;
  292. align-items: center;
  293. justify-content: center;
  294. gap: 10rpx;
  295. font-size: 26rpx;
  296. font-weight: 800;
  297. }
  298. .action-icon {
  299. flex-shrink: 0;
  300. }
  301. .rule-card {
  302. margin-top: 20rpx;
  303. padding: 28rpx;
  304. }
  305. .rule-title {
  306. color: #172033;
  307. font-size: 30rpx;
  308. font-weight: 900;
  309. }
  310. .rule-text {
  311. display: block;
  312. margin-top: 14rpx;
  313. color: #687386;
  314. font-size: 25rpx;
  315. line-height: 1.5;
  316. }
  317. .rule-steps {
  318. margin-top: 22rpx;
  319. display: flex;
  320. flex-direction: column;
  321. gap: 14rpx;
  322. }
  323. .step {
  324. height: 58rpx;
  325. border-radius: 16rpx;
  326. background: #f5f9ff;
  327. display: flex;
  328. align-items: center;
  329. padding: 0 18rpx;
  330. color: #39445a;
  331. font-size: 25rpx;
  332. font-weight: 700;
  333. }
  334. .step-no {
  335. margin-right: 16rpx;
  336. color: #0b8cff;
  337. font-size: 21rpx;
  338. font-weight: 900;
  339. }
  340. .qr-mask {
  341. position: fixed;
  342. inset: 0;
  343. z-index: 99;
  344. background: rgba(9, 16, 28, 0.45);
  345. display: flex;
  346. align-items: flex-end;
  347. }
  348. .qr-sheet {
  349. width: 100%;
  350. padding: 28rpx 28rpx calc(30rpx + env(safe-area-inset-bottom));
  351. border-radius: 34rpx 34rpx 0 0;
  352. background: #ffffff;
  353. box-sizing: border-box;
  354. }
  355. .sheet-head,
  356. .sheet-actions {
  357. display: flex;
  358. align-items: center;
  359. justify-content: space-between;
  360. }
  361. .sheet-title {
  362. color: #172033;
  363. font-size: 32rpx;
  364. font-weight: 900;
  365. }
  366. .sheet-close {
  367. width: 58rpx;
  368. height: 58rpx;
  369. border-radius: 50%;
  370. color: #7f8896;
  371. background: #f4f7fb;
  372. font-size: 38rpx;
  373. line-height: 54rpx;
  374. text-align: center;
  375. }
  376. .qr-card {
  377. margin: 26rpx auto 24rpx;
  378. width: 430rpx;
  379. min-height: 500rpx;
  380. border-radius: 28rpx;
  381. background: #f6f9fd;
  382. display: flex;
  383. flex-direction: column;
  384. align-items: center;
  385. justify-content: center;
  386. }
  387. .qr-img {
  388. width: 360rpx;
  389. height: 360rpx;
  390. }
  391. .qr-loading,
  392. .qr-tip {
  393. color: #7f8896;
  394. font-size: 24rpx;
  395. }
  396. .qr-tip {
  397. margin-top: 18rpx;
  398. }
  399. .ghost-btn,
  400. .save-btn {
  401. flex: 1;
  402. height: 82rpx;
  403. border-radius: 999rpx;
  404. font-size: 27rpx;
  405. font-weight: 900;
  406. line-height: 82rpx;
  407. text-align: center;
  408. }
  409. .ghost-btn {
  410. margin-right: 18rpx;
  411. color: #0b8cff;
  412. background: #edf7ff;
  413. }
  414. .save-btn {
  415. color: #ffffff;
  416. background: #0b8cff;
  417. }
  418. </style>