feat: 支持邀请好友功能

This commit is contained in:
richarjiang
2026-09-08 22:31:39 +08:00
parent 5a4d2c7a1b
commit 30ebc1c344
21 changed files with 445 additions and 632 deletions

View File

@@ -58,7 +58,7 @@
<text class="card-times-unit">课时</text>
</view>
<view class="price-row">
<text class="price-current">¥{{ formatPrice(card.price) }}</text>
<text class="price-current">¥{{ formatPrice(invite.price(card.price)) }}</text>
<text
v-if="card.originalPrice && card.originalPrice > card.price"
class="price-original"
@@ -69,6 +69,7 @@
</view>
</view>
<text v-if="invite.eligible" class="renew-tag">95 </text>
<!-- Arrow -->
<text class="card-arrow"></text>
</view>
@@ -82,6 +83,7 @@
</template>
<script setup lang="ts">
import { useInviteStore } from '../stores/invite'
import { computed, ref, onMounted } from 'vue'
import type { CardType } from '@mp-pilates/shared'
import { CardTypeCategory } from '@mp-pilates/shared'
@@ -89,6 +91,7 @@ import { get } from '../utils/request'
import { formatPrice, getCardCoverClass } from '../utils/format'
import { useUserStore } from '../stores/user'
const invite = useInviteStore()
const userStore = useUserStore()
const cardTypes = ref<CardType[]>([])
const loading = ref(false)

View File

@@ -52,8 +52,9 @@
<view class="price-area">
<view class="flash-price-row">
<text class="flash-currency">¥</text>
<text class="flash-price">{{ formatPrice(sale.flashPrice) }}</text>
<text class="flash-price">{{ formatPrice(invite.price(sale.flashPrice)) }}</text>
</view>
<text v-if="invite.eligible" class="original-price">好友 95 </text>
<text class="original-price">¥{{ formatPrice(sale.originalPrice) }}</text>
</view>
@@ -78,6 +79,8 @@
</template>
<script setup lang="ts">
import { useInviteStore } from "../stores/invite"
const invite = useInviteStore()
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { FlashSalePhase } from '@mp-pilates/shared'
import type { FlashSaleListItem } from '@mp-pilates/shared'

View File

@@ -0,0 +1,57 @@
<template>
<view class="invite-entry">
<view class="invite-heading" @tap="open">
<view><text class="eyebrow">好友享 95 · 一起变好</text><text class="entry-title">邀请好友领课时 <text></text></text></view>
<view class="gift"><text class="gift-number">1</text><text>节免费课</text></view>
</view>
<view class="code-line" @tap="copy"><text>{{ invite.code ? '我的邀请码' : '登录领取专属邀请码' }}</text><text class="code">{{ invite.code }}</text><text v-if="invite.code">复制</text></view>
<view v-if="visible" class="veil" @tap="visible = false" @touchmove.stop.prevent>
<view class="sheet" @tap.stop>
<button class="close" @tap="visible = false" aria-label="关闭">×</button>
<text class="eyebrow">A LITTLE GIFT, SHARED</text>
<text class="headline">好朋友好好练</text>
<text class="subtitle">把你的练习日常分享给在意的人</text>
<view class="benefits"><view><text class="benefit-num">1<text> </text></text><text>你得免费课</text></view><view><text class="benefit-num">95<text> </text></text><text>好友购卡优惠</text></view></view>
<view class="ticket" @tap="copy"><text>你的专属邀请码 · 点击复制</text><text class="ticket-code">{{ invite.code }}</text></view>
<text class="rules">好友确认邀请码体验卡次卡和期限卡均享 95 每位好友首次成功购买非体验卡你得 1 节免费课购买或完成体验课不触发赠课</text>
<text class="rules">赠课自动存入我的会员卡365 天内可预约每位好友仅绑定一位邀请人</text>
<button class="share" open-type="share" :disabled="!invite.code">分享给微信好友</button>
<text class="footnote">送朋友一份心意也给自己一次练习</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useInviteStore } from '../stores/invite'
import { useUserStore } from '../stores/user'
import { getErrorMessage } from '../utils/auth'
const invite = useInviteStore()
const user = useUserStore()
const visible = ref(false)
async function open() {
try {
if (!user.loggedIn) await user.login()
await invite.refresh()
visible.value = true
} catch (err) { uni.showToast({ title: getErrorMessage(err, '暂时无法获取邀请码,请重试'), icon: 'none' }) }
}
function copy() {
if (!invite.code) { open(); return }
uni.setClipboardData({ data: invite.code })
}
</script>
<style scoped lang="scss">
.invite-entry { margin: 24rpx 32rpx; padding: 28rpx; background: #eaf0e7; border: 1rpx solid #d9e3d4; border-radius: 26rpx; color: #435b4c; }
.invite-heading { display: flex; justify-content: space-between; align-items: center; }
.eyebrow { display: block; font-size: 19rpx; letter-spacing: 3rpx; color: #788976; }
.entry-title { display: block; font-size: 29rpx; margin-top: 12rpx; font-weight: 500; }
.gift { display: flex; flex-direction: column; align-items: center; font-size: 18rpx; }.gift-number { font-family: Georgia, serif; font-size: 56rpx; line-height: 1; }
.code-line { display: flex; align-items: center; gap: 18rpx; border-top: 1rpx solid #d2dece; padding-top: 20rpx; margin-top: 22rpx; font-size: 21rpx; }.code { flex: 1; letter-spacing: 4rpx; font-family: monospace; font-size: 27rpx; }
.veil { position: fixed; inset: 0; z-index: 400; background: rgba(32,43,35,.48); display: flex; align-items: center; padding: 30rpx; }
.sheet { position: relative; width: 100%; max-height: 85vh; overflow-y: auto; box-sizing: border-box; padding: 48rpx 34rpx 32rpx; border-radius: 32rpx; background: #fcfaf5; }.close { position: absolute; right: 18rpx; top: 12rpx; margin: 0; background: transparent; color: #7b8477; font-size: 36rpx; }.close::after,.share::after { border: none; }
.headline { display: block; margin-top: 20rpx; font-family: 'Songti SC',serif; font-size: 46rpx; }.subtitle { display: block; margin-top: 12rpx; font-size: 23rpx; color: #87907f; }
.benefits { display: flex; margin: 30rpx 0; padding: 25rpx 0; background: #eaf0e5; border-radius: 22rpx; }.benefits>view { flex: 1; text-align: center; font-size: 24rpx; }.benefits>view+view { border-left: 1rpx solid #cbd8c5; }.benefit-num { display: block; font: 66rpx Georgia,serif; margin-bottom: 10rpx; }.benefit-num text { font-size: 26rpx; }
.ticket { padding: 22rpx; text-align: center; border: 1rpx dashed #b7c5af; border-radius: 16rpx; font-size: 21rpx; color: #7a8772; }.ticket-code { display: block; margin-top: 12rpx; font: 42rpx monospace; letter-spacing: 10rpx; color: #435b4c; }
.rules { display: block; margin-top: 18rpx; font-size: 21rpx; line-height: 1.8; color: #7d8076; }.share { margin-top: 26rpx; border-radius: 999rpx; background: #526e58; color: #fff; font-size: 27rpx; line-height: 86rpx; }.footnote { display: block; text-align: center; margin-top: 16rpx; font-size: 19rpx; color: #93978a; }
</style>