perf: 个人中心支持展示约课数量

This commit is contained in:
richarjiang
2026-04-12 22:27:36 +08:00
parent 6cee28bf66
commit 1f45c3dc3f
3 changed files with 107 additions and 4 deletions

View File

@@ -48,6 +48,7 @@ const props = defineProps<{
isAdmin: boolean isAdmin: boolean
requireAuth?: boolean requireAuth?: boolean
activeMembershipCount?: number activeMembershipCount?: number
upcomingBookingCount?: number
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -60,6 +61,9 @@ const menuItems = computed<MenuItem[]>(() => {
const membershipBadge = props.activeMembershipCount && props.activeMembershipCount > 0 const membershipBadge = props.activeMembershipCount && props.activeMembershipCount > 0
? `${props.activeMembershipCount}` ? `${props.activeMembershipCount}`
: undefined : undefined
const bookingBadge = props.upcomingBookingCount && props.upcomingBookingCount > 0
? `${props.upcomingBookingCount}`
: undefined
const items: MenuItem[] = [ const items: MenuItem[] = [
{ {
@@ -75,6 +79,7 @@ const menuItems = computed<MenuItem[]>(() => {
type: 'item', type: 'item',
title: '我的预约', title: '我的预约',
path: '/pages/profile/bookings', path: '/pages/profile/bookings',
badge: bookingBadge,
requireAuth: true, requireAuth: true,
}, },
{ {

View File

@@ -21,9 +21,12 @@
<template v-else-if="!isSlotMode && booking"> <template v-else-if="!isSlotMode && booking">
<!-- Booking info card --> <!-- Booking info card -->
<view class="info-card"> <view class="info-card">
<!-- Status banner --> <view class="info-card-header">
<view class="status-banner" :class="bookingStatusBannerClass(booking.status)"> <view class="status-banner" :class="bookingStatusBannerClass(booking.status)">
<text class="status-banner-text">{{ bookingStatusLabel(booking.status) }}</text> <view class="status-banner-dot" />
<text class="status-banner-text">{{ bookingStatusLabel(booking.status) }}</text>
</view>
<text class="status-banner-hint">{{ bookingStatusHint(booking.status) }}</text>
</view> </view>
<!-- Course info --> <!-- Course info -->
@@ -326,6 +329,23 @@ function formatDateTime(dateStr: string): string {
return `${y}-${m}-${day} ${hh}:${mm}` return `${y}-${m}-${day} ${hh}:${mm}`
} }
function bookingStatusHint(status: string): string {
switch (status) {
case BookingStatus.PENDING_CONFIRMATION:
return '预约已提交,等待教练确认后生效'
case BookingStatus.CONFIRMED:
return '课程已确认,请按时到店上课'
case BookingStatus.COMPLETED:
return '本次课程已完成'
case BookingStatus.CANCELLED:
return '该预约已取消'
case BookingStatus.NO_SHOW:
return '该预约已标记为未出席'
default:
return '查看当前预约状态'
}
}
// ─── Data loading ───────────────────────────────────────────────────────── // ─── Data loading ─────────────────────────────────────────────────────────
async function loadBookingData() { async function loadBookingData() {
loading.value = true loading.value = true
@@ -586,6 +606,72 @@ onLoad((query) => {
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.05);
} }
.info-card-header {
padding: 28rpx 24rpx 22rpx;
background: linear-gradient(180deg, rgba(160, 144, 128, 0.08), rgba(160, 144, 128, 0.02));
border-bottom: 1rpx solid rgba(160, 144, 128, 0.08);
}
.status-banner {
display: inline-flex;
align-items: center;
gap: 10rpx;
padding: 12rpx 20rpx;
border-radius: 999rpx;
border: 1rpx solid transparent;
}
.status-banner-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: currentColor;
}
.status-banner-text {
font-size: 24rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.status-banner-hint {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.6;
color: #8c8175;
}
.banner--pending {
color: #c9811c;
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.18);
}
.banner--confirmed {
color: #8b6b4d;
background: rgba(160, 144, 128, 0.12);
border-color: rgba(160, 144, 128, 0.2);
}
.banner--completed {
color: #2f8f67;
background: rgba(102, 187, 106, 0.12);
border-color: rgba(102, 187, 106, 0.2);
}
.banner--cancelled {
color: #9b9b9b;
background: rgba(224, 224, 224, 0.42);
border-color: rgba(210, 210, 210, 0.8);
}
.banner--noshow {
color: #d85b57;
background: rgba(239, 83, 80, 0.12);
border-color: rgba(239, 83, 80, 0.18);
}
.info-section { .info-section {
padding: 24rpx; padding: 24rpx;
border-bottom: 1rpx solid #f5f5f5; border-bottom: 1rpx solid #f5f5f5;

View File

@@ -12,6 +12,7 @@
:is-admin="isAdmin" :is-admin="isAdmin"
:require-auth="loggedIn" :require-auth="loggedIn"
:active-membership-count="activeMembershipCount" :active-membership-count="activeMembershipCount"
:upcoming-booking-count="upcomingBookingCount"
@clear-cache="handleClearCache" @clear-cache="handleClearCache"
@about="handleAbout" @about="handleAbout"
@require-login="handleLogin" @require-login="handleLogin"
@@ -29,6 +30,7 @@ import { ref, computed, onMounted } from 'vue'
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useUserStore } from '../../stores/user' import { useUserStore } from '../../stores/user'
import { useBookingStore } from '../../stores/booking'
import { getSystemLayout } from '../../utils/system' import { getSystemLayout } from '../../utils/system'
import { getErrorMessage } from '../../utils/auth' import { getErrorMessage } from '../../utils/auth'
import UserCard from '../../components/UserCard.vue' import UserCard from '../../components/UserCard.vue'
@@ -36,7 +38,9 @@ import ProfileMenu from '../../components/ProfileMenu.vue'
import CustomNavBar from '../../components/CustomNavBar.vue' import CustomNavBar from '../../components/CustomNavBar.vue'
const userStore = useUserStore() const userStore = useUserStore()
const bookingStore = useBookingStore()
const { loggedIn, hasProfile, user, stats, memberships, isAdmin } = storeToRefs(userStore) const { loggedIn, hasProfile, user, stats, memberships, isAdmin } = storeToRefs(userStore)
const { upcomingBookings } = storeToRefs(bookingStore)
const loginLoading = ref(false) const loginLoading = ref(false)
const navBarHeight = ref(64) const navBarHeight = ref(64)
@@ -45,6 +49,10 @@ const activeMembershipCount = computed(
() => user.value?.activeMembershipCount ?? userStore.activeMemberships.length, () => user.value?.activeMembershipCount ?? userStore.activeMemberships.length,
) )
const upcomingBookingCount = computed(
() => (loggedIn.value ? upcomingBookings.value.length : 0),
)
// ─── 微信分享 ─────────────────────────────────────────────── // ─── 微信分享 ───────────────────────────────────────────────
onShareAppMessage(() => { onShareAppMessage(() => {
return { return {
@@ -71,6 +79,7 @@ onShow(async () => {
userStore.fetchProfile(), userStore.fetchProfile(),
userStore.fetchStats(), userStore.fetchStats(),
userStore.fetchMemberships(), userStore.fetchMemberships(),
bookingStore.fetchUpcomingBookings(),
]) ])
} }
}) })
@@ -81,7 +90,10 @@ async function handleLogin() {
try { try {
const { isNewUser } = await userStore.loginWithSetup() const { isNewUser } = await userStore.loginWithSetup()
if (!isNewUser) { if (!isNewUser) {
await userStore.fetchStats() await Promise.all([
userStore.fetchStats(),
bookingStore.fetchUpcomingBookings(),
])
} }
} catch (err: unknown) { } catch (err: unknown) {
uni.showToast({ title: getErrorMessage(err, '登录失败,请重试'), icon: 'none' }) uni.showToast({ title: getErrorMessage(err, '登录失败,请重试'), icon: 'none' })