feat: 完善个人中心
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
:class="{ 'profile-menu__item--admin': item.isAdmin }"
|
:class="{ 'profile-menu__item--admin': item.isAdmin }"
|
||||||
hover-class="profile-menu__item--hover"
|
hover-class="profile-menu__item--hover"
|
||||||
hover-stay-time="150"
|
hover-stay-time="150"
|
||||||
@tap="navigate(item.path!)"
|
@tap="handleTap(item)"
|
||||||
>
|
>
|
||||||
<view class="profile-menu__icon-wrap" :class="{ 'profile-menu__icon-wrap--admin': item.isAdmin }">
|
<view class="profile-menu__icon-wrap" :class="{ 'profile-menu__icon-wrap--admin': item.isAdmin }">
|
||||||
<text class="profile-menu__icon">{{ item.icon }}</text>
|
<text class="profile-menu__icon">{{ item.icon }}</text>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<text class="profile-menu__title" :class="{ 'profile-menu__title--admin': item.isAdmin }">
|
<text class="profile-menu__title" :class="{ 'profile-menu__title--admin': item.isAdmin }">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</text>
|
</text>
|
||||||
|
<text v-if="item.badge" class="profile-menu__badge">{{ item.badge }}</text>
|
||||||
<text class="profile-menu__arrow">›</text>
|
<text class="profile-menu__arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,10 +36,20 @@ interface MenuItem {
|
|||||||
title?: string
|
title?: string
|
||||||
path?: string
|
path?: string
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
|
badge?: string
|
||||||
|
action?: 'clear' | 'about'
|
||||||
|
requireAuth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
requireAuth?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'clear-cache'): void
|
||||||
|
(e: 'about'): void
|
||||||
|
(e: 'require-login'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const menuItems = computed<MenuItem[]>(() => {
|
const menuItems = computed<MenuItem[]>(() => {
|
||||||
@@ -49,6 +60,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
icon: '💳',
|
icon: '💳',
|
||||||
title: '我的会员卡',
|
title: '我的会员卡',
|
||||||
path: '/pages/profile/membership',
|
path: '/pages/profile/membership',
|
||||||
|
requireAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'bookings',
|
key: 'bookings',
|
||||||
@@ -56,6 +68,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
icon: '📅',
|
icon: '📅',
|
||||||
title: '我的预约',
|
title: '我的预约',
|
||||||
path: '/pages/profile/bookings',
|
path: '/pages/profile/bookings',
|
||||||
|
requireAuth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'info',
|
key: 'info',
|
||||||
@@ -63,11 +76,30 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
icon: '👤',
|
icon: '👤',
|
||||||
title: '个人信息',
|
title: '个人信息',
|
||||||
path: '/pages/profile/info',
|
path: '/pages/profile/info',
|
||||||
|
requireAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sep1',
|
||||||
|
type: 'separator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clear',
|
||||||
|
type: 'item',
|
||||||
|
icon: '🗑️',
|
||||||
|
title: '清除缓存',
|
||||||
|
action: 'clear',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'about',
|
||||||
|
type: 'item',
|
||||||
|
icon: 'ℹ️',
|
||||||
|
title: '关于我们',
|
||||||
|
action: 'about',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (props.isAdmin) {
|
if (props.isAdmin) {
|
||||||
items.push({ key: 'sep', type: 'separator' })
|
items.push({ key: 'sep2', type: 'separator' })
|
||||||
items.push({
|
items.push({
|
||||||
key: 'admin',
|
key: 'admin',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@@ -75,14 +107,25 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
title: '管理中心',
|
title: '管理中心',
|
||||||
path: '/pages/admin/index',
|
path: '/pages/admin/index',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
|
requireAuth: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
function navigate(path: string) {
|
function handleTap(item: MenuItem) {
|
||||||
uni.navigateTo({ url: path })
|
if (item.requireAuth && !props.requireAuth) {
|
||||||
|
emit('require-login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.action === 'clear') {
|
||||||
|
emit('clear-cache')
|
||||||
|
} else if (item.action === 'about') {
|
||||||
|
emit('about')
|
||||||
|
} else if (item.path) {
|
||||||
|
uni.navigateTo({ url: item.path })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -90,7 +133,7 @@ function navigate(path: string) {
|
|||||||
.profile-menu {
|
.profile-menu {
|
||||||
background: $bg-card;
|
background: $bg-card;
|
||||||
border-radius: $radius-lg;
|
border-radius: $radius-lg;
|
||||||
margin: $spacing-md $spacing-lg 0;
|
margin: $spacing-lg $spacing-lg 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__separator {
|
&__separator {
|
||||||
@@ -111,11 +154,11 @@ function navigate(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--hover {
|
&--hover {
|
||||||
background: #f9f9f9;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--admin {
|
&--admin {
|
||||||
// Admin row gets a subtle accent tint
|
background: rgba($accent-color, 0.04);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +166,7 @@ function navigate(path: string) {
|
|||||||
width: 64rpx;
|
width: 64rpx;
|
||||||
height: 64rpx;
|
height: 64rpx;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
background: rgba($brand-color, 0.06);
|
background: rgba($brand-color, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -144,7 +187,6 @@ function navigate(path: string) {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
&--admin {
|
&--admin {
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
@@ -152,6 +194,15 @@ function navigate(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
background: $error-color;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
margin-right: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
&__arrow {
|
&__arrow {
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
color: $text-hint;
|
color: $text-hint;
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="studio-info">
|
<view class="studio-info">
|
||||||
<!-- Horizontal photo strip -->
|
<!-- Horizontal photo strip -->
|
||||||
<scroll-view
|
<scroll-view v-if="studioInfo?.photos?.length" scroll-x class="photo-strip" :show-scrollbar="false">
|
||||||
v-if="studioInfo?.photos?.length"
|
|
||||||
scroll-x
|
|
||||||
class="photo-strip"
|
|
||||||
:show-scrollbar="false"
|
|
||||||
>
|
|
||||||
<view class="photo-strip-inner">
|
<view class="photo-strip-inner">
|
||||||
<image
|
<image v-for="(photo, idx) in studioInfo.photos" :key="idx" class="strip-photo" :src="photo" mode="aspectFill"
|
||||||
v-for="(photo, idx) in studioInfo.photos"
|
@tap="previewPhoto(idx)" />
|
||||||
:key="idx"
|
|
||||||
class="strip-photo"
|
|
||||||
:src="photo"
|
|
||||||
mode="aspectFill"
|
|
||||||
@tap="previewPhoto(idx)"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
@@ -24,7 +13,7 @@
|
|||||||
<view class="location-left" @tap="handleAddressTap">
|
<view class="location-left" @tap="handleAddressTap">
|
||||||
<text class="location-icon">📍</text>
|
<text class="location-icon">📍</text>
|
||||||
<text class="location-text">
|
<text class="location-text">
|
||||||
{{ studioInfo?.address || '地址加载中…' }}
|
{{ studioInfo?.address || '深圳市宝安区西乡街道财富港 D 座 1203D' }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="phone-btn" @tap="handlePhoneTap">
|
<view class="phone-btn" @tap="handlePhoneTap">
|
||||||
@@ -129,7 +118,7 @@ function handlePhoneTap() {
|
|||||||
|
|
||||||
.location-left {
|
.location-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -138,7 +127,6 @@ function handlePhoneTap() {
|
|||||||
.location-icon {
|
.location-icon {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-text {
|
.location-text {
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="training-stats">
|
|
||||||
<view class="training-stats__item">
|
|
||||||
<text class="training-stats__value">{{ stats?.monthBookings ?? 0 }}</text>
|
|
||||||
<text class="training-stats__unit">次</text>
|
|
||||||
<text class="training-stats__label">本月训练</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="training-stats__divider" />
|
|
||||||
|
|
||||||
<view class="training-stats__item">
|
|
||||||
<text class="training-stats__value">{{ stats?.monthDays ?? 0 }}</text>
|
|
||||||
<text class="training-stats__unit">天</text>
|
|
||||||
<text class="training-stats__label">训练天数</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="training-stats__divider" />
|
|
||||||
|
|
||||||
<view class="training-stats__item">
|
|
||||||
<text class="training-stats__value">{{ stats?.monthHours ?? 0 }}</text>
|
|
||||||
<text class="training-stats__unit">小时</text>
|
|
||||||
<text class="training-stats__label">训练时长</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { UserStatsResponse } from '@mp-pilates/shared'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
stats: UserStatsResponse | null
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.training-stats {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
margin: 0 $spacing-lg;
|
|
||||||
padding: $spacing-md 0;
|
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
// Pull card up to overlap the dark header
|
|
||||||
margin-top: -$spacing-xl;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: $spacing-sm 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__divider {
|
|
||||||
width: 1rpx;
|
|
||||||
background: $border-color;
|
|
||||||
margin: $spacing-xs 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__value {
|
|
||||||
font-size: 48rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $brand-color;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__unit {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: $text-secondary;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
margin-top: $spacing-xs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="user-card">
|
<view class="user-card">
|
||||||
<!-- Not logged in state -->
|
<!-- Header: gradient background -->
|
||||||
<view v-if="!loggedIn" class="user-card__guest">
|
<view class="user-card__header">
|
||||||
<view class="user-card__guest-avatar">
|
<!-- Not logged in state -->
|
||||||
<image class="user-card__avatar-img" src="/static/default-avatar.png" mode="aspectFill" />
|
<view v-if="!loggedIn" class="user-card__guest">
|
||||||
|
<view class="user-card__avatar-wrap">
|
||||||
|
<image class="user-card__avatar-img" src="/static/default-avatar.png" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view class="user-card__guest-info">
|
||||||
|
<text class="user-card__guest-title">Hi,欢迎来到普拉提</text>
|
||||||
|
<text class="user-card__guest-sub">登录后查看个人数据</text>
|
||||||
|
</view>
|
||||||
|
<button class="user-card__login-btn" :loading="loading" @tap="handleLogin">
|
||||||
|
微信登录
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-card__guest-info">
|
|
||||||
<text class="user-card__guest-title">Hi,欢迎来到普拉提</text>
|
<!-- Logged in state -->
|
||||||
<text class="user-card__guest-sub">登录后查看个人数据</text>
|
<view v-else class="user-card__user">
|
||||||
|
<view class="user-card__avatar-wrap">
|
||||||
|
<image
|
||||||
|
class="user-card__avatar-img"
|
||||||
|
:src="avatarSrc"
|
||||||
|
mode="aspectFill"
|
||||||
|
@error="onAvatarError"
|
||||||
|
/>
|
||||||
|
<view class="user-card__vip-badge" v-if="vipLevel">
|
||||||
|
<text class="user-card__vip-text">{{ vipLevel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-card__info">
|
||||||
|
<view class="user-card__name-row">
|
||||||
|
<text class="user-card__nickname">{{ user!.nickname }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="maskedPhone" class="user-card__phone">{{ maskedPhone }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="user-card__login-btn" :loading="loading" @tap="handleLogin">
|
|
||||||
微信登录
|
|
||||||
</button>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Logged in state -->
|
<!-- Stats row: shown only when logged in -->
|
||||||
<view v-else class="user-card__user">
|
<view v-if="loggedIn" class="user-card__stats">
|
||||||
<view class="user-card__avatar-wrap">
|
<view class="user-card__stat-item">
|
||||||
<image
|
<text class="user-card__stat-value">{{ stats?.totalBookings ?? 0 }}</text>
|
||||||
class="user-card__avatar-img"
|
<text class="user-card__stat-label">总训练(次)</text>
|
||||||
:src="avatarSrc"
|
|
||||||
mode="aspectFill"
|
|
||||||
@error="onAvatarError"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="user-card__info">
|
<view class="user-card__stat-divider" />
|
||||||
<text class="user-card__nickname">{{ user!.nickname }}</text>
|
<view class="user-card__stat-item">
|
||||||
<text v-if="maskedPhone" class="user-card__phone">{{ maskedPhone }}</text>
|
<text class="user-card__stat-value">{{ stats?.monthBookings ?? 0 }}</text>
|
||||||
<text class="user-card__joined">注册于 {{ joinedDate }}</text>
|
<text class="user-card__stat-label">本月(次)</text>
|
||||||
|
</view>
|
||||||
|
<view class="user-card__stat-divider" />
|
||||||
|
<view class="user-card__stat-item">
|
||||||
|
<text class="user-card__stat-value">{{ remainingSessions }}</text>
|
||||||
|
<text class="user-card__stat-label">剩余课时(节)</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -35,12 +60,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { UserProfileResponse } from '@mp-pilates/shared'
|
import type { UserProfileResponse, UserStatsResponse, MembershipWithCardType } from '@mp-pilates/shared'
|
||||||
import { formatDate } from '../utils/format'
|
import { MembershipStatus } from '@mp-pilates/shared'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
user: UserProfileResponse | null
|
user: UserProfileResponse | null
|
||||||
|
stats: UserStatsResponse | null
|
||||||
|
memberships?: readonly MembershipWithCardType[]
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -60,15 +87,29 @@ const avatarSrc = computed(() => {
|
|||||||
const maskedPhone = computed(() => {
|
const maskedPhone = computed(() => {
|
||||||
const phone = props.user?.phone
|
const phone = props.user?.phone
|
||||||
if (!phone) return null
|
if (!phone) return null
|
||||||
// Mask middle 4 digits: 138****8888
|
|
||||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
})
|
})
|
||||||
|
|
||||||
const joinedDate = computed(() => {
|
// Derive VIP level from active memberships count
|
||||||
if (!props.user?.createdAt) return ''
|
const activeMemberships = computed(() =>
|
||||||
return formatDate(props.user.createdAt)
|
props.memberships?.filter((m) => m.status === MembershipStatus.ACTIVE) ?? [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const vipLevel = computed(() => {
|
||||||
|
const count = activeMemberships.value.length
|
||||||
|
if (count >= 3) return 'VIP3'
|
||||||
|
if (count >= 2) return 'VIP2'
|
||||||
|
if (count >= 1) return 'VIP1'
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sum remaining sessions from all active time-based memberships
|
||||||
|
const remainingSessions = computed(() =>
|
||||||
|
activeMemberships.value
|
||||||
|
.filter((m) => m.cardType.type === 'TIMES')
|
||||||
|
.reduce((sum, m) => sum + m.remainingCount, 0),
|
||||||
|
)
|
||||||
|
|
||||||
function onAvatarError() {
|
function onAvatarError() {
|
||||||
avatarFailed.value = true
|
avatarFailed.value = true
|
||||||
}
|
}
|
||||||
@@ -80,19 +121,21 @@ function handleLogin() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.user-card {
|
.user-card {
|
||||||
background: $brand-color;
|
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #ec4899 100%);
|
||||||
padding: 80rpx $spacing-lg $spacing-xl;
|
border-radius: 0 0 40rpx 40rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: 60rpx $spacing-lg $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Guest state ──
|
||||||
&__guest {
|
&__guest {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__guest-avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__guest-info {
|
&__guest-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -108,12 +151,12 @@ function handleLogin() {
|
|||||||
|
|
||||||
&__guest-sub {
|
&__guest-sub {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__login-btn {
|
&__login-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: $accent-color;
|
background: rgba(255, 255, 255, 0.2);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -129,6 +172,7 @@ function handleLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Logged-in user ──
|
||||||
&__user {
|
&__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -136,18 +180,35 @@ function handleLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__avatar-wrap {
|
&__avatar-wrap {
|
||||||
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 120rpx;
|
width: 120rpx;
|
||||||
height: 120rpx;
|
height: 120rpx;
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar-img {
|
&__avatar-img {
|
||||||
width: 120rpx;
|
width: 120rpx;
|
||||||
height: 120rpx;
|
height: 120rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__vip-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -6rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
border: 2rpx solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__vip-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #7c2d12;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@@ -157,6 +218,12 @@ function handleLogin() {
|
|||||||
gap: 8rpx;
|
gap: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
&__nickname {
|
&__nickname {
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -168,9 +235,43 @@ function handleLogin() {
|
|||||||
color: rgba(255, 255, 255, 0.75);
|
color: rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__joined {
|
// ── Stats row ──
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
margin: 0 $spacing-lg $spacing-lg;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: $spacing-md 0;
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $spacing-xs 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
margin: $spacing-xs 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-value {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-label {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-top: 6rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,46 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-page">
|
<view class="profile-page">
|
||||||
<!-- User card: always visible -->
|
<!-- User card -->
|
||||||
<UserCard
|
<UserCard
|
||||||
:logged-in="loggedIn"
|
:logged-in="loggedIn"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:stats="stats"
|
||||||
|
:memberships="memberships"
|
||||||
:loading="loginLoading"
|
:loading="loginLoading"
|
||||||
@login="handleLogin"
|
@login="handleLogin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Logged-in content -->
|
<!-- Menu section: always visible -->
|
||||||
<template v-if="loggedIn">
|
<ProfileMenu
|
||||||
<!-- Training stats: overlaps bottom of UserCard -->
|
:is-admin="isAdmin"
|
||||||
<TrainingStats :stats="stats" />
|
:require-auth="loggedIn"
|
||||||
|
@clear-cache="handleClearCache"
|
||||||
|
@about="handleAbout"
|
||||||
|
@require-login="handleLogin"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Menu section -->
|
<!-- Logout button: only when logged in -->
|
||||||
<ProfileMenu :is-admin="isAdmin" />
|
<view v-if="loggedIn" class="profile-page__logout-wrap">
|
||||||
|
<button class="profile-page__logout-btn" @tap="handleLogout">退出登录</button>
|
||||||
<!-- Logout button -->
|
</view>
|
||||||
<view class="profile-page__logout-wrap">
|
|
||||||
<button class="profile-page__logout-btn" @tap="handleLogout">退出登录</button>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { useUserStore } from '../../stores/user'
|
import { useUserStore } from '../../stores/user'
|
||||||
import UserCard from '../../components/UserCard.vue'
|
import UserCard from '../../components/UserCard.vue'
|
||||||
import TrainingStats from '../../components/TrainingStats.vue'
|
|
||||||
import ProfileMenu from '../../components/ProfileMenu.vue'
|
import ProfileMenu from '../../components/ProfileMenu.vue'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { loggedIn, user, stats, memberships, isAdmin } = storeToRefs(userStore)
|
||||||
const { loggedIn, user, stats, isAdmin } = userStore
|
|
||||||
|
|
||||||
const loginLoading = ref(false)
|
const loginLoading = ref(false)
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
if (loggedIn) {
|
if (loggedIn.value) {
|
||||||
await Promise.all([userStore.fetchProfile(), userStore.fetchStats()])
|
await Promise.all([
|
||||||
|
userStore.fetchProfile(),
|
||||||
|
userStore.fetchStats(),
|
||||||
|
userStore.fetchMemberships(),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -49,8 +54,11 @@ async function handleLogin() {
|
|||||||
loginLoading.value = true
|
loginLoading.value = true
|
||||||
try {
|
try {
|
||||||
await userStore.login()
|
await userStore.login()
|
||||||
// After login, fetch stats immediately
|
await Promise.all([
|
||||||
await Promise.all([userStore.fetchProfile(), userStore.fetchStats()])
|
userStore.fetchProfile(),
|
||||||
|
userStore.fetchStats(),
|
||||||
|
userStore.fetchMemberships(),
|
||||||
|
])
|
||||||
} catch {
|
} catch {
|
||||||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -71,6 +79,27 @@ function handleLogout() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClearCache() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '清除缓存',
|
||||||
|
content: '确定要清除本地缓存数据吗?',
|
||||||
|
success(res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.clearStorageSync()
|
||||||
|
uni.showToast({ title: '缓存已清除', icon: 'success' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAbout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '关于我们',
|
||||||
|
content: 'Focus Core 普拉提工作室\n版本 1.0.0\n\n专注核心,遇见更好的自己',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -78,11 +107,8 @@ function handleLogout() {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg-page;
|
background: $bg-page;
|
||||||
|
|
||||||
// Content area below the dark header card
|
|
||||||
// UserCard has its own dark bg, content sits on $bg-page
|
|
||||||
|
|
||||||
&__logout-wrap {
|
&__logout-wrap {
|
||||||
margin: $spacing-xl $spacing-lg $spacing-lg;
|
margin: $spacing-xl $spacing-lg $spacing-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__logout-btn {
|
&__logout-btn {
|
||||||
|
|||||||
Reference in New Issue
Block a user