feat: 优化个人中心邀请卡片

This commit is contained in:
richarjiang
2026-09-09 10:10:52 +08:00
parent 99c5c211ad
commit f4cee127ce
4 changed files with 49 additions and 13 deletions

View File

@@ -1,10 +1,16 @@
<template> <template>
<view class="invite-entry"> <view class="invite-entry">
<view class="invite-heading" @tap="open"> <button class="invite-heading" :disabled="opening" @tap="open" aria-label="邀请好友查看邀请码与邀请权益">
<view><text class="eyebrow">好友享 95 · 一起变好</text><text class="entry-title">邀请好友领课时 <text></text></text></view> <view class="entry-copy">
<view class="gift"><text class="gift-number">1</text><text>节免费课</text></view> <text class="entry-title">邀请好友领课时</text>
<text class="entry-caption">好友享 95 · {{ user.loggedIn && invite.activity ? `已获 ${invite.activity.rewardedTimes} 节赠课` : '一起练,得赠课' }}</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 class="invite-count">
<text class="count-number">{{ user.loggedIn && invite.activity ? invite.activity.referrals.length : '—' }}<text v-if="user.loggedIn"> </text></text>
<text class="count-label">{{ user.loggedIn ? '已邀请' : '登录查看' }}</text>
</view>
<text class="entry-arrow"></text>
</button>
<view v-if="visible" class="veil" @tap="visible = false" @touchmove.stop.prevent> <view v-if="visible" class="veil" @tap="visible = false" @touchmove.stop.prevent>
<view class="sheet" @tap.stop> <view class="sheet" @tap.stop>
<button class="close" @tap="visible = false" aria-label="关闭">×</button> <button class="close" @tap="visible = false" aria-label="关闭">×</button>
@@ -29,12 +35,16 @@ import { getErrorMessage } from '../utils/auth'
const invite = useInviteStore() const invite = useInviteStore()
const user = useUserStore() const user = useUserStore()
const visible = ref(false) const visible = ref(false)
const opening = ref(false)
async function open() { async function open() {
if (opening.value) return
opening.value = true
try { try {
if (!user.loggedIn) await user.login() if (!user.loggedIn) await user.login()
await invite.refresh() await Promise.all([invite.refresh(), invite.refreshActivity().catch(() => {})])
visible.value = true visible.value = true
} catch (err) { uni.showToast({ title: getErrorMessage(err, '暂时无法获取邀请码,请重试'), icon: 'none' }) } } catch (err) { uni.showToast({ title: getErrorMessage(err, '暂时无法获取邀请码,请重试'), icon: 'none' }) }
finally { opening.value = false }
} }
function copy() { function copy() {
if (!invite.code) { open(); return } if (!invite.code) { open(); return }
@@ -42,12 +52,16 @@ function copy() {
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.invite-entry { margin: 24rpx 32rpx; padding: 28rpx; background: #eaf0e7; border: 1rpx solid #d9e3d4; border-radius: 26rpx; color: #435b4c; } .invite-entry { margin: 16rpx 32rpx; padding: 0; background: #eaf0e7; border: 1rpx solid #d9e3d4; border-radius: 20rpx; color: #435b4c; }
.invite-heading { display: flex; justify-content: space-between; align-items: center; } .invite-heading { width: 100%; min-height: 120rpx; box-sizing: border-box; margin: 0; padding: 18rpx 24rpx; background: transparent; border-radius: 20rpx; color: inherit; display: flex; gap: 20rpx; text-align: left; align-items: center; line-height: 1.5; &::after { border: none; } &:active { background: #e2eadf; } }
.entry-copy { flex: 1; min-width: 0; }
.entry-title { display: block; font-size: 28rpx; font-weight: 500; }
.entry-caption { display: block; margin-top: 6rpx; font-size: 21rpx; color: #6b7d65; }
.invite-count { flex-shrink: 0; text-align: right; }
.count-number { display: block; font-family: Georgia, serif; font-size: 34rpx; line-height: 1.2; font-variant-numeric: tabular-nums; text { font-size: 20rpx; } }
.count-label { display: block; margin-top: 5rpx; font-size: 19rpx; color: #6b7d65; }
.entry-arrow { flex-shrink: 0; font-size: 30rpx; color: #87967f; }
.eyebrow { display: block; font-size: 19rpx; letter-spacing: 3rpx; color: #788976; } .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; } .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; } .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; } .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; }

View File

@@ -88,6 +88,7 @@ onShow(async () => {
if (loggedIn.value) { if (loggedIn.value) {
await Promise.all([ await Promise.all([
invite.refresh().catch(() => {}), invite.refresh().catch(() => {}),
invite.refreshActivity().catch(() => {}),
userStore.fetchProfile(), userStore.fetchProfile(),
userStore.fetchStats(), userStore.fetchStats(),
userStore.fetchMemberships(), userStore.fetchMemberships(),
@@ -103,6 +104,7 @@ async function handleLogin() {
const { isNewUser } = await userStore.loginWithSetup() const { isNewUser } = await userStore.loginWithSetup()
if (!isNewUser) { if (!isNewUser) {
await Promise.all([ await Promise.all([
invite.refreshActivity().catch(() => {}),
userStore.fetchStats(), userStore.fetchStats(),
bookingStore.fetchUpcomingBookings(), bookingStore.fetchUpcomingBookings(),
]) ])

View File

@@ -9,6 +9,11 @@ import { getSystemLayout } from '../../utils/system'
const invite = useInviteStore() const invite = useInviteStore()
const user = useUserStore() const user = useUserStore()
const navBarHeight = `${getSystemLayout().navBarHeight}px` const navBarHeight = `${getSystemLayout().navBarHeight}px`
onShow(() => { if (user.loggedIn) invite.refresh().catch(() => {}) }) onShow(() => {
if (user.loggedIn) {
invite.refresh().catch(() => {})
invite.refreshActivity().catch(() => {})
}
})
onShareAppMessage(() => ({ title: '送你 95 折购卡礼,一起练普拉提', path: invite.code ? `/pages/card/detail?showAll=1&inviteCode=${invite.code}` : '/pages/home/index' })) onShareAppMessage(() => ({ title: '送你 95 折购卡礼,一起练普拉提', path: invite.code ? `/pages/card/detail?showAll=1&inviteCode=${invite.code}` : '/pages/home/index' }))
</script> </script>

View File

@@ -1,8 +1,21 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import type { InviteActivitySummary } from '@mp-pilates/shared'
import { get, post } from '../utils/request' import { get, post } from '../utils/request'
export const useInviteStore = defineStore('invite', () => { export const useInviteStore = defineStore('invite', () => {
const activity = ref<InviteActivitySummary | null>(null)
let activityRevision = 0
async function refreshActivity() {
const requestRevision = ++activityRevision
try {
const result = await get<InviteActivitySummary>('/invite/activity')
if (requestRevision === activityRevision) activity.value = result
} catch (err) {
if (requestRevision === activityRevision) activity.value = null
throw err
}
}
const code = ref('') const code = ref('')
const eligible = ref(false) const eligible = ref(false)
const pendingCode = ref('') const pendingCode = ref('')
@@ -30,11 +43,13 @@ export const useInviteStore = defineStore('invite', () => {
} }
} }
function reset() { function reset() {
activityRevision++
activity.value = null
revision++ revision++
code.value = '' code.value = ''
eligible.value = false eligible.value = false
pendingCode.value = '' pendingCode.value = ''
} }
function price(value: number) { return eligible.value ? Math.round(value * 95 / 100) : value } function price(value: number) { return eligible.value ? Math.round(value * 95 / 100) : value }
return { code, eligible, pendingCode, refresh, confirm, reset, price } return { activity, refreshActivity, code, eligible, pendingCode, refresh, confirm, reset, price }
}) })