会员个人中心页面初步完成

This commit is contained in:
时舟年
2026-06-04 14:18:53 +08:00
committed by liwentao
parent c19e0e0181
commit f30514c700
170 changed files with 18092 additions and 35 deletions
+61 -32
View File
@@ -1,59 +1,91 @@
<template>
<!-- 底部导航栏容器 -->
<view class="tab-bar">
<!-- 导航栏项 -->
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: activeTab === index }]"
@click="activeTab = index"
:key="tab.path"
:class="['tab-item', { active: currentIndex === index }]"
hover-class="tab-item--hover"
@tap="onTabTap(index)"
>
<!-- 导航栏图标 -->
<image :src="activeTab === index ? tab.iconActive : tab.icon" mode="aspectFit" class="tab-icon" />
<!-- 导航栏标签文字 -->
<image
:src="currentIndex === index ? tab.iconActive : tab.icon"
mode="aspectFit"
class="tab-icon"
/>
<text class="tab-label">{{ tab.label }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import {
PAGE,
TAB_ROUTES,
getCurrentRoutePath,
getTabIndexByRoute,
switchToTab
} from '@/common/constants/routes.js'
// 当前激活的导航栏索引
const activeTab = ref(0)
const props = defineProps({
/** 当前 Tab 索引,由 Tab 页传入以保证高亮准确 */
active: {
type: Number,
default: -1
}
})
const tapping = ref(false)
// 导航栏数据列表
const tabs = [
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/home.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/home.png',
label: '首页',
path: PAGE.INDEX,
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/home.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/home.png',
label: '首页'
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/course.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/course.png',
label: '课程'
path: PAGE.COURSE,
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/course.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/course.png',
label: '课程'
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/train.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/train.png',
label: '训练' ,
path: PAGE.TRAIN,
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/train.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/train.png',
label: '训练'
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/discover.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/discover.png',
label: '发现',
path: PAGE.DISCOVER,
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/discover.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/discover.png',
label: '发现'
},
{
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/profile.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/profile.png',
label: '我的',
path: PAGE.MEMBER,
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/profile.png',
iconActive: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/tabBar/active/profile.png',
label: '我的'
}
]
const currentIndex = computed(() => {
if (props.active >= 0) return props.active
return getTabIndexByRoute(getCurrentRoutePath())
})
function onTabTap(index) {
if (tapping.value || index === currentIndex.value) return
tapping.value = true
switchToTab(TAB_ROUTES[index])
setTimeout(() => {
tapping.value = false
}, 350)
}
</script>
<style lang="scss" scoped>
/* 底部导航栏容器样式 */
.tab-bar {
position: fixed;
bottom: 0;
@@ -68,9 +100,9 @@ const tabs = [
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
border-radius: 32rpx 32rpx 0 0;
z-index: 999;
}
/* 导航栏项样式 */
.tab-item {
display: flex;
flex-direction: column;
@@ -79,19 +111,16 @@ const tabs = [
padding: 12rpx 24rpx;
}
/* 导航栏图标样式 */
.tab-icon {
width: 40rpx;
height: 40rpx;
}
/* 导航栏标签文字样式 */
.tab-label {
font-size: 22rpx;
color: #94a3b8;
}
/* 导航栏激活状态文字样式 */
.tab-item.active .tab-label {
color: #f97316;
font-weight: 600;
@@ -0,0 +1,79 @@
<template>
<view class="bt-radar">
<canvas
:id="canvasId"
:canvas-id="canvasId"
type="2d"
class="bt-radar__canvas"
:style="{ width: width + 'px', height: height + 'px' }"
/>
</view>
</template>
<script>
import { drawRadarChart } from '@/common/memberInfo/bodyTestChart.js'
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
labels: { type: Array, default: () => [] },
values: { type: Array, default: () => [] },
width: { type: Number, default: 280 },
height: { type: Number, default: 240 }
},
data() {
return {
canvasId: `bt-radar-${Math.random().toString(36).slice(2, 9)}`
}
},
watch: {
values: {
deep: true,
handler() {
this.renderChart()
}
}
},
mounted() {
this.renderChart()
},
methods: {
renderChart() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query
.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
const node = res?.[0]?.node
if (!node) return
const dpr = uni.getSystemInfoSync().pixelRatio || 1
drawRadarChart(node, {
width: this.width,
height: this.height,
labels: this.labels,
values: this.values,
dpr
})
})
})
}
}
}
</script>
<style>
.bt-radar {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.bt-radar__canvas {
display: block;
}
</style>
@@ -0,0 +1,79 @@
<template>
<view class="bt-trend">
<canvas
:id="canvasId"
:canvas-id="canvasId"
type="2d"
class="bt-trend__canvas"
:style="{ width: width + 'px', height: height + 'px' }"
/>
</view>
</template>
<script>
import { drawTrendChart } from '@/common/memberInfo/bodyTestChart.js'
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
points: { type: Array, default: () => [] },
unit: { type: String, default: '' },
width: { type: Number, default: 300 },
height: { type: Number, default: 160 }
},
data() {
return {
canvasId: `bt-trend-${Math.random().toString(36).slice(2, 9)}`
}
},
watch: {
points: {
deep: true,
handler() {
this.renderChart()
}
}
},
mounted() {
this.renderChart()
},
methods: {
renderChart() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query
.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
const node = res?.[0]?.node
if (!node) return
const dpr = uni.getSystemInfoSync().pixelRatio || 1
drawTrendChart(node, {
width: this.width,
height: this.height,
points: this.points,
unit: this.unit,
dpr
})
})
})
}
}
}
</script>
<style>
.bt-trend {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.bt-trend__canvas {
display: block;
}
</style>
@@ -0,0 +1,133 @@
<template>
<view class="body-report-section">
<view class="body-report-section__inner">
<view class="body-report-section__header">
<view class="body-report-section__header-inner">
<text class="body-report-section__title">体测报告</text>
<view
class="body-report-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-history')"
>
<text class="body-report-section__history-link">历史记录</text>
<image
class="body-report-section__link-arrow"
src="/static/images/chevronright3.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view class="body-report-section__card">
<view class="body-report-section__card-inner">
<view class="body-report-section__card-head">
<view class="body-report-section__card-head-inner">
<text class="body-report-section__desc">
最新数据 · {{ report.date }}
</text>
<view
class="body-report-section__view-btn"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap="$emit('view-report')"
>
<image
class="body-report-section__view-icon"
src="/static/images/filetext.png"
mode="aspectFit"
/>
<text class="body-report-section__view-report">查看报告</text>
</view>
</view>
</view>
<view class="body-report-section__metrics">
<view class="body-report-section__metrics-inner">
<view class="body-report-section__metric">
<view class="body-report-section__metric-inner">
<text class="body-report-section__text">{{ report.weight }}</text>
<text class="body-report-section__metric-value">体重(kg)</text>
</view>
</view>
<view class="body-report-section__metric-divider"></view>
<view class="body-report-section__metric">
<view class="body-report-section__metric-inner">
<text class="body-report-section__text-2">{{ report.bmi }}</text>
<text class="body-report-section__text-3">BMI</text>
</view>
</view>
<view class="body-report-section__metric-divider"></view>
<view class="body-report-section__metric">
<view class="body-report-section__metric-inner">
<text class="body-report-section__text-4">{{ report.bodyFat }}</text>
<text class="body-report-section__metric-label">体脂率</text>
</view>
</view>
<view class="body-report-section__metric-divider"></view>
<view class="body-report-section__metric">
<view class="body-report-section__metric-inner">
<text class="body-report-section__num">{{ report.bmr }}</text>
<text class="body-report-section__text-5">BMR</text>
</view>
</view>
</view>
</view>
<view class="body-report-section__summary">
<view class="body-report-section__summary-inner">
<view class="body-report-section__goal">
<image
class="body-report-section__goal-icon"
src="/static/images/target.png"
mode="aspectFit"
/>
<text class="body-report-section__goal-text">
状态{{ report.status }}
</text>
</view>
<view class="body-report-section__change">
<image
class="body-report-section__change-icon"
src="/static/images/trendingdown.png"
mode="aspectFit"
/>
<text class="body-report-section__metric-value-2">
较上次 {{ report.change }}
</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
report: {
type: Object,
default: () => ({
date: '2024-07-01',
weight: '63.5',
bmi: '22.1',
bodyFat: '24.8%',
bmr: '165',
status: '比较健康',
change: '-1.2kg'
})
}
},
emits: ['view-history', 'view-report']
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-body-report.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,117 @@
<template>
<view class="booking-section">
<view class="booking-section__inner">
<view class="booking-section__header">
<view class="booking-section__header-inner">
<text class="booking-section__title">我的预约</text>
<view
class="booking-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text class="booking-section__view-all">预约记录</text>
<image
class="booking-section__link-arrow"
src="/static/images/chevronright4.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view
v-for="item in previewItems"
:key="item.id"
class="booking-section__item"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="$emit('item-tap', item)"
>
<view class="booking-section__item-inner">
<view class="booking-section__date">
<view class="booking-section__date-inner">
<text class="booking-section__num">{{ item.dateDay }}</text>
<text class="booking-section__date-sub">{{ item.dateMonth }}</text>
</view>
</view>
<view class="booking-section__content">
<view class="booking-section__content-inner">
<text class="booking-section__desc">{{ item.desc }}</text>
<view class="booking-section__meta">
<view class="booking-section__meta-inner">
<image
class="booking-section__icon-coach"
src="/static/images/user2.png"
mode="aspectFit"
/>
<text class="booking-section__coach">教练{{ item.coach }}</text>
<image
class="booking-section__icon-location"
src="/static/images/mappin1.png"
mode="aspectFit"
/>
<text class="booking-section__text">{{ item.location }}</text>
</view>
</view>
</view>
</view>
<view class="booking-section__status-wrap">
<view
class="booking-section__status-badge"
:class="'booking-section__status-badge--' + item.status"
>
<text
class="booking-section__status-text"
:class="{ 'booking-section__status-text--pending': item.status === 'pending' }"
>
{{ item.statusLabel }}
</text>
</view>
</view>
</view>
</view>
<view v-if="!previewItems.length" class="booking-section__empty">
<text class="booking-section__empty-text">暂无进行中的预约</text>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
items: {
type: Array,
default: () => []
}
},
emits: ['view-all', 'item-tap'],
computed: {
previewItems() {
return this.items
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-booking-list.css';
@import '@/common/style/memberInfo/member-info-tap.css';
.booking-section__empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
.booking-section__empty-text {
font-size: 14px;
color: #8A99B4;
}
</style>
@@ -0,0 +1,87 @@
<template>
<view class="checkin-section">
<view class="checkin-section__inner">
<view class="checkin-section__header">
<view class="checkin-section__header-inner">
<text class="checkin-section__title">签到记录</text>
<view
class="checkin-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text class="checkin-section__view-all">查看全部</text>
<image
class="checkin-section__link-arrow"
src="/static/images/chevronright2.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view class="checkin-section__list">
<view class="checkin-section__list-inner">
<view
v-for="(item, index) in items"
:key="item.id"
class="checkin-section__row"
>
<view v-if="index > 0" class="checkin-section__divider"></view>
<view
class="checkin-section__item"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="$emit('item-tap', item)"
>
<view class="checkin-section__item-inner">
<view
class="checkin-section__dot"
:class="'checkin-section__dot--' + item.tagTheme"
></view>
<view class="checkin-section__content">
<view class="checkin-section__content-inner">
<text class="checkin-section__desc">{{ item.title }}</text>
<text class="checkin-section__text">{{ item.time }}</text>
</view>
</view>
<view
class="checkin-section__tag-badge"
:class="'checkin-section__tag-badge--' + item.tagTheme"
>
<text
class="checkin-section__tag-text"
:class="'checkin-section__tag-text--' + item.tagTheme"
>
{{ item.tag }}
</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
items: {
type: Array,
default: () => []
}
},
emits: ['view-all', 'item-tap']
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-check-in-list.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,85 @@
<template>
<view class="coupon-section">
<view class="coupon-section__inner">
<view class="coupon-section__header">
<view class="coupon-section__header-inner">
<text class="coupon-section__title">优惠券 & 积分</text>
<view
class="coupon-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text class="coupon-section__view-all">更多详情</text>
<image
class="coupon-section__link-arrow"
src="/static/images/chevronright5.png"
mode="aspectFit"
/>
</view>
</view>
</view>
<view class="coupon-section__cards">
<view class="coupon-section__cards-inner">
<view
class="coupon-section__coupon"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
@tap="$emit('use-coupon')"
>
<view class="coupon-section__coupon-inner">
<text class="coupon-section__amount">{{ data.amount }}</text>
<text class="coupon-section__desc">{{ data.couponDesc }}</text>
<view class="coupon-section__coupon-status">
<text class="coupon-section__status">{{ data.couponAction }}</text>
</view>
</view>
</view>
<view
class="coupon-section__points"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
@tap="$emit('redeem-points')"
>
<view class="coupon-section__points-inner">
<text class="coupon-section__num">{{ data.points }}</text>
<text class="coupon-section__points-label">{{ data.pointsLabel }}</text>
<view class="coupon-section__points-action">
<text class="coupon-section__text">{{ data.pointsAction }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
data: {
type: Object,
default: () => ({
amount: '¥50',
couponDesc: '满500可用 · 1张',
couponAction: '去使用',
points: 1250,
pointsLabel: '我的积分',
pointsAction: '去兑换'
})
}
},
emits: ['view-all', 'use-coupon', 'redeem-points']
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-coupon-points.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,151 @@
<template>
<view class="profile-header">
<!-- 顶栏白底居中标题左侧放通知/设置右侧留胶囊安全区 -->
<view class="profile-header__toolbar" :style="toolbarStyle">
<view class="profile-header__nav">
<view class="profile-header__nav-left">
<image
class="profile-header__icon-bell"
src="/static/images/bell.png"
mode="aspectFit"
/>
<image
class="profile-header__icon-settings"
src="/static/images/settings.png"
mode="aspectFit"
/>
</view>
<text class="profile-header__title">个人中心</text>
<view class="profile-header__nav-right" :style="navRightStyle"></view>
</view>
</view>
<view class="profile-header__toolbar-spacer" :style="toolbarSpacerStyle"></view>
<!-- 用户信息区深蓝渐变 -->
<view class="profile-header__hero">
<view class="profile-header__inner">
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="$emit('user-info')">
<view class="profile-header__user-inner">
<view class="profile-header__avatar-wrap">
<view class="profile-header__avatar-ring">
<image
class="profile-header__avatar"
:key="userInfo.avatar"
:src="displayAvatar"
mode="aspectFill"
/>
</view>
<view class="profile-header__avatar-badge">
<image
class="profile-header__avatar-badge-icon"
src="/static/images/camera.png"
mode="aspectFit"
/>
</view>
</view>
<view class="profile-header__user-meta">
<view class="profile-header__user-meta-inner">
<text class="profile-header__name">{{ userInfo.name }}</text>
<text class="profile-header__phone">{{ userInfo.phone }}</text>
<view class="profile-header__badge">
<image
class="profile-header__badge-icon"
src="/static/images/crown0.png"
mode="aspectFit"
/>
<text class="profile-header__level">{{ userInfo.memberLevel }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="profile-header__stats">
<view class="profile-header__stats-inner">
<view class="profile-header__stat">
<view class="profile-header__stat-inner">
<text class="profile-header__stat-value">{{ stats.checkInCount }}</text>
<text class="profile-header__stat-label">累计签到</text>
</view>
</view>
<view class="profile-header__stat-divider"></view>
<view class="profile-header__stat">
<view class="profile-header__stat-inner">
<text class="profile-header__stat-value">{{ stats.trainingHours }}</text>
<text class="profile-header__stat-label">训练时长</text>
</view>
</view>
<view class="profile-header__stat-divider"></view>
<view class="profile-header__stat">
<view class="profile-header__stat-inner">
<text class="profile-header__stat-value">{{ stats.pointsBalance }}</text>
<text class="profile-header__stat-label">累计积分</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
userInfo: { type: Object, required: true },
stats: { type: Object, required: true }
},
emits: ['user-info'],
computed: {
displayAvatar() {
return this.userInfo.avatar || '/static/images/AvatarEditWrap.png'
}
},
data() {
return {
toolbarStyle: {},
toolbarSpacerStyle: {},
navRightStyle: {}
}
},
mounted() {
this.syncNavSafeArea()
},
methods: {
syncNavSafeArea() {
try {
const sys = uni.getSystemInfoSync()
const statusBarHeight = sys.statusBarHeight || 0
const navHeight = 44
const menu = uni.getMenuButtonBoundingClientRect?.()
this.toolbarStyle = {
paddingTop: `${statusBarHeight}px`
}
this.toolbarSpacerStyle = {
height: `${statusBarHeight + navHeight}px`
}
if (menu && menu.width) {
const capsuleGap = sys.windowWidth - menu.left + 8
this.navRightStyle = {
width: `${capsuleGap}px`,
minWidth: `${capsuleGap}px`
}
}
} catch (e) {
this.toolbarSpacerStyle = { height: '44px' }
}
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-header.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,31 @@
<template>
<view class="logout-section">
<view
class="logout-section__btn"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap="$emit('logout')"
>
<image
class="logout-section__icon"
src="/static/images/logout.png"
mode="aspectFit"
/>
<text class="logout-section__text">退出登录</text>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
emits: ['logout']
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-logout.css';
</style>
@@ -0,0 +1,135 @@
<template>
<view class="member-card-section">
<view class="member-card-section__inner">
<view class="member-card-section__head">
<view class="member-card-section__head-inner">
<text class="member-card-section__title">
我的会员卡
</text>
<view
class="member-card-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<text
class="member-card-section__link-text"
>
查看全部
</text>
<image class="member-card-section__link-arrow" src="/static/images/chevronright12.png" mode="aspectFit" />
</view>
</view>
</view>
<view
class="member-card-preview"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
>
<view class="member-card-preview__inner">
<view class="member-card-preview__head">
<view class="member-card-preview__head-inner">
<view
class="member-card-preview__type-row"
>
<view
class="member-card-preview__icon-wrap"
>
<view
class="member-card-preview__icon-border"
>
<view
class="member-card-preview__icon-bg"
></view>
<view
class="member-card-preview__icon-stroke"
></view>
</view>
<image class="member-card-preview__icon-line" src="/static/images/Line_2_468.png" mode="aspectFill" />
</view>
<text
class="member-card-preview__name"
>
{{ cardInfo.name }}
</text>
</view>
<view
class="member-card-preview__tag"
>
<text class="member-card-preview__tag-text">
{{ cardInfo.detailTag || '详情' }}
</text>
</view>
</view>
</view>
<text class="member-card-preview__expire">
{{ cardInfo.expireDate }}
</text>
<view class="member-card-preview__footer">
<view class="member-card-preview__footer-inner">
<view
class="member-card-preview__days"
>
<text
class="member-card-preview__days-num"
>
{{ cardInfo.remainingDays }}
</text>
<text
class="member-card-preview__days-unit"
>
天剩余
</text>
</view>
<view
class="member-card-preview__renew"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="$emit('renew')"
>
<text
class="member-card-preview__renew-text"
>
续费
</text>
</view>
</view>
</view>
</view>
</view>
<view class="member-card-tip">
<view class="member-card-tip__inner">
<view class="member-card-tip__content">
<image class="member-card-tip__icon" src="/static/images/clock1.png" mode="aspectFit" />
<text
class="member-card-tip__text"
>
{{ cardInfo.tip }}
</text>
</view>
</view>
<view class="member-card-tip__border"></view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
cardInfo: { type: Object, required: true }
},
emits: ['view-all', 'renew'],
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-member-card.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,116 @@
<template>
<view class="quick-actions">
<view class="quick-actions__inner">
<view class="quick-actions__grid">
<view class="quick-actions__grid-inner">
<view
v-for="item in row1"
:key="item.key"
class="quick-actions__item"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('action', item.key)"
>
<view class="quick-actions__item-inner">
<view class="quick-actions__icon-wrap">
<view class="quick-actions__icon-wrap-inner">
<view v-if="item.key === 'booking'" class="quick-actions__icon">
<image
class="quick-actions__icon-part"
src="/static/images/Vector_2_490.png"
mode="aspectFit"
/>
<image
class="quick-actions__icon-part"
src="/static/images/Vector_2_491.png"
mode="aspectFit"
/>
<view class="quick-actions__border-wrap">
<view class="quick-actions__rect"></view>
<view class="quick-actions__border"></view>
</view>
<image
class="quick-actions__icon-part"
src="/static/images/Vector_2_493.png"
mode="aspectFit"
/>
<image
class="quick-actions__icon-part"
src="/static/images/Vector_2_494.png"
mode="aspectFit"
/>
</view>
<image
v-else
class="quick-actions__icon-img"
:src="item.icon"
mode="aspectFit"
/>
</view>
</view>
<text :class="item.textClass">{{ item.label }}</text>
</view>
</view>
</view>
</view>
<view class="quick-actions__divider"></view>
<view class="quick-actions__grid">
<view class="quick-actions__grid-inner">
<view
v-for="item in row2"
:key="item.key"
class="quick-actions__item"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('action', item.key)"
>
<view class="quick-actions__item-inner">
<view class="quick-actions__icon-wrap">
<view class="quick-actions__icon-wrap-inner">
<image
class="quick-actions__icon-img"
:src="item.icon"
mode="aspectFit"
/>
</view>
</view>
<text :class="item.textClass">{{ item.label }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
emits: ['action'],
data() {
return {
row1: [
{ key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' },
{ key: 'bodyTest', label: '智能体测', textClass: 'quick-actions__title-2', icon: '/static/images/mappin2.png' },
{ key: 'bodyReport', label: '体测报告', textClass: 'quick-actions__title-3', icon: '/static/images/activity.png' },
{ key: 'trainReport', label: '训练报告', textClass: 'quick-actions__coach', icon: '/static/images/usercheck.png' }
],
row2: [
{ key: 'coupon', label: '我的优惠券', textClass: 'quick-actions__text', icon: '/static/images/ticket.png' },
{ key: 'points', label: '我的积分', textClass: 'quick-actions__points-desc', icon: '/static/images/star.png' },
{ key: 'referral', label: '邀请好友', textClass: 'quick-actions__title-4', icon: '/static/images/share2.png' },
{ key: 'course', label: '我的课程', textClass: 'quick-actions__text-2', icon: '/static/images/play.png' }
]
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-quick-actions.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,101 @@
<template>
<view class="referral-section">
<view class="referral-section__inner">
<view class="referral-section__header">
<text class="referral-section__title">推荐奖励</text>
<view
class="referral-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-rules')"
>
<text class="referral-section__records-link">规则说明</text>
<image
class="referral-section__link-arrow"
src="/static/images/chevronright11.png"
mode="aspectFit"
/>
</view>
</view>
<view class="referral-section__code-row">
<view class="referral-section__code-box">
<text class="referral-section__code-label">我的邀请码</text>
<text class="referral-section__code-value">{{ data.code }}</text>
</view>
<view
class="referral-section__copy-btn"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap="copyCode"
>
<view class="referral-section__copy-icon">
<view class="referral-section__copy-sheet referral-section__copy-sheet--back"></view>
<view class="referral-section__copy-sheet referral-section__copy-sheet--front"></view>
</view>
<text class="referral-section__copy-text">复制</text>
</view>
</view>
<view class="referral-section__stats">
<view class="referral-section__stat">
<text class="referral-section__stat-num referral-section__stat-num--orange">
{{ data.invited }}
</text>
<text class="referral-section__stat-label">已推荐</text>
</view>
<view class="referral-section__stat-divider"></view>
<view class="referral-section__stat">
<text class="referral-section__stat-num referral-section__stat-num--green">
{{ data.registered }}
</text>
<text class="referral-section__stat-label">已注册</text>
</view>
<view class="referral-section__stat-divider"></view>
<view class="referral-section__stat">
<text class="referral-section__stat-num referral-section__stat-num--amber">
{{ data.purchased }}
</text>
<text class="referral-section__stat-label">已购课</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
data: {
type: Object,
default: () => ({
code: 'FIT-ZXF-2024',
invited: 5,
registered: 3,
purchased: 2
})
}
},
emits: ['view-rules'],
methods: {
copyCode() {
uni.setClipboardData({
data: this.data.code,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-referral.css';
@import '@/common/style/memberInfo/member-info-tap.css';
</style>
@@ -0,0 +1,109 @@
<template>
<view class="settings-section">
<view class="settings-section__inner">
<text class="settings-section__title">设置与安全</text>
<view class="settings-section__list">
<view class="settings-section__list-inner">
<view
v-for="(item, index) in items"
:key="item.key"
>
<view v-if="index > 0" class="settings-section__item-divider"></view>
<view
class="settings-section__item"
:class="{ 'settings-section__item--tall': item.subtitle }"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="$emit('setting', item.key)"
>
<view class="settings-section__item-inner">
<view
class="settings-section__item-icon-wrap"
:class="item.iconWrapClass"
>
<image
class="settings-section__item-icon"
:src="item.icon"
mode="aspectFit"
/>
</view>
<view
v-if="item.subtitle"
class="settings-section__item-texts"
>
<text class="settings-section__item-title">{{ item.label }}</text>
<text class="settings-section__item-desc">{{ item.subtitle }}</text>
</view>
<text
v-else
class="settings-section__item-label"
:class="item.labelClass"
>
{{ item.label }}
</text>
<image
class="settings-section__item-arrow"
src="/static/images/chevronright10.png"
mode="aspectFit"
/>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
emits: ['setting'],
data() {
return {
items: [
{
key: 'notify',
label: '通知设置',
icon: '/static/images/bell.png',
iconWrapClass: ''
},
{
key: 'password',
label: '修改密码',
icon: '/static/images/Vector_2_727.png',
iconWrapClass: 'settings-section__item-icon-wrap--blue'
},
{
key: 'privacy',
label: '隐私政策',
icon: '/static/images/shield.png',
iconWrapClass: 'settings-section__item-icon-wrap--green'
},
{
key: 'nfc',
label: 'NFC 门禁卡',
subtitle: '已绑定',
icon: '/static/images/ticket.png',
iconWrapClass: ''
},
{
key: 'delete',
label: '注销账户',
icon: '/static/images/userx.png',
iconWrapClass: 'settings-section__item-icon-wrap--red',
labelClass: 'settings-section__item-label--danger'
}
]
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-settings.css';
</style>
@@ -0,0 +1,20 @@
<template>
<view class="status-bar">
<view class="status-bar__inner">
<text class="status-bar__time">{{ statusBarTime }}</text>
<text class="status-bar__icons">...</text>
</view>
</view>
</template>
<script>
export default {
options: {
virtualHost: true,
styleIsolation: 'apply-shared'
},
props: {
statusBarTime: { type: String, default: '9:41' }
},
}
</script>
@@ -0,0 +1,106 @@
<template>
<view class="sub-nav">
<view class="sub-nav__toolbar" :style="toolbarStyle">
<view class="sub-nav__nav">
<view class="sub-nav__back" @tap.stop="$emit('back')">
<image
class="sub-nav__back-icon"
src="/static/images/chevronleft.png"
mode="aspectFit"
/>
</view>
<text class="sub-nav__title">{{ title }}</text>
<view class="sub-nav__right">
<view
v-if="rightText"
class="sub-nav__action"
:class="{ 'sub-nav__action--button': actionButton }"
@tap.stop="$emit('right-action')"
>
<text class="sub-nav__action-text">{{ rightText }}</text>
</view>
<view class="sub-nav__capsule" :class="{ 'sub-nav__capsule--h5': isH5 }" :style="capsuleStyle"></view>
</view>
</view>
</view>
<view class="sub-nav__spacer" :style="toolbarSpacerStyle"></view>
</view>
</template>
<script>
export default {
options: {
virtualHost: false,
styleIsolation: 'apply-shared'
},
props: {
title: { type: String, required: true },
rightText: { type: String, default: '' },
actionButton: { type: Boolean, default: false }
},
emits: ['back', 'right-action'],
data() {
return {
toolbarStyle: {},
toolbarSpacerStyle: {},
capsuleStyle: {},
isH5: false
}
},
mounted() {
this.syncNavSafeArea()
this.$nextTick(() => {
setTimeout(() => this.syncNavSafeArea(), 50)
})
},
methods: {
syncNavSafeArea() {
try {
const sys = uni.getSystemInfoSync()
const statusBarHeight = sys.statusBarHeight || 0
const navHeight = 44
const extraGap = 4
const menu = uni.getMenuButtonBoundingClientRect?.()
// #ifdef H5
this.isH5 = true
// #endif
// #ifndef H5
this.isH5 = false
// #endif
if (!this.isH5 && typeof window !== 'undefined' && !menu?.width) {
this.isH5 = sys.uniPlatform === 'web' || sys.platform === 'web'
}
this.toolbarStyle = {
paddingTop: `${statusBarHeight}px`
}
this.toolbarSpacerStyle = {
height: `${statusBarHeight + navHeight + extraGap}px`
}
if (!this.isH5 && menu && menu.width) {
const capsuleGap = sys.windowWidth - menu.left + 8
this.capsuleStyle = {
width: `${capsuleGap}px`,
minWidth: `${capsuleGap}px`
}
} else {
this.capsuleStyle = {
width: '0px',
minWidth: '0px'
}
}
} catch (e) {
this.toolbarSpacerStyle = { height: '44px' }
this.isH5 = true
this.capsuleStyle = { width: '0px', minWidth: '0px' }
}
}
}
}
</script>
<style>
@import '@/common/style/memberInfo/member-info-sub-nav.css';
</style>