feat: 支持邀请好友功能
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
57
packages/app/src/components/InviteCard.vue
Normal file
57
packages/app/src/components/InviteCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user