会员个人中心页面初步完成
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">课程</text>
|
||||
<text class="tab-page__subtitle">精品团课 · 私教 · 线上课</text>
|
||||
</view>
|
||||
|
||||
<RecommendCourses />
|
||||
|
||||
<view class="tab-page__actions">
|
||||
<view class="tab-page__btn" hover-class="tab-page__btn--hover" @tap="goCourseList">
|
||||
<text class="tab-page__btn-text">预约课程</text>
|
||||
</view>
|
||||
<view class="tab-page__btn tab-page__btn--ghost" hover-class="tab-page__btn--hover" @tap="goMyCourses">
|
||||
<text class="tab-page__btn-text tab-page__btn-text--ghost">我的课程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
<TabBar :active="1" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
function goCourseList() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
|
||||
function goMyCourses() {
|
||||
navigateToPage(PAGE.MY_COURSES)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tab-page__actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.tab-page__btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8c5a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-page__btn--ghost {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tab-page__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-page__btn-text--ghost {
|
||||
color: #1a4a6f;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">发现</text>
|
||||
<text class="tab-page__subtitle">活动 · 资讯 · 今日推荐</text>
|
||||
</view>
|
||||
|
||||
<TodayRecommend />
|
||||
|
||||
<view class="discover-links">
|
||||
<view class="discover-link" hover-class="discover-link--hover" @tap="goReferral">
|
||||
<text class="discover-link__title">邀请好友</text>
|
||||
<text class="discover-link__desc">邀请注册/购课,双方得积分</text>
|
||||
</view>
|
||||
<view class="discover-link" hover-class="discover-link--hover" @tap="goCouponCenter">
|
||||
<text class="discover-link__title">领券中心</text>
|
||||
<text class="discover-link__desc">限时优惠券,先到先得</text>
|
||||
</view>
|
||||
<view class="discover-link" hover-class="discover-link--hover" @tap="goPointsMall">
|
||||
<text class="discover-link__title">积分商城</text>
|
||||
<text class="discover-link__desc">积分兑换好礼</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
<TabBar :active="3" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TodayRecommend from '@/components/index/TodayRecommend.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
function goReferral() {
|
||||
navigateToPage(PAGE.REFERRAL)
|
||||
}
|
||||
|
||||
function goCouponCenter() {
|
||||
navigateToPage(PAGE.COUPON_CENTER)
|
||||
}
|
||||
|
||||
function goPointsMall() {
|
||||
navigateToPage(PAGE.POINTS_MALL)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.discover-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.discover-link {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.discover-link__title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.discover-link__desc {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
<view class="bottom-placeholder"></view>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<TabBar />
|
||||
<TabBar :active="0" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="历史对比" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">选择对比记录</text>
|
||||
<view class="bt-compare-header">
|
||||
<view
|
||||
class="bt-compare-picker"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="pickRecord('a')"
|
||||
>
|
||||
<text class="bt-compare-picker__label">记录 A(较新)</text>
|
||||
<text class="bt-compare-picker__date">{{ recordA?.date || '点击选择' }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-compare-picker"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="pickRecord('b')"
|
||||
>
|
||||
<text class="bt-compare-picker__label">记录 B(较旧)</text>
|
||||
<text class="bt-compare-picker__date">{{ recordB?.date || '点击选择' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="compareData" class="bt-card">
|
||||
<text class="bt-card__title">指标对比</text>
|
||||
<view class="bt-compare-row">
|
||||
<text class="bt-compare-row__label">指标</text>
|
||||
<text class="bt-compare-row__val">A</text>
|
||||
<text class="bt-compare-row__val">B</text>
|
||||
<text class="bt-compare-row__diff">差值</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="row in compareData.metrics"
|
||||
:key="row.key"
|
||||
class="bt-compare-row"
|
||||
>
|
||||
<text class="bt-compare-row__label">{{ row.label }}</text>
|
||||
<text class="bt-compare-row__val">{{ row.valueA }}</text>
|
||||
<text class="bt-compare-row__val">{{ row.valueB }}</text>
|
||||
<text
|
||||
class="bt-compare-row__diff"
|
||||
:style="{ color: diffColor(row) }"
|
||||
>
|
||||
{{ formatDiff(row.diff, row.key) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="compareData" class="bt-card">
|
||||
<text class="bt-card__title">评分变化</text>
|
||||
<view class="bt-metrics">
|
||||
<view class="bt-metric">
|
||||
<text class="bt-metric__value">{{ compareData.recordA.score }}</text>
|
||||
<text class="bt-metric__label">记录 A 评分</text>
|
||||
</view>
|
||||
<view class="bt-metric">
|
||||
<text class="bt-metric__value">{{ compareData.recordB.score }}</text>
|
||||
<text class="bt-metric__label">记录 B 评分</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="bt-card__desc" style="margin-top: 12px;">
|
||||
综合评分变化 {{ scoreDiff > 0 ? '+' : '' }}{{ scoreDiff }} 分
|
||||
{{ scoreDiff > 0 ? ',整体趋势向好' : scoreDiff < 0 ? ',建议加强训练' : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getBodyTestHistory, getCompareData } from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
records: [],
|
||||
recordA: null,
|
||||
recordB: null,
|
||||
compareData: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scoreDiff() {
|
||||
if (!this.compareData) return 0
|
||||
return this.compareData.recordA.score - this.compareData.recordB.score
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.records = getBodyTestHistory(store)
|
||||
if (this.records.length >= 2 && !this.recordA) {
|
||||
this.recordA = this.records[0]
|
||||
this.recordB = this.records[1]
|
||||
this.refreshCompare()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HISTORY)
|
||||
},
|
||||
pickRecord(which) {
|
||||
const labels = this.records.map(
|
||||
(r) => `${r.date} · ${r.score}分 · ${r.gradeLabel}`
|
||||
)
|
||||
uni.showActionSheet({
|
||||
itemList: labels,
|
||||
success: (res) => {
|
||||
const picked = this.records[res.tapIndex]
|
||||
if (which === 'a') this.recordA = picked
|
||||
else this.recordB = picked
|
||||
this.refreshCompare()
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshCompare() {
|
||||
if (!this.recordA || !this.recordB) {
|
||||
this.compareData = null
|
||||
return
|
||||
}
|
||||
if (this.recordA.id === this.recordB.id) {
|
||||
uni.showToast({ title: '请选择两条不同记录', icon: 'none' })
|
||||
this.compareData = null
|
||||
return
|
||||
}
|
||||
const store = loadMemberStore()
|
||||
this.compareData = getCompareData(store, this.recordA.id, this.recordB.id)
|
||||
},
|
||||
formatDiff(diff, key) {
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
const units = { bodyFat: '%', weight: '', bmi: '', muscleMass: '', visceralFat: '', bmr: '' }
|
||||
return `${sign}${diff}${units[key] || ''}`
|
||||
},
|
||||
diffColor(row) {
|
||||
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(row.key)
|
||||
const good = lowerBetter ? row.diff < 0 : row.diff > 0
|
||||
if (row.diff === 0) return '#8A99B4'
|
||||
return good ? '#2ECC71' : '#F39C12'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="连接设备" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ device.name }}</text>
|
||||
<text class="bt-card__desc">型号 {{ device.model }} · 请按以下步骤完成蓝牙配对</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">连接引导</text>
|
||||
<view class="bt-steps">
|
||||
<view
|
||||
v-for="step in steps"
|
||||
:key="step.step"
|
||||
class="bt-step"
|
||||
>
|
||||
<view class="bt-step__num">{{ step.step }}</view>
|
||||
<view class="bt-step__content">
|
||||
<text class="bt-step__title">{{ step.title }}</text>
|
||||
<text class="bt-step__desc">{{ step.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="searching" class="bt-card">
|
||||
<view class="bt-measure">
|
||||
<text class="bt-measure__hint">正在搜索附近设备…</text>
|
||||
<text class="bt-measure__hint">{{ searchHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="connected" class="bt-card">
|
||||
<view class="bt-device">
|
||||
<view class="bt-device__icon-wrap">
|
||||
<image class="bt-device__icon" src="/static/images/shield.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="bt-device__info">
|
||||
<text class="bt-device__name">连接成功</text>
|
||||
<text class="bt-device__status bt-device__status--on">
|
||||
{{ device.name }} · 电量 {{ device.battery }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="bt-device__dot bt-device__dot--on"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-footer-actions">
|
||||
<view
|
||||
v-if="!connected"
|
||||
class="bt-btn bt-btn--primary"
|
||||
:class="{ 'bt-btn--outline': searching }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="searchDevice"
|
||||
>
|
||||
<text class="bt-btn__text">{{ searching ? '搜索中…' : '搜索并连接' }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="bt-btn bt-btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goMeasuring"
|
||||
>
|
||||
<text class="bt-btn__text">开始测量</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
persistMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
connectBodyTestDevice,
|
||||
bodyTestMock
|
||||
} from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
device: {},
|
||||
steps: bodyTestMock.connectSteps,
|
||||
searching: false,
|
||||
connected: false,
|
||||
searchHint: '请保持手机蓝牙已开启'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.device = { ...store.bodyTest.device }
|
||||
this.connected = store.bodyTest.device.connected
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
searchDevice() {
|
||||
if (this.searching) return
|
||||
this.searching = true
|
||||
this.searchHint = '发现 InBody 270…'
|
||||
setTimeout(() => {
|
||||
const store = loadMemberStore()
|
||||
connectBodyTestDevice(store)
|
||||
persistMemberStore(store)
|
||||
this.device = { ...store.bodyTest.device }
|
||||
this.connected = true
|
||||
this.searching = false
|
||||
uni.showToast({ title: '设备已连接', icon: 'success' })
|
||||
}, 1800)
|
||||
},
|
||||
goMeasuring() {
|
||||
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.bt-footer-actions .bt-btn--primary.bt-btn--outline {
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bt-footer-actions .bt-btn--primary.bt-btn--outline .bt-btn__text {
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="体测报告" @back="onBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="y in years"
|
||||
:key="y"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeYear === y }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="activeYear = y"
|
||||
>
|
||||
<text class="bt-tab__text">{{ y === 'all' ? '全部' : y + '年' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCompare"
|
||||
>
|
||||
历史对比
|
||||
</text>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="(item, index) in records"
|
||||
:key="item.id"
|
||||
class="mi-timeline-card"
|
||||
hover-class="mi-tap-row--hover"
|
||||
@tap="viewReport(item)"
|
||||
>
|
||||
<view class="mi-timeline-card__line">
|
||||
<view class="mi-timeline-card__dot"></view>
|
||||
<view v-if="index < records.length - 1" class="mi-timeline-card__bar"></view>
|
||||
</view>
|
||||
<view class="mi-timeline-card__content">
|
||||
<view class="mi-timeline-card__head">
|
||||
<text class="mi-timeline-card__date">{{ item.date }} {{ item.time }}</text>
|
||||
<text class="mi-timeline-card__score">{{ item.score }}分</text>
|
||||
</view>
|
||||
<text class="mi-timeline-card__grade">{{ item.grade }} {{ item.gradeLabel }} · {{ item.status }}</text>
|
||||
<text class="mi-timeline-card__metrics">
|
||||
体脂 {{ item.metrics.bodyFat }}% · 肌肉 {{ item.metrics.muscleMass }}kg · BMI {{ item.metrics.bmi }}
|
||||
</text>
|
||||
<view v-if="item.changeBadge" class="mi-timeline-card__badge" :class="item.changeBadge.good ? 'mi-timeline-card__badge--good' : 'mi-timeline-card__badge--warn'">
|
||||
<text>{{ item.changeBadge.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!records.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无体测报告</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getBodyTestHistory, getBodyTestYears, getBodyTestChangeBadge } from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { activeYear: 'all', years: [], allRecords: [] }
|
||||
},
|
||||
computed: {
|
||||
records() {
|
||||
const list = this.activeYear === 'all'
|
||||
? this.allRecords
|
||||
: this.allRecords.filter((r) => r.date.startsWith(this.activeYear))
|
||||
return list.map((item, index) => {
|
||||
const previous = list[index + 1]
|
||||
return {
|
||||
...item,
|
||||
changeBadge: getBodyTestChangeBadge(item, previous)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeYear() { this.loadList() }
|
||||
},
|
||||
onShow() { this.loadList() },
|
||||
methods: {
|
||||
loadList() {
|
||||
const store = loadMemberStore()
|
||||
this.years = getBodyTestYears(store)
|
||||
this.allRecords = getBodyTestHistory(store)
|
||||
},
|
||||
onBack() { goBackOrTab(PAGE.MEMBER) },
|
||||
viewReport(item) {
|
||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${item.id}`)
|
||||
},
|
||||
goCompare() {
|
||||
navigateToPage(PAGE.BODY_TEST_COMPARE)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.mi-timeline-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mi-timeline-card__line {
|
||||
width: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mi-timeline-card__dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-orange, #FF6B35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-timeline-card__bar {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: var(--border-light, #E9EDF2);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.mi-timeline-card__content {
|
||||
flex: 1;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 14px;
|
||||
background: var(--bg-white, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.mi-timeline-card__head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mi-timeline-card__date {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.mi-timeline-card__score {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
}
|
||||
|
||||
.mi-timeline-card__grade {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-timeline-card__metrics {
|
||||
font-size: 11px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-timeline-card__badge {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.mi-timeline-card__badge--good {
|
||||
background: rgba(46, 204, 113, 0.12);
|
||||
}
|
||||
|
||||
.mi-timeline-card__badge--good text {
|
||||
font-size: 10px;
|
||||
color: var(--success-green, #2ECC71);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mi-timeline-card__badge--warn text {
|
||||
font-size: 10px;
|
||||
color: var(--warning-amber, #F39C12);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="智能体测" @back="goBack" />
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goSettings"
|
||||
>
|
||||
体测设置
|
||||
</text>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-hero">
|
||||
<view class="bt-hero__top">
|
||||
<text class="bt-hero__label">最新体测评分</text>
|
||||
<view class="bt-hero__badge">
|
||||
<text class="bt-hero__badge-text">{{ latest?.status || '暂无数据' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-hero__score-row">
|
||||
<text class="bt-hero__score">{{ latest?.score ?? '--' }}</text>
|
||||
<text class="bt-hero__grade">{{ latest?.grade ?? '' }} {{ latest?.gradeLabel ?? '' }}</text>
|
||||
</view>
|
||||
<text class="bt-hero__meta">
|
||||
{{ latest ? `最近测量 · ${latest.date} ${latest.time}` : '完成首次体测,获取健康画像' }}
|
||||
</text>
|
||||
<view class="bt-hero__actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="startMeasure"
|
||||
>
|
||||
<image class="bt-btn__icon" src="/static/images/activity.png" mode="aspectFit" />
|
||||
<text class="bt-btn__text">开始体测</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="latest"
|
||||
class="bt-btn bt-btn--ghost"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="viewLatestReport"
|
||||
>
|
||||
<text class="bt-btn__text">查看报告</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">设备状态</text>
|
||||
<view class="bt-device">
|
||||
<view class="bt-device__icon-wrap">
|
||||
<image class="bt-device__icon" src="/static/images/mappin2.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="bt-device__info">
|
||||
<text class="bt-device__name">{{ device.name }}</text>
|
||||
<text
|
||||
class="bt-device__status"
|
||||
:class="{ 'bt-device__status--on': device.connected }"
|
||||
>
|
||||
{{ deviceStatusText }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-device__dot"
|
||||
:class="{ 'bt-device__dot--on': device.connected }"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">快捷入口</text>
|
||||
<view class="bt-grid">
|
||||
<view
|
||||
v-for="item in quickLinks"
|
||||
:key="item.key"
|
||||
class="bt-grid__item"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="onQuickLink(item.key)"
|
||||
>
|
||||
<image class="bt-grid__icon" :src="item.icon" mode="aspectFit" />
|
||||
<text class="bt-grid__label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="latest" class="bt-card">
|
||||
<text class="bt-card__title">核心指标概览</text>
|
||||
<view class="bt-metrics">
|
||||
<view
|
||||
v-for="m in previewMetrics"
|
||||
:key="m.key"
|
||||
class="bt-metric"
|
||||
>
|
||||
<text class="bt-metric__value">{{ m.value }}</text>
|
||||
<text class="bt-metric__label">{{ m.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getLatestBodyTestRecord } from '@/common/memberInfo/bodyTestStore.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
latest: null,
|
||||
device: {},
|
||||
quickLinks: [
|
||||
{ key: 'history', label: '历史记录', icon: '/static/images/clock.png' },
|
||||
{ key: 'compare', label: '历史对比', icon: '/static/images/trendingdown.png' },
|
||||
{ key: 'trend', label: '趋势分析', icon: '/static/images/activity.png' },
|
||||
{ key: 'report', label: '体测报告', icon: '/static/images/filetext.png' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
deviceStatusText() {
|
||||
if (this.device.connected) {
|
||||
return `已连接 · 电量 ${this.device.battery}%`
|
||||
}
|
||||
return '未连接 · 点击开始体测进行配对'
|
||||
},
|
||||
previewMetrics() {
|
||||
if (!this.latest?.metrics) return []
|
||||
const m = this.latest.metrics
|
||||
return [
|
||||
{ key: 'weight', label: '体重(kg)', value: m.weight },
|
||||
{ key: 'bmi', label: 'BMI', value: m.bmi },
|
||||
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat },
|
||||
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass }
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.latest = getLatestBodyTestRecord(store)
|
||||
this.device = { ...store.bodyTest.device }
|
||||
},
|
||||
startMeasure() {
|
||||
const store = loadMemberStore()
|
||||
if (store.bodyTest.device.connected) {
|
||||
navigateToPage(PAGE.BODY_TEST_MEASURING)
|
||||
} else {
|
||||
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||
}
|
||||
},
|
||||
viewLatestReport() {
|
||||
if (!this.latest) return
|
||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`)
|
||||
},
|
||||
goSettings() {
|
||||
navigateToPage(PAGE.BODY_TEST_SETTINGS)
|
||||
},
|
||||
onQuickLink(key) {
|
||||
const routes = {
|
||||
history: PAGE.BODY_TEST_HISTORY,
|
||||
compare: PAGE.BODY_TEST_COMPARE,
|
||||
trend: PAGE.BODY_TEST_TREND,
|
||||
report: this.latest
|
||||
? `${PAGE.BODY_TEST_REPORT}?id=${this.latest.id}`
|
||||
: PAGE.BODY_TEST_HISTORY
|
||||
}
|
||||
navigateToPage(routes[key] || PAGE.BODY_TEST_HOME)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="测量中" @back="onCancel" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<view class="bt-measure">
|
||||
<view class="bt-measure__ring-wrap">
|
||||
<view class="bt-measure__ring-bg"></view>
|
||||
<view
|
||||
class="bt-measure__ring-fill"
|
||||
:style="{ transform: `rotate(${ringRotation}deg)` }"
|
||||
></view>
|
||||
<view class="bt-measure__center">
|
||||
<text class="bt-measure__percent">{{ progress }}%</text>
|
||||
<text class="bt-measure__hint">{{ phaseHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="bt-card__desc">{{ statusText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">实时数据</text>
|
||||
<view class="bt-measure__live">
|
||||
<view
|
||||
v-for="item in liveDisplay"
|
||||
:key="item.key"
|
||||
class="bt-measure__live-item"
|
||||
>
|
||||
<text class="bt-measure__live-value">{{ item.value }}</text>
|
||||
<text class="bt-measure__live-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
persistMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
interpolateMeasuringMetrics,
|
||||
saveSimulatedBodyTestRecord
|
||||
} from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
const PHASES = [
|
||||
{ until: 20, hint: '校准中', text: '请保持站立姿势,双手自然下垂' },
|
||||
{ until: 50, hint: '阻抗测量', text: '请勿移动,正在进行生物电阻抗分析' },
|
||||
{ until: 80, hint: '数据分析', text: '正在计算体脂与肌肉分布' },
|
||||
{ until: 100, hint: '即将完成', text: '生成健康报告中…' }
|
||||
]
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
progress: 0,
|
||||
liveMetrics: {},
|
||||
timer: null,
|
||||
finished: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ringRotation() {
|
||||
return -90 + (this.progress / 100) * 360
|
||||
},
|
||||
phaseHint() {
|
||||
const phase = PHASES.find((p) => this.progress <= p.until)
|
||||
return phase?.hint || '完成'
|
||||
},
|
||||
statusText() {
|
||||
const phase = PHASES.find((p) => this.progress <= p.until)
|
||||
return phase?.text || '测量完成'
|
||||
},
|
||||
liveDisplay() {
|
||||
const m = this.liveMetrics
|
||||
return [
|
||||
{ key: 'weight', label: '体重(kg)', value: m.weight ?? '--' },
|
||||
{ key: 'bodyFat', label: '体脂率(%)', value: m.bodyFat ?? '--' },
|
||||
{ key: 'muscleMass', label: '肌肉量(kg)', value: m.muscleMass ?? '--' },
|
||||
{ key: 'bmr', label: '基础代谢', value: m.bmr ?? '--' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const store = loadMemberStore()
|
||||
if (!store.bodyTest.device.connected) {
|
||||
uni.showToast({ title: '请先连接设备', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
navigateToPage(PAGE.BODY_TEST_CONNECT)
|
||||
}, 800)
|
||||
return
|
||||
}
|
||||
this.startMeasurement()
|
||||
},
|
||||
onUnload() {
|
||||
this.clearTimer()
|
||||
},
|
||||
methods: {
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
},
|
||||
startMeasurement() {
|
||||
const store = loadMemberStore()
|
||||
this.liveMetrics = interpolateMeasuringMetrics(0, store.profile)
|
||||
this.timer = setInterval(() => {
|
||||
if (this.progress >= 100) {
|
||||
this.completeMeasurement()
|
||||
return
|
||||
}
|
||||
this.progress = Math.min(100, this.progress + 2)
|
||||
const s = loadMemberStore()
|
||||
this.liveMetrics = interpolateMeasuringMetrics(this.progress, s.profile)
|
||||
}, 120)
|
||||
},
|
||||
completeMeasurement() {
|
||||
if (this.finished) return
|
||||
this.finished = true
|
||||
this.clearTimer()
|
||||
const store = loadMemberStore()
|
||||
const record = saveSimulatedBodyTestRecord(store, {
|
||||
...this.liveMetrics,
|
||||
visceralFat: 6,
|
||||
boneMass: 2.42,
|
||||
bodyWater: this.liveMetrics.bodyWater || 52.8,
|
||||
protein: 16.4
|
||||
})
|
||||
persistMemberStore(store)
|
||||
uni.showToast({ title: '测量完成', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navigateToPage(`${PAGE.BODY_TEST_REPORT}?id=${record.id}&new=1`)
|
||||
}, 600)
|
||||
},
|
||||
onCancel() {
|
||||
if (this.finished) return
|
||||
uni.showModal({
|
||||
title: '取消测量',
|
||||
content: '确定要中断当前体测吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.clearTimer()
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="体测报告" @back="onBack" />
|
||||
<view v-if="record" class="bt-page__body">
|
||||
<view class="bt-score-card">
|
||||
<view class="bt-score-card__circle">
|
||||
<text class="bt-score-card__num">{{ record.score }}</text>
|
||||
<text class="bt-score-card__grade">{{ record.grade }}</text>
|
||||
</view>
|
||||
<view class="bt-score-card__info">
|
||||
<text class="bt-score-card__title">{{ record.gradeLabel }} · {{ record.status }}</text>
|
||||
<text class="bt-score-card__date">{{ record.date }} {{ record.time }}</text>
|
||||
<text v-if="record.bodyAge" class="bt-score-card__date">
|
||||
身体年龄 {{ record.bodyAge }} 岁 · 实际 {{ record.realAge }} 岁
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">核心指标</text>
|
||||
<view class="bt-metrics">
|
||||
<view
|
||||
v-for="m in metricCards"
|
||||
:key="m.key"
|
||||
class="bt-metric"
|
||||
>
|
||||
<text class="bt-metric__value">{{ m.display }}</text>
|
||||
<text class="bt-metric__label">{{ m.label }}</text>
|
||||
<text
|
||||
v-if="m.changeText"
|
||||
class="bt-metric__change"
|
||||
:class="m.changeClass"
|
||||
>
|
||||
较上次 {{ m.changeText }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">体成分雷达图</text>
|
||||
<BodyTestRadarChart
|
||||
:labels="radarLabels"
|
||||
:values="radarValues"
|
||||
:width="chartWidth"
|
||||
:height="220"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">人体成分分布</text>
|
||||
<view class="bt-body-map">
|
||||
<view class="bt-body-map__figure">
|
||||
<view class="bt-body-map__head"></view>
|
||||
<view class="bt-body-map__limbs">
|
||||
<view class="bt-body-map__arm"></view>
|
||||
<view class="bt-body-map__arm"></view>
|
||||
</view>
|
||||
<view class="bt-body-map__torso"></view>
|
||||
<view class="bt-body-map__legs">
|
||||
<view class="bt-body-map__leg"></view>
|
||||
<view class="bt-body-map__leg"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-body-map__segments">
|
||||
<view
|
||||
v-for="seg in record.bodySegments"
|
||||
:key="seg.part"
|
||||
class="bt-body-map__seg"
|
||||
:class="'bt-body-map__seg--' + seg.level"
|
||||
>
|
||||
<text class="bt-body-map__seg-name">{{ seg.part }}</text>
|
||||
<text class="bt-body-map__seg-val">{{ seg.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">指标变化趋势</text>
|
||||
<BodyTestTrendChart
|
||||
:points="trendPreview"
|
||||
unit="kg"
|
||||
:width="chartWidth"
|
||||
:height="140"
|
||||
/>
|
||||
<view
|
||||
class="bt-trend-link"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goTrend"
|
||||
>
|
||||
<text class="bt-trend-link__text">查看完整趋势分析</text>
|
||||
<image
|
||||
class="bt-trend-link__arrow"
|
||||
src="/static/images/chevronright3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">健康建议</text>
|
||||
<view class="bt-advice-list">
|
||||
<view
|
||||
v-for="(tip, idx) in record.advice"
|
||||
:key="idx"
|
||||
class="bt-advice-item"
|
||||
>
|
||||
<view class="bt-advice-item__dot"></view>
|
||||
<text class="bt-advice-item__text">{{ tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="courses.length" class="bt-card">
|
||||
<text class="bt-card__title">推荐课程</text>
|
||||
<view
|
||||
v-for="course in courses"
|
||||
:key="course.id"
|
||||
class="bt-course"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="onCourseTap(course)"
|
||||
>
|
||||
<image class="bt-course__banner" :src="course.banner" mode="aspectFill" />
|
||||
<view class="bt-course__info">
|
||||
<text class="bt-course__tag">{{ course.tag }}</text>
|
||||
<text class="bt-course__title">{{ course.title }}</text>
|
||||
<text class="bt-course__meta">{{ course.coach }} · {{ course.schedule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-footer-actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--outline"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="exportReport"
|
||||
>
|
||||
<image class="bt-btn__icon" src="/static/images/filetext.png" mode="aspectFit" />
|
||||
<text class="bt-btn__text">导出 PDF</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--outline"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="shareReport"
|
||||
>
|
||||
<image class="bt-btn__icon" src="/static/images/share2.png" mode="aspectFit" />
|
||||
<text class="bt-btn__text">分享</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="retest"
|
||||
>
|
||||
<text class="bt-btn__text">再次体测</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="bt-empty">
|
||||
<text class="bt-empty__text">未找到体测报告</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import BodyTestRadarChart from '@/components/memberInfo/BodyTestRadarChart.vue'
|
||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
getBodyTestRecordById,
|
||||
getBodyTestTrendData,
|
||||
getRecommendedCourses,
|
||||
computeChanges,
|
||||
formatChangeValue,
|
||||
bodyTestMock
|
||||
} from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav, BodyTestRadarChart, BodyTestTrendChart },
|
||||
data() {
|
||||
return {
|
||||
recordId: null,
|
||||
record: null,
|
||||
previous: null,
|
||||
chartWidth: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
radarLabels() {
|
||||
return bodyTestMock.radarLabels.map((l) => l.label)
|
||||
},
|
||||
radarValues() {
|
||||
if (!this.record?.radar) return []
|
||||
const keys = bodyTestMock.radarLabels.map((l) => l.key)
|
||||
return keys.map((k) => this.record.radar[k] || 0)
|
||||
},
|
||||
metricCards() {
|
||||
if (!this.record?.metrics) return []
|
||||
const changes = this.changes
|
||||
const defs = bodyTestMock.metricDefs.slice(0, 8)
|
||||
return defs.map((def) => {
|
||||
const val = this.record.metrics[def.key]
|
||||
const diff = changes[def.key]
|
||||
let changeText = ''
|
||||
let changeClass = ''
|
||||
if (diff !== undefined && diff !== null) {
|
||||
changeText = formatChangeValue(def.key, diff)
|
||||
const lowerBetter = ['weight', 'bodyFat', 'visceralFat'].includes(def.key)
|
||||
const isGood = lowerBetter ? diff < 0 : diff > 0
|
||||
changeClass = isGood ? 'bt-metric__change--down' : 'bt-metric__change--up'
|
||||
}
|
||||
const display = def.unit ? `${val}${def.unit === '%' ? '%' : def.unit === 'kg' ? '' : ` ${def.unit}`}` : val
|
||||
return {
|
||||
key: def.key,
|
||||
label: def.label + (def.unit && def.unit !== '%' && def.unit !== 'kg' ? `(${def.unit})` : def.unit === 'kg' ? '(kg)' : def.unit === '%' ? '(%)' : ''),
|
||||
display: def.key === 'bodyFat' ? `${val}%` : def.key === 'bodyWater' ? `${val}%` : def.unit === 'kg' ? val : def.unit ? `${val}` : val,
|
||||
changeText,
|
||||
changeClass
|
||||
}
|
||||
})
|
||||
},
|
||||
changes() {
|
||||
if (this.record?.changes) return this.record.changes
|
||||
if (this.previous) return computeChanges(this.record, this.previous)
|
||||
return {}
|
||||
},
|
||||
trendPreview() {
|
||||
const store = loadMemberStore()
|
||||
return getBodyTestTrendData(store, 'weight', 4)
|
||||
},
|
||||
courses() {
|
||||
return getRecommendedCourses(this.record)
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.recordId = options?.id ? Number(options.id) : null
|
||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||
this.loadRecord()
|
||||
},
|
||||
methods: {
|
||||
loadRecord() {
|
||||
const store = loadMemberStore()
|
||||
const records = store.bodyTest.records
|
||||
if (this.recordId) {
|
||||
this.record = getBodyTestRecordById(store, this.recordId)
|
||||
} else {
|
||||
this.record = records.length ? { ...records[0] } : null
|
||||
}
|
||||
if (this.record) {
|
||||
const idx = records.findIndex((r) => r.id === this.record.id)
|
||||
this.previous = idx >= 0 && records[idx + 1] ? records[idx + 1] : null
|
||||
}
|
||||
},
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
goTrend() {
|
||||
navigateToPage(`${PAGE.BODY_TEST_TREND}?metric=weight`)
|
||||
},
|
||||
exportReport() {
|
||||
uni.showLoading({ title: '生成中…' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '报告已保存到相册', icon: 'success' })
|
||||
}, 1200)
|
||||
},
|
||||
shareReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享给微信好友', '生成分享海报', '复制报告链接'],
|
||||
success: (res) => {
|
||||
const msgs = ['已唤起微信分享', '海报已生成', '链接已复制']
|
||||
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
onCourseTap(course) {
|
||||
uni.showModal({
|
||||
title: course.title,
|
||||
content: `${course.coach}\n${course.schedule}\n\n是否前往预约?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
retest() {
|
||||
navigateToPage(PAGE.BODY_TEST_HOME)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.bt-footer-actions .bt-btn {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="体测设置" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">连接与同步</text>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">蓝牙自动连接</text>
|
||||
<text class="bt-setting__desc">进入体测页时自动搜索已配对设备</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.bluetoothEnabled"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('bluetoothEnabled', $event)"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">测量完成自动同步</text>
|
||||
<text class="bt-setting__desc">结果自动保存至云端与本地</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.autoSync"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('autoSync', $event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">通知与隐私</text>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">测量完成通知</text>
|
||||
<text class="bt-setting__desc">体测结束后推送报告摘要</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.notifyOnComplete"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('notifyOnComplete', $event)"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">分享时匿名化</text>
|
||||
<text class="bt-setting__desc">隐藏姓名与手机号</text>
|
||||
</view>
|
||||
<switch
|
||||
:checked="settings.shareAnonymous"
|
||||
color="#FF6B35"
|
||||
@change="onSwitch('shareAnonymous', $event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">单位制</text>
|
||||
<view class="bt-setting">
|
||||
<view>
|
||||
<text class="bt-setting__label">度量单位</text>
|
||||
<text class="bt-setting__desc">{{ unitLabel }}</text>
|
||||
</view>
|
||||
<view
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleUnit"
|
||||
>
|
||||
<text style="font-size: 14px; color: #1A4A6F; font-weight: 600;">切换</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">设备管理</text>
|
||||
<view class="bt-device">
|
||||
<view class="bt-device__icon-wrap">
|
||||
<image class="bt-device__icon" src="/static/images/mappin2.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="bt-device__info">
|
||||
<text class="bt-device__name">{{ device.name }}</text>
|
||||
<text class="bt-device__status">
|
||||
{{ device.connected ? '已连接' : '未连接' }}
|
||||
· 上次 {{ device.lastConnected || '--' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--outline"
|
||||
style="margin-top: 12px;"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="disconnect"
|
||||
>
|
||||
<text class="bt-btn__text">解除设备配对</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
persistMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
updateBodyTestSettings,
|
||||
disconnectBodyTestDevice
|
||||
} from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
device: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unitLabel() {
|
||||
return this.settings.unitSystem === 'imperial' ? '英制 (lb / in)' : '公制 (kg / cm)'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
this.device = { ...store.bodyTest.device }
|
||||
},
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
onSwitch(key, e) {
|
||||
const store = loadMemberStore()
|
||||
updateBodyTestSettings(store, { [key]: e.detail.value })
|
||||
persistMemberStore(store)
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
},
|
||||
toggleUnit() {
|
||||
const store = loadMemberStore()
|
||||
const next = this.settings.unitSystem === 'metric' ? 'imperial' : 'metric'
|
||||
updateBodyTestSettings(store, { unitSystem: next })
|
||||
persistMemberStore(store)
|
||||
this.settings = { ...store.bodyTest.settings }
|
||||
uni.showToast({ title: `已切换为${this.unitLabel}`, icon: 'none' })
|
||||
},
|
||||
disconnect() {
|
||||
uni.showModal({
|
||||
title: '解除配对',
|
||||
content: '解除后下次体测需重新连接设备,确定继续?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
disconnectBodyTestDevice(store)
|
||||
persistMemberStore(store)
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '已解除配对', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="趋势分析" @back="onBack" />
|
||||
<view class="bt-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeMetric === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="switchMetric(tab.key)"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ activeLabel }}变化趋势</text>
|
||||
<text class="bt-card__desc">基于最近 {{ trendPoints.length }} 次体测数据</text>
|
||||
<BodyTestTrendChart
|
||||
:points="trendPoints"
|
||||
:unit="activeUnit"
|
||||
:width="chartWidth"
|
||||
:height="200"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">数据明细</text>
|
||||
<view
|
||||
v-for="(pt, idx) in trendPointsReversed"
|
||||
:key="pt.id"
|
||||
class="bt-compare-row"
|
||||
>
|
||||
<text class="bt-compare-row__label">{{ pt.date }}</text>
|
||||
<text class="bt-compare-row__val">{{ pt.value }}{{ activeUnit }}</text>
|
||||
<text
|
||||
v-if="idx < trendPointsReversed.length - 1"
|
||||
class="bt-compare-row__diff"
|
||||
:style="{ color: rowDiffColor(pt, trendPointsReversed[idx + 1]) }"
|
||||
>
|
||||
{{ rowDiffText(pt, trendPointsReversed[idx + 1]) }}
|
||||
</text>
|
||||
<text v-else class="bt-compare-row__diff">--</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="summaryText" class="bt-card">
|
||||
<text class="bt-card__title">趋势解读</text>
|
||||
<text class="bt-card__desc">{{ summaryText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getBodyTestTrendData, bodyTestMock } from '@/common/memberInfo/bodyTestStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
||||
data() {
|
||||
return {
|
||||
tabs: bodyTestMock.trendMetrics,
|
||||
activeMetric: 'weight',
|
||||
trendPoints: [],
|
||||
chartWidth: 300
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeLabel() {
|
||||
return this.tabs.find((t) => t.key === this.activeMetric)?.label || ''
|
||||
},
|
||||
activeUnit() {
|
||||
const units = {
|
||||
weight: 'kg',
|
||||
bodyFat: '%',
|
||||
muscleMass: 'kg',
|
||||
bmi: ''
|
||||
}
|
||||
return units[this.activeMetric] || ''
|
||||
},
|
||||
trendPointsReversed() {
|
||||
return [...this.trendPoints].reverse()
|
||||
},
|
||||
summaryText() {
|
||||
if (this.trendPoints.length < 2) return ''
|
||||
const first = this.trendPoints[0].value
|
||||
const last = this.trendPoints[this.trendPoints.length - 1].value
|
||||
const diff = Math.round((last - first) * 10) / 10
|
||||
const sign = diff > 0 ? '上升' : diff < 0 ? '下降' : '持平'
|
||||
const abs = Math.abs(diff)
|
||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
||||
let advice = ''
|
||||
if (diff !== 0) {
|
||||
const good = lowerBetter ? diff < 0 : diff > 0
|
||||
advice = good ? ',变化方向符合健康目标' : ',建议关注饮食与训练计划'
|
||||
}
|
||||
return `期间${this.activeLabel}${sign} ${abs}${this.activeUnit}${advice}`
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options?.metric) {
|
||||
this.activeMetric = options.metric
|
||||
}
|
||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||
this.loadTrend()
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.BODY_TEST_HOME)
|
||||
},
|
||||
switchMetric(key) {
|
||||
this.activeMetric = key
|
||||
this.loadTrend()
|
||||
},
|
||||
loadTrend() {
|
||||
const store = loadMemberStore()
|
||||
this.trendPoints = getBodyTestTrendData(store, this.activeMetric, 6)
|
||||
},
|
||||
rowDiffText(current, older) {
|
||||
const diff = Math.round((current.value - older.value) * 10) / 10
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return `${sign}${diff}${this.activeUnit}`
|
||||
},
|
||||
rowDiffColor(current, older) {
|
||||
const diff = current.value - older.value
|
||||
if (diff === 0) return '#8A99B4'
|
||||
const lowerBetter = ['weight', 'bodyFat'].includes(this.activeMetric)
|
||||
const good = lowerBetter ? diff < 0 : diff > 0
|
||||
return good ? '#2ECC71' : '#F39C12'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="booking-page">
|
||||
<MemberInfoSubNav title="我的预约" @back="goBack" />
|
||||
|
||||
<view class="booking-page__tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="booking-page__tab"
|
||||
:class="{ 'booking-page__tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="setActiveTab(tab.key)"
|
||||
>
|
||||
<text
|
||||
class="booking-page__tab-text"
|
||||
:class="{ 'booking-page__tab-text--active': activeTab === tab.key }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
<view
|
||||
v-if="activeTab === tab.key"
|
||||
class="booking-page__tab-indicator"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link bt-page__action-link--primary"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCourseList"
|
||||
>
|
||||
预约课程
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="booking-page__body">
|
||||
<view
|
||||
v-if="activeTab === 'ongoing' && upcomingAlert"
|
||||
class="booking-page__alert"
|
||||
>
|
||||
<image
|
||||
class="booking-page__alert-icon"
|
||||
src="/static/images/clock1.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="booking-page__alert-text">{{ upcomingAlert }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in displayedBookings"
|
||||
:key="item.id"
|
||||
class="bk-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<image
|
||||
class="bk-card__banner"
|
||||
:src="item.banner"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="bk-card__content">
|
||||
<view class="bk-card__header">
|
||||
<text class="bk-card__title">{{ item.title }}</text>
|
||||
<view
|
||||
class="bk-card__status"
|
||||
:class="'bk-card__status--' + item.status"
|
||||
>
|
||||
<text
|
||||
class="bk-card__status-text"
|
||||
:class="'bk-card__status-text--' + item.status"
|
||||
>
|
||||
{{ item.statusLabel }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bk-card__meta">
|
||||
<view class="bk-card__meta-row">
|
||||
<image
|
||||
class="bk-card__meta-icon"
|
||||
src="/static/images/clock0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="bk-card__meta-text">{{ item.schedule }}</text>
|
||||
</view>
|
||||
<view class="bk-card__meta-row">
|
||||
<image
|
||||
class="bk-card__meta-icon"
|
||||
src="/static/images/user0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="bk-card__meta-text">{{ item.coach }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bk-card__footer">
|
||||
<text class="bk-card__footer-info">{{ item.footerText }}</text>
|
||||
<view
|
||||
v-if="item.canCancel"
|
||||
class="bk-card__cancel"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="cancelBooking(item)"
|
||||
>
|
||||
<text class="bk-card__cancel-text">取消预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!displayedBookings.length" class="booking-page__empty">
|
||||
<text class="booking-page__empty-text">
|
||||
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { bookingMock } from '@/common/memberInfo/mockData.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
cancelOngoingBooking,
|
||||
formatUpcomingAlert
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { canCancelBooking } from '@/common/memberInfo/bookingStore.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: bookingMock.tabs,
|
||||
ongoingBookings: [],
|
||||
historyBookings: [],
|
||||
activeTab: 'ongoing'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayedBookings() {
|
||||
return this.activeTab === 'ongoing'
|
||||
? this.ongoingBookings
|
||||
: this.historyBookings
|
||||
},
|
||||
upcomingAlert() {
|
||||
return formatUpcomingAlert(this.ongoingBookings[0])
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.ongoingBookings = store.ongoingBookings.map((item) => ({
|
||||
...item,
|
||||
canCancel: canCancelBooking(item)
|
||||
}))
|
||||
this.historyBookings = store.historyBookings.map((item) => ({ ...item }))
|
||||
},
|
||||
goCourseList() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
},
|
||||
setActiveTab(tab) {
|
||||
this.activeTab = tab
|
||||
},
|
||||
cancelBooking(item) {
|
||||
if (!canCancelBooking(item)) {
|
||||
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '取消预约',
|
||||
content: `确定要取消「${item.title}」吗?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = cancelOngoingBooking(store, item.id)
|
||||
if (!result.ok) {
|
||||
uni.showToast({ title: result.message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/booking-page.css';
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="签到记录" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeFilter === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="activeFilter = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">共 {{ filteredList.length }} 条记录</text>
|
||||
<view
|
||||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
class="mi-mod-checkin-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="showDetail(item)"
|
||||
>
|
||||
<view class="mi-mod-checkin-row__icon" :class="'mi-mod-checkin-row__icon--' + item.tagTheme">
|
||||
<image class="mi-mod-checkin-row__icon-img" src="/static/images/usercheck.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="mi-mod-checkin-row__info">
|
||||
<text class="mi-mod-checkin-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-checkin-row__time">{{ item.time }} · {{ item.location }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-checkin-row__tag" :class="'mi-mod-checkin-row__tag--' + item.tagTheme">
|
||||
<text class="mi-mod-checkin-row__tag-text">{{ item.tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!filteredList.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无签到记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { moduleMock, getCheckInHistory } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.checkInTabs,
|
||||
activeFilter: 'all',
|
||||
list: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredList() {
|
||||
if (this.activeFilter === 'all') return this.list
|
||||
return this.list.filter((i) => i.tagTheme === this.activeFilter)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.list = getCheckInHistory(store, 'all')
|
||||
},
|
||||
methods: {
|
||||
showDetail(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.time}\n地点:${item.location}\n类型:${item.tag}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="领券中心" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">可领取优惠券</text>
|
||||
<text class="bt-card__desc">领取后自动放入「可用」列表</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mi-center-coupon"
|
||||
>
|
||||
<view class="mi-center-coupon__info">
|
||||
<text class="mi-center-coupon__amount">¥{{ item.amount }}</text>
|
||||
<view>
|
||||
<text class="mi-center-coupon__title">{{ item.title }}</text>
|
||||
<text class="mi-center-coupon__desc">{{ item.desc }} · {{ item.expireDays }}天有效</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="mi-center-coupon__btn"
|
||||
:class="{ 'mi-center-coupon__btn--done': item.claimed }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="claim(item)"
|
||||
>
|
||||
<text>{{ item.claimed ? '已领取' : '立即领取' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getCouponCenterList, claimCouponFromCenter } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { list: [] }
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.list = getCouponCenterList(store)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
||||
claim(item) {
|
||||
if (item.claimed) return
|
||||
const store = loadMemberStore()
|
||||
const result = claimCouponFromCenter(store, item.id)
|
||||
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
||||
if (result.ok) {
|
||||
persistMemberStore(store)
|
||||
this.list = getCouponCenterList(store)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.mi-center-coupon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-white, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.mi-center-coupon__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mi-center-coupon__amount {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-center-coupon__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-center-coupon__desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn {
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn--done {
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
}
|
||||
|
||||
.mi-center-coupon__btn--done text {
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="coupon">
|
||||
<MemberInfoSubNav title="优惠券详情" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-coupon mi-mod-coupon--detail">
|
||||
<view class="mi-mod-coupon__left">
|
||||
<text class="mi-mod-coupon__amount">¥{{ coupon.amount }}</text>
|
||||
<text class="mi-mod-coupon__min">满{{ coupon.minSpend }}可用</text>
|
||||
</view>
|
||||
<view class="mi-mod-coupon__right">
|
||||
<text class="mi-mod-coupon__title">{{ coupon.title }}</text>
|
||||
<text class="mi-mod-coupon__desc">{{ coupon.desc }}</text>
|
||||
<text class="mi-mod-coupon__expire">有效期至 {{ coupon.expire }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">使用规则</text>
|
||||
<text class="bt-card__desc" style="white-space: pre-line;">{{ coupon.rules }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">适用范围</text>
|
||||
<text class="bt-card__desc">{{ coupon.scope }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">使用流程</text>
|
||||
<text class="bt-card__desc">{{ coupon.flow }}</text>
|
||||
</view>
|
||||
<view v-if="coupon.status === 'available'" class="bt-footer-actions">
|
||||
<view class="bt-btn bt-btn--primary" hover-class="mi-tap-btn--hover" @tap="useNow">
|
||||
<text class="bt-btn__text">立即使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getCouponById } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { coupon: null }
|
||||
},
|
||||
onLoad(options) {
|
||||
const store = loadMemberStore()
|
||||
this.coupon = getCouponById(store, options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.COUPONS) },
|
||||
useNow() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
.mi-mod-coupon--detail { margin-bottom: 12px; }
|
||||
.bt-footer-actions .bt-btn { flex: 1; }
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的优惠券" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}({{ countByTab[tab.key] }})</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link bt-page__action-link--primary"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCenter"
|
||||
>
|
||||
领券中心
|
||||
</text>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="item in displayedCoupons"
|
||||
:key="item.id"
|
||||
class="mi-mod-coupon"
|
||||
:class="'mi-mod-coupon--' + item.status"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="onCouponTap(item)"
|
||||
>
|
||||
<view class="mi-mod-coupon__left">
|
||||
<text class="mi-mod-coupon__amount">¥{{ item.amount }}</text>
|
||||
<text class="mi-mod-coupon__min">满{{ item.minSpend }}可用</text>
|
||||
</view>
|
||||
<view class="mi-mod-coupon__right">
|
||||
<text class="mi-mod-coupon__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-coupon__desc">{{ item.desc }}</text>
|
||||
<text class="mi-mod-coupon__expire">
|
||||
{{ item.status === 'used' ? `已于 ${item.usedAt} 使用` : `有效期至 ${item.expire}` }}
|
||||
</text>
|
||||
<view v-if="item.status === 'available'" class="mi-mod-coupon__use" @tap.stop="useNow(item)">
|
||||
<text>立即使用</text>
|
||||
</view>
|
||||
<view v-if="item.status === 'expired'" class="mi-mod-coupon__del" @tap.stop="removeExpired(item)">
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!displayedCoupons.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无{{ activeTabLabel }}优惠券</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { moduleMock, getCouponsByStatus, useCoupon, deleteExpiredCoupon } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.couponTabs,
|
||||
activeTab: 'available',
|
||||
coupons: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayedCoupons() {
|
||||
return this.coupons.filter((c) => c.status === this.activeTab)
|
||||
},
|
||||
countByTab() {
|
||||
return {
|
||||
available: this.coupons.filter((c) => c.status === 'available').length,
|
||||
used: this.coupons.filter((c) => c.status === 'used').length,
|
||||
expired: this.coupons.filter((c) => c.status === 'expired').length
|
||||
}
|
||||
},
|
||||
activeTabLabel() {
|
||||
return this.tabs.find((t) => t.key === this.activeTab)?.label || ''
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshList()
|
||||
},
|
||||
methods: {
|
||||
refreshList() {
|
||||
const store = loadMemberStore()
|
||||
this.coupons = [
|
||||
...getCouponsByStatus(store, 'available'),
|
||||
...getCouponsByStatus(store, 'used'),
|
||||
...getCouponsByStatus(store, 'expired')
|
||||
]
|
||||
},
|
||||
onCouponTap(item) {
|
||||
navigateToPage(`${PAGE.COUPON_DETAIL}?id=${item.id}`)
|
||||
},
|
||||
useNow(item) {
|
||||
uni.showModal({
|
||||
title: '使用优惠券',
|
||||
content: `确认使用「${item.title}」?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
useCoupon(store, item.id)
|
||||
persistMemberStore(store)
|
||||
this.refreshList()
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeExpired(item) {
|
||||
const store = loadMemberStore()
|
||||
deleteExpiredCoupon(store, item.id)
|
||||
persistMemberStore(store)
|
||||
this.refreshList()
|
||||
},
|
||||
goCenter() {
|
||||
navigateToPage(PAGE.COUPON_CENTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-coupon__use {
|
||||
margin-top: 8px;
|
||||
align-self: flex-start;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__use text {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__del {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mi-mod-coupon__del text {
|
||||
font-size: 11px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="course">
|
||||
<MemberInfoSubNav title="课程详情" @back="onBack" />
|
||||
<image class="mi-detail-hero" :src="course.banner" mode="aspectFill" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ course.title }}</text>
|
||||
<text class="bt-card__desc">{{ course.startTime }}-{{ course.endTime }} · {{ course.location }}</text>
|
||||
<view class="mi-detail-coach">
|
||||
<image class="mi-detail-coach__avatar" :src="course.coachAvatar" mode="aspectFill" />
|
||||
<view>
|
||||
<text class="mi-detail-coach__name">{{ course.coach }}</text>
|
||||
<text class="mi-detail-coach__rating">评分 {{ course.coachRating }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程介绍</text>
|
||||
<text class="bt-card__desc">{{ course.intro }}</text>
|
||||
<text class="bt-card__title" style="margin-top:12px;">适合人群</text>
|
||||
<text class="bt-card__desc">{{ course.suitable }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">教练介绍</text>
|
||||
<text class="bt-card__desc">{{ course.coachBio }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="course.reviews?.length" class="bt-card">
|
||||
<text class="bt-card__title">会员评价</text>
|
||||
<view v-for="(r, i) in course.reviews" :key="i" class="mi-detail-review">
|
||||
<text class="mi-detail-review__user">{{ r.user }} · {{ r.score }}分</text>
|
||||
<text class="mi-detail-review__text">{{ r.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">取消规则</text>
|
||||
<text class="bt-card__desc">{{ course.cancelRule }}</text>
|
||||
<text class="bt-card__desc" style="margin-top:8px;">费用:{{ course.price }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-footer-actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
:class="{ 'bt-btn--outline': course.full }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="confirmBook"
|
||||
>
|
||||
<text class="bt-btn__text">{{ course.full ? '已约满' : '确认预约' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getCourseById, enrichCourseForDisplay, bookCourse } from '@/common/memberInfo/bookingStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { course: null, courseId: null }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.courseId = options?.id
|
||||
this.loadCourse()
|
||||
},
|
||||
methods: {
|
||||
loadCourse() {
|
||||
const store = loadMemberStore()
|
||||
const raw = getCourseById(store, this.courseId)
|
||||
this.course = raw ? enrichCourseForDisplay(raw) : null
|
||||
},
|
||||
onBack() {
|
||||
goBackOrTab(PAGE.COURSE_LIST)
|
||||
},
|
||||
confirmBook() {
|
||||
if (!this.course || this.course.full) return
|
||||
uni.showModal({
|
||||
title: '确认预约',
|
||||
content: `预约「${this.course.title}」\n${this.course.date} ${this.course.startTime}-${this.course.endTime}`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = bookCourse(store, this.courseId)
|
||||
if (!result.ok) {
|
||||
uni.showToast({ title: result.message, icon: 'none' })
|
||||
return
|
||||
}
|
||||
persistMemberStore(store)
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
setTimeout(() => navigateToPage(PAGE.BOOKING), 800)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-detail-hero {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-coach {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mi-detail-coach__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.mi-detail-coach__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-coach__rating {
|
||||
font-size: 12px;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-detail-review {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.mi-detail-review:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mi-detail-review__user {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mi-detail-review__text {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-footer-actions .bt-btn {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="课程评价" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ title }}</text>
|
||||
<text class="bt-card__desc">请为本次课程打分并填写评价</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">评分</text>
|
||||
<view class="mi-eval-stars">
|
||||
<text
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
class="mi-eval-star"
|
||||
:class="{ 'mi-eval-star--on': n <= score }"
|
||||
@tap="score = n"
|
||||
>★</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">评价内容</text>
|
||||
<textarea
|
||||
v-model="comment"
|
||||
class="mi-eval-textarea"
|
||||
placeholder="分享您的上课体验…"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
<view class="bt-footer-actions">
|
||||
<view class="bt-btn bt-btn--primary" hover-class="mi-tap-btn--hover" @tap="submit">
|
||||
<text class="bt-btn__text">提交评价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { title: '', score: 5, comment: '' }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.title = decodeURIComponent(options?.title || '课程评价')
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.MY_COURSES) },
|
||||
submit() {
|
||||
if (!this.comment.trim()) {
|
||||
uni.showToast({ title: '请填写评价', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '评价已提交', icon: 'success' })
|
||||
setTimeout(() => goBackOrTab(PAGE.MY_COURSES), 800)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
|
||||
.mi-eval-stars { display: flex; flex-direction: row; gap: 8px; margin-top: 8px; }
|
||||
.mi-eval-star { font-size: 32px; color: #E9EDF2; }
|
||||
.mi-eval-star--on { color: #FF6B35; }
|
||||
.mi-eval-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bt-footer-actions .bt-btn { flex: 1; }
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page mi-course-list">
|
||||
<MemberInfoSubNav title="预约课程" @back="goBack" />
|
||||
<view class="mi-course-list__filters">
|
||||
<view class="mi-course-list__date-bar">
|
||||
<view
|
||||
class="mi-course-list__mode"
|
||||
hover-class="mi-tap--hover"
|
||||
@tap="toggleDateMode"
|
||||
>
|
||||
<text class="mi-course-list__mode-text">{{ dateMode === 'day' ? '按天' : '按周' }}</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="mi-course-list__dates">
|
||||
<view
|
||||
v-for="d in dateOptions"
|
||||
:key="d.value"
|
||||
class="mi-course-list__date"
|
||||
:class="{ 'mi-course-list__date--active': selectedDate === d.value }"
|
||||
@tap="selectedDate = d.value"
|
||||
>
|
||||
<text class="mi-course-list__date-week">{{ d.week }}</text>
|
||||
<text class="mi-course-list__date-day">{{ d.day }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<scroll-view scroll-x class="mi-course-list__chips">
|
||||
<view
|
||||
v-for="t in typeOptions"
|
||||
:key="t.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': courseType === t.key }"
|
||||
@tap="courseType = t.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ t.label }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="mi-course-list__row">
|
||||
<picker :range="coaches" @change="onCoachChange">
|
||||
<view class="mi-course-list__picker">
|
||||
<text>{{ coach }}</text>
|
||||
<image class="mi-course-list__arrow" src="/static/images/chevronright3.png" mode="aspectFit" />
|
||||
</view>
|
||||
</picker>
|
||||
<picker :range="periodLabels" @change="onPeriodChange">
|
||||
<view class="mi-course-list__picker">
|
||||
<text>{{ periodLabel }}</text>
|
||||
<image class="mi-course-list__arrow" src="/static/images/chevronright3.png" mode="aspectFit" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="course in displayCourses"
|
||||
:key="course.id"
|
||||
class="mi-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="openDetail(course)"
|
||||
>
|
||||
<image class="mi-course-card__banner" :src="course.banner" mode="aspectFill" />
|
||||
<view class="mi-course-card__body">
|
||||
<view class="mi-course-card__head">
|
||||
<text class="mi-course-card__title">{{ course.title }}</text>
|
||||
<view class="mi-course-card__type" :class="'mi-course-card__type--' + course.type">
|
||||
<text>{{ course.type === 'group' ? '团课' : '私教' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mi-course-card__coach">
|
||||
<image class="mi-course-card__avatar" :src="course.coachAvatar" mode="aspectFill" />
|
||||
<text>{{ course.coach }}</text>
|
||||
</view>
|
||||
<text class="mi-course-card__meta">
|
||||
{{ course.startTime }}-{{ course.endTime }} · {{ course.location }}
|
||||
</text>
|
||||
<view class="mi-course-card__capacity">
|
||||
<view class="mi-course-card__bar">
|
||||
<view class="mi-course-card__bar-fill" :style="{ width: course.percent + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-course-card__cap-text">{{ course.enrolled }}/{{ course.capacity }}</text>
|
||||
<text v-if="course.scarcityLabel" class="mi-course-card__scarcity">{{ course.scarcityLabel }}</text>
|
||||
</view>
|
||||
<view class="mi-course-card__footer">
|
||||
<text class="mi-course-card__price">{{ course.price }}</text>
|
||||
<view
|
||||
class="mi-course-card__btn"
|
||||
:class="{ 'mi-course-card__btn--disabled': course.full }"
|
||||
@tap.stop="quickBook(course)"
|
||||
>
|
||||
<text>{{ course.full ? '已约满' : '预约' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!displayCourses.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无符合条件的课程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mi-course-list__fab"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
@tap="goMyBooking"
|
||||
>
|
||||
<image class="mi-course-list__fab-icon" src="/static/images/clock.png" mode="aspectFit" />
|
||||
<text class="mi-course-list__fab-text">我的预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import {
|
||||
courseCatalogMock,
|
||||
filterCourses,
|
||||
enrichCourseForDisplay,
|
||||
getWeekDates,
|
||||
bookCourse
|
||||
} from '@/common/memberInfo/bookingStore.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
const WEEK = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
dateMode: 'day',
|
||||
selectedDate: '2024-07-15',
|
||||
courseType: 'all',
|
||||
coach: '全部',
|
||||
period: 'all',
|
||||
coaches: courseCatalogMock.coaches,
|
||||
typeOptions: courseCatalogMock.typeOptions,
|
||||
periodOptions: courseCatalogMock.periodOptions,
|
||||
courses: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
periodLabels() {
|
||||
return this.periodOptions.map((p) => p.label)
|
||||
},
|
||||
periodLabel() {
|
||||
return this.periodOptions.find((p) => p.key === this.period)?.label || '全部时段'
|
||||
},
|
||||
weekDates() {
|
||||
return getWeekDates(this.selectedDate)
|
||||
},
|
||||
dateOptions() {
|
||||
const dates = this.dateMode === 'week' ? this.weekDates : this.weekDates
|
||||
return dates.map((value) => {
|
||||
const d = new Date(value.replace(/-/g, '/'))
|
||||
return {
|
||||
value,
|
||||
week: WEEK[d.getDay()],
|
||||
day: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
},
|
||||
displayCourses() {
|
||||
const filters = {
|
||||
date: this.dateMode === 'day' ? this.selectedDate : '',
|
||||
weekDates: this.dateMode === 'week' ? this.weekDates : [],
|
||||
type: this.courseType,
|
||||
coach: this.coach,
|
||||
period: this.period
|
||||
}
|
||||
return filterCourses(this.courses, filters).map(enrichCourseForDisplay)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.courses = store.courseCatalog.map((c) => ({ ...c }))
|
||||
if (!this.selectedDate && this.courses.length) {
|
||||
this.selectedDate = this.courses[0].date
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDateMode() {
|
||||
this.dateMode = this.dateMode === 'day' ? 'week' : 'day'
|
||||
},
|
||||
onCoachChange(e) {
|
||||
this.coach = this.coaches[e.detail.value]
|
||||
},
|
||||
onPeriodChange(e) {
|
||||
this.period = this.periodOptions[e.detail.value].key
|
||||
},
|
||||
openDetail(course) {
|
||||
navigateToPage(`${PAGE.COURSE_DETAIL}?id=${course.id}`)
|
||||
},
|
||||
quickBook(course) {
|
||||
if (course.full) return
|
||||
navigateToPage(`${PAGE.COURSE_DETAIL}?id=${course.id}`)
|
||||
},
|
||||
goMyBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
@import '@/common/style/memberInfo/pages/course-list-page.css';
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="member-card-page">
|
||||
<MemberInfoSubNav title="我的会员卡" @back="goBack" />
|
||||
<view class="member-card-page__body">
|
||||
<view class="mc-hero">
|
||||
<view class="mc-hero__top">
|
||||
<view class="mc-hero__title-row">
|
||||
<image
|
||||
class="mc-hero__crown"
|
||||
src="/static/images/crown.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="mc-hero__name">{{ card.name }}</text>
|
||||
</view>
|
||||
<view class="mc-hero__badge">
|
||||
<text class="mc-hero__badge-text">{{ card.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mc-hero__validity">有效期:{{ card.validity }}</text>
|
||||
<view class="mc-hero__bottom">
|
||||
<view class="mc-hero__days">
|
||||
<text class="mc-hero__days-num">{{ remainingDays }}</text>
|
||||
<text class="mc-hero__days-unit">天</text>
|
||||
</view>
|
||||
<view
|
||||
class="mc-hero__renew"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="renewCard"
|
||||
>
|
||||
<image
|
||||
class="mc-hero__renew-icon"
|
||||
src="/static/images/refreshcw.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="mc-hero__renew-text">立即续费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mc-records">
|
||||
<view class="mc-records__header">
|
||||
<text class="mc-records__title">使用记录</text>
|
||||
<view class="mc-records__tabs">
|
||||
<view
|
||||
v-for="tab in recordTabs"
|
||||
:key="tab.key"
|
||||
class="mc-records__tab"
|
||||
:class="{ 'mc-records__tab--active': activeFilter === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="switchFilter(tab.key)"
|
||||
>
|
||||
<text
|
||||
class="mc-records__tab-text"
|
||||
:class="{ 'mc-records__tab-text--active': activeFilter === tab.key }"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mc-records__divider"></view>
|
||||
<view
|
||||
v-for="(item, index) in filteredRecords"
|
||||
:key="item.id"
|
||||
class="mc-records__item"
|
||||
>
|
||||
<view v-if="index > 0" class="mc-records__divider"></view>
|
||||
<view
|
||||
class="mc-records__item-inner"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="showRecordDetail(item)"
|
||||
>
|
||||
<view
|
||||
class="mc-records__icon-wrap"
|
||||
:class="'mc-records__icon-wrap--' + item.iconTheme"
|
||||
>
|
||||
<image
|
||||
class="mc-records__icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="mc-records__info">
|
||||
<text class="mc-records__item-title">{{ item.title }}</text>
|
||||
<text class="mc-records__item-time">{{ item.time }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="mc-records__value"
|
||||
:class="'mc-records__value--' + item.valueType"
|
||||
>
|
||||
{{ item.value }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!filteredRecords.length" class="mc-records__empty">
|
||||
<text class="mc-records__empty-text">暂无记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mc-rules">
|
||||
<text class="mc-rules__title">使用规则</text>
|
||||
<view
|
||||
v-for="(rule, index) in rules"
|
||||
:key="index"
|
||||
class="mc-rules__item"
|
||||
>
|
||||
<view class="mc-rules__bullet"></view>
|
||||
<text class="mc-rules__text">{{ rule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { memberCardMock } from '@/common/memberInfo/mockData.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
computeRemainingDays,
|
||||
renewMemberCard
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
card: {},
|
||||
recordTabs: memberCardMock.recordTabs,
|
||||
records: [],
|
||||
rules: memberCardMock.rules,
|
||||
activeFilter: 'all'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredRecords() {
|
||||
if (this.activeFilter === 'all') {
|
||||
return this.records
|
||||
}
|
||||
return this.records.filter((item) => item.type === this.activeFilter)
|
||||
},
|
||||
remainingDays() {
|
||||
return computeRemainingDays(this.card.validityEnd)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
this.card = { ...store.card }
|
||||
this.records = store.records.map((item) => ({ ...item }))
|
||||
},
|
||||
switchFilter(filter) {
|
||||
this.activeFilter = filter
|
||||
},
|
||||
renewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
renewMemberCard(store, 90)
|
||||
this.activeFilter = 'all'
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
showRecordDetail(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.time}\n变动:${item.value}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/member-card-page.css';
|
||||
|
||||
.mc-records__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.mc-records__empty-text {
|
||||
font-size: 14px;
|
||||
color: #8A99B4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="member-page">
|
||||
<MemberInfoHeader
|
||||
:user-info="userInfo"
|
||||
:stats="stats"
|
||||
@user-info="goUserInfo"
|
||||
/>
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<MemberInfoMemberCard
|
||||
:card-info="cardInfo"
|
||||
@view-all="goMemberCard"
|
||||
@renew="onRenewCard"
|
||||
/>
|
||||
<MemberInfoQuickActions @action="onQuickAction" />
|
||||
<MemberInfoBookingList
|
||||
:items="bookingPreview"
|
||||
@view-all="goBooking"
|
||||
@item-tap="goBooking"
|
||||
/>
|
||||
<MemberInfoCheckInList
|
||||
:items="checkIns"
|
||||
@view-all="onCheckInViewAll"
|
||||
@item-tap="onCheckInTap"
|
||||
/>
|
||||
<MemberInfoBodyReport
|
||||
:report="bodyReport"
|
||||
@view-history="onBodyReportHistory"
|
||||
@view-report="onBodyReportView"
|
||||
/>
|
||||
<MemberInfoCouponPoints
|
||||
:data="couponPoints"
|
||||
@view-all="onCouponViewAll"
|
||||
@use-coupon="onUseCoupon"
|
||||
@redeem-points="onRedeemPoints"
|
||||
/>
|
||||
<MemberInfoReferral
|
||||
:data="referral"
|
||||
@view-rules="onReferralRules"
|
||||
/>
|
||||
<MemberInfoSettings @setting="onSetting" />
|
||||
<MemberInfoLogout @logout="handleLogout" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<TabBar :active="4" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import {
|
||||
loadMemberStore,
|
||||
getCenterPageData,
|
||||
renewMemberCard
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
|
||||
import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.vue'
|
||||
import MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
||||
import MemberInfoBookingList from '@/components/memberInfo/MemberInfoBookingList.vue'
|
||||
import MemberInfoCheckInList from '@/components/memberInfo/MemberInfoCheckInList.vue'
|
||||
import MemberInfoBodyReport from '@/components/memberInfo/MemberInfoBodyReport.vue'
|
||||
import MemberInfoCouponPoints from '@/components/memberInfo/MemberInfoCouponPoints.vue'
|
||||
import MemberInfoReferral from '@/components/memberInfo/MemberInfoReferral.vue'
|
||||
import MemberInfoSettings from '@/components/memberInfo/MemberInfoSettings.vue'
|
||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MemberInfoHeader,
|
||||
MemberInfoMemberCard,
|
||||
MemberInfoQuickActions,
|
||||
MemberInfoBookingList,
|
||||
MemberInfoCheckInList,
|
||||
MemberInfoBodyReport,
|
||||
MemberInfoCouponPoints,
|
||||
MemberInfoReferral,
|
||||
MemberInfoSettings,
|
||||
MemberInfoLogout,
|
||||
TabBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
stats: {},
|
||||
cardInfo: {},
|
||||
bookingPreview: [],
|
||||
checkIns: [],
|
||||
bodyReport: {},
|
||||
couponPoints: {},
|
||||
referral: {}
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshFromStore()
|
||||
},
|
||||
methods: {
|
||||
refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
const pageData = getCenterPageData(store)
|
||||
this.userInfo = pageData.userInfo
|
||||
this.stats = pageData.stats
|
||||
this.cardInfo = pageData.cardInfo
|
||||
this.bookingPreview = pageData.bookingPreview
|
||||
this.checkIns = pageData.checkIns
|
||||
this.bodyReport = pageData.bodyReport
|
||||
this.couponPoints = pageData.couponPoints
|
||||
this.referral = pageData.referral
|
||||
},
|
||||
goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
},
|
||||
goBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
},
|
||||
goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
},
|
||||
onRenewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
renewMemberCard(store, 90)
|
||||
this.refreshFromStore()
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
onQuickAction(type) {
|
||||
const routes = {
|
||||
booking: PAGE.COURSE_LIST,
|
||||
bodyTest: PAGE.BODY_TEST_HOME,
|
||||
bodyReport: PAGE.BODY_TEST_HISTORY,
|
||||
trainReport: PAGE.TRAIN_REPORT,
|
||||
coupon: PAGE.COUPONS,
|
||||
points: PAGE.POINTS,
|
||||
referral: PAGE.REFERRAL,
|
||||
course: PAGE.MY_COURSES
|
||||
}
|
||||
if (routes[type]) {
|
||||
navigateToPage(routes[type])
|
||||
}
|
||||
},
|
||||
onCheckInViewAll() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
},
|
||||
onCheckInTap(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: item.time,
|
||||
showCancel: false
|
||||
})
|
||||
},
|
||||
onBodyReportHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
},
|
||||
onBodyReportView() {
|
||||
const id = this.bodyReport?.recordId
|
||||
const url = id
|
||||
? `${PAGE.BODY_TEST_REPORT}?id=${id}`
|
||||
: PAGE.BODY_TEST_HISTORY
|
||||
navigateToPage(url)
|
||||
},
|
||||
onCouponViewAll() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
},
|
||||
onUseCoupon() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
},
|
||||
onRedeemPoints() {
|
||||
navigateToPage(PAGE.POINTS)
|
||||
},
|
||||
onReferralRules() {
|
||||
navigateToPage(PAGE.REFERRAL)
|
||||
},
|
||||
onSetting(key) {
|
||||
const labels = {
|
||||
notify: '通知设置',
|
||||
password: '修改密码',
|
||||
privacy: '隐私政策',
|
||||
nfc: 'NFC 门禁卡',
|
||||
delete: '注销账户'
|
||||
}
|
||||
if (key === 'delete') {
|
||||
uni.showModal({
|
||||
title: '注销账户',
|
||||
content: '注销后数据将无法恢复,确定继续吗?',
|
||||
confirmColor: '#E74C3C'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (key === 'privacy') {
|
||||
uni.showModal({
|
||||
title: '隐私政策',
|
||||
content: '我们重视您的隐私,详细政策请前往官网查看。',
|
||||
showCancel: false
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: `${labels[key] || '设置'}开发中`,
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/member-info-page.css';
|
||||
@import '@/common/style/memberInfo/member-info-header.css';
|
||||
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的课程" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<!-- 团课 -->
|
||||
<template v-if="activeTab === 'group'">
|
||||
<view class="mi-mod-subtabs">
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'ongoing' }"
|
||||
@tap="groupSub = 'ongoing'"
|
||||
>进行中</text>
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'completed' }"
|
||||
@tap="groupSub = 'completed'"
|
||||
>已完成</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in groupList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="onGroupCourse(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }} · {{ item.location }}</text>
|
||||
<view v-if="groupSub === 'ongoing' && item.canCancel" class="mi-mod-course-card__action" @tap.stop="goBooking">
|
||||
<text>取消预约</text>
|
||||
</view>
|
||||
<view v-if="groupSub === 'completed' && item.canEvaluate" class="mi-mod-course-card__action" @tap.stop="evaluate(item)">
|
||||
<text>去评价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 私教 -->
|
||||
<template v-else-if="activeTab === 'private'">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">剩余课时 {{ privateData.remaining }} 节</text>
|
||||
<view class="mi-detail-coach">
|
||||
<image class="mi-detail-coach__avatar" :src="privateData.coachAvatar" mode="aspectFill" />
|
||||
<view>
|
||||
<text class="mi-detail-coach__name">{{ privateData.coach }}</text>
|
||||
<text class="mi-detail-coach__rating">下次 {{ privateData.nextClass }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">已约课程</text>
|
||||
<view v-for="b in privateData.bookings" :key="b.id" class="mi-mod-session">
|
||||
<text class="mi-mod-session__title">{{ b.title }}</text>
|
||||
<text class="mi-mod-session__meta">{{ b.time }} · {{ b.location }} · {{ b.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 线上课 -->
|
||||
<template v-else-if="activeTab === 'online'">
|
||||
<view
|
||||
v-for="item in onlineList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="goOnline(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.cover" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__meta">{{ item.duration }} · 进度 {{ item.progress }}%</text>
|
||||
<text v-if="item.type === 'live'" class="mi-mod-course-card__next">直播 {{ item.liveTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 训练营 -->
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="item in packageList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }} 节</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="isEmpty" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无课程</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { moduleMock, getMyCoursesData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.myCourseTabs,
|
||||
activeTab: 'group',
|
||||
groupSub: 'ongoing',
|
||||
privateData: {},
|
||||
onlineList: [],
|
||||
packageList: [],
|
||||
groupData: { ongoing: [], completed: [] }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupList() {
|
||||
return this.groupData[this.groupSub] || []
|
||||
},
|
||||
isEmpty() {
|
||||
if (this.activeTab === 'group') return !this.groupList.length
|
||||
if (this.activeTab === 'private') return !this.privateData.bookings?.length
|
||||
if (this.activeTab === 'online') return !this.onlineList.length
|
||||
return !this.packageList.length
|
||||
}
|
||||
},
|
||||
onShow() { this.refresh() },
|
||||
methods: {
|
||||
refresh() {
|
||||
const store = loadMemberStore()
|
||||
this.groupData = getMyCoursesData(store, 'group')
|
||||
this.privateData = getMyCoursesData(store, 'private')
|
||||
this.onlineList = getMyCoursesData(store, 'online').list || []
|
||||
this.packageList = getMyCoursesData(store, 'package').list || []
|
||||
},
|
||||
pct(item) {
|
||||
return item.total ? Math.min(100, Math.round((item.progress / item.total) * 100)) : 0
|
||||
},
|
||||
goBooking() { navigateToPage(PAGE.BOOKING) },
|
||||
goOnline(item) { navigateToPage(`${PAGE.ONLINE_COURSE}?id=${item.id}`) },
|
||||
evaluate(item) {
|
||||
navigateToPage(`${PAGE.COURSE_EVALUATE}?title=${encodeURIComponent(item.title)}`)
|
||||
},
|
||||
onGroupCourse(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.coach}\n${item.schedule}\n${item.location}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-subtabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mi-mod-subtab {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.mi-mod-subtab--on {
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="course">
|
||||
<MemberInfoSubNav title="线上课程" @back="onBack" />
|
||||
<image class="mi-detail-hero" :src="course.cover" mode="aspectFill" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">{{ course.title }}</text>
|
||||
<text class="bt-card__desc">时长 {{ course.duration }} · 进度 {{ course.progress }}%</text>
|
||||
<view class="mi-mod-course-card__progress" style="margin-top:12px;">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: course.progress + '%' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">视频播放</text>
|
||||
<view class="mi-online-player">
|
||||
<image class="mi-online-player__play" src="/static/images/play.png" mode="aspectFit" />
|
||||
<text class="mi-online-player__hint">点击播放(支持倍速与拖拽)</text>
|
||||
</view>
|
||||
<view class="mi-online-controls">
|
||||
<text class="mi-online-controls__btn" @tap="seek(-10)">-10s</text>
|
||||
<text class="mi-online-controls__btn" @tap="togglePlay">{{ playing ? '暂停' : '播放' }}</text>
|
||||
<text class="mi-online-controls__btn" @tap="seek(10)">+10s</text>
|
||||
<text class="mi-online-controls__btn" @tap="changeSpeed">倍速 {{ speed }}x</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程目录</text>
|
||||
<view v-for="ch in course.chapters" :key="ch.id" class="mi-online-chapter">
|
||||
<text class="mi-online-chapter__title">{{ ch.title }}</text>
|
||||
<text class="mi-online-chapter__meta">{{ ch.duration }} · {{ ch.done ? '已学完' : '未学习' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getOnlineCourseById, updateOnlineProgress } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { course: null, playing: false, speed: 1 }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.course = getOnlineCourseById(loadMemberStore(), options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.MY_COURSES) },
|
||||
togglePlay() {
|
||||
this.playing = !this.playing
|
||||
if (this.playing && this.course.progress < 100) {
|
||||
const store = loadMemberStore()
|
||||
updateOnlineProgress(store, this.course.id, Math.min(100, this.course.progress + 5))
|
||||
persistMemberStore(store)
|
||||
this.course.progress = Math.min(100, this.course.progress + 5)
|
||||
}
|
||||
},
|
||||
seek(sec) { uni.showToast({ title: `跳转 ${sec > 0 ? '+' : ''}${sec}s`, icon: 'none' }) },
|
||||
changeSpeed() {
|
||||
const speeds = [1, 1.25, 1.5, 2]
|
||||
const idx = (speeds.indexOf(this.speed) + 1) % speeds.length
|
||||
this.speed = speeds[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-online-player {
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mi-online-player__play { width: 48px; height: 48px; }
|
||||
.mi-online-player__hint { font-size: 12px; color: rgba(255,255,255,0.7); }
|
||||
|
||||
.mi-online-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mi-online-controls__btn {
|
||||
font-size: 12px;
|
||||
color: var(--primary-deep, #1A4A6F);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mi-online-chapter {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.mi-online-chapter__title { font-size: 14px; color: var(--text-dark, #1E2A3A); display: block; }
|
||||
.mi-online-chapter__meta { font-size: 11px; color: var(--text-muted, #5E6F8D); }
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的积分" @back="goBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-points-hero">
|
||||
<text class="mi-mod-points-hero__label">当前积分</text>
|
||||
<text class="mi-mod-points-hero__value">{{ balance }}</text>
|
||||
<text class="mi-mod-points-hero__tip">{{ config.rate }} · {{ config.rule }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">快捷入口</text>
|
||||
<view class="bt-grid">
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goMall">
|
||||
<image class="bt-grid__icon" src="/static/images/star.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">积分商城</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goHistory">
|
||||
<image class="bt-grid__icon" src="/static/images/clock.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">积分明细</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="checkIn">
|
||||
<image class="bt-grid__icon" src="/static/images/usercheck.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">签到赚积分</text>
|
||||
</view>
|
||||
<view class="bt-grid__item" hover-class="mi-tap--hover" @tap="goReferral">
|
||||
<image class="bt-grid__icon" src="/static/images/share2.png" mode="aspectFit" />
|
||||
<text class="bt-grid__label">邀请赚积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<view class="bt-trend-link" style="border:none;margin:0;padding:0;" @tap="goHistory">
|
||||
<text class="bt-card__title">最近明细</text>
|
||||
<text class="bt-trend-link__text">查看全部</text>
|
||||
</view>
|
||||
<view v-for="item in historyPreview" :key="item.id" class="mi-mod-points-row">
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<text
|
||||
class="mi-mod-points-row__amount"
|
||||
:class="item.amount > 0 ? 'mi-mod-points-row__amount--earn' : 'mi-mod-points-row__amount--spend'"
|
||||
>{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getPointsPageData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return { balance: 0, config: {}, historyPreview: [] }
|
||||
},
|
||||
onShow() {
|
||||
const data = getPointsPageData(loadMemberStore())
|
||||
this.balance = data.balance
|
||||
this.config = data.config
|
||||
this.historyPreview = data.history.slice(0, 5)
|
||||
},
|
||||
methods: {
|
||||
goMall() { navigateToPage(PAGE.POINTS_MALL) },
|
||||
goHistory() { navigateToPage(PAGE.POINTS_HISTORY) },
|
||||
goReferral() { navigateToPage(PAGE.REFERRAL) },
|
||||
checkIn() { uni.showToast({ title: '签到成功 +10 积分', icon: 'success' }) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="积分明细" @back="onBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': filter === tab.key }"
|
||||
@tap="filter = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mi-mod-points-row"
|
||||
>
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-points-row__right">
|
||||
<text
|
||||
class="mi-mod-points-row__amount"
|
||||
:class="item.amount > 0 ? 'mi-mod-points-row__amount--earn' : 'mi-mod-points-row__amount--spend'"
|
||||
>
|
||||
{{ item.amount > 0 ? '+' : '' }}{{ item.amount }}
|
||||
</text>
|
||||
<text class="mi-mod-points-row__balance">余额 {{ item.balance }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!list.length" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无明细</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { filterPointsHistory } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return {
|
||||
filter: 'all',
|
||||
tabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'earn', label: '获取' },
|
||||
{ key: 'spend', label: '消耗' }
|
||||
],
|
||||
list: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filter() { this.loadList() }
|
||||
},
|
||||
onShow() { this.loadList() },
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
||||
loadList() {
|
||||
const store = loadMemberStore()
|
||||
this.list = filterPointsHistory(store, this.filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="积分商城" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-points-hero">
|
||||
<text class="mi-mod-points-hero__label">可用积分</text>
|
||||
<text class="mi-mod-points-hero__value">{{ balance }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">兑换商品</text>
|
||||
<view class="mi-mod-rewards">
|
||||
<view
|
||||
v-for="item in rewards"
|
||||
:key="item.id"
|
||||
class="mi-mod-reward"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="redeem(item)"
|
||||
>
|
||||
<image class="mi-mod-reward__icon" :src="item.icon" mode="aspectFit" />
|
||||
<text class="mi-mod-reward__name">{{ item.name }}</text>
|
||||
<text class="mi-mod-reward__cost">{{ item.cost }} 积分</text>
|
||||
<text class="mi-mod-reward__stock">剩余 {{ item.stock }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="redeemRecords.length" class="bt-card">
|
||||
<text class="bt-card__title">兑换记录</text>
|
||||
<view v-for="r in redeemRecords" :key="r.id" class="mi-mod-points-row">
|
||||
<view class="mi-mod-points-row__info">
|
||||
<text class="mi-mod-points-row__title">{{ r.name }}</text>
|
||||
<text class="mi-mod-points-row__time">{{ r.time }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-points-row__amount mi-mod-points-row__amount--spend">-{{ r.cost }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore, persistMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getPointsPageData, redeemPointsReward } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() {
|
||||
return { balance: 0, rewards: [], redeemRecords: [] }
|
||||
},
|
||||
onShow() {
|
||||
const data = getPointsPageData(loadMemberStore())
|
||||
this.balance = data.balance
|
||||
this.rewards = data.rewards
|
||||
this.redeemRecords = data.redeemRecords
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.POINTS) },
|
||||
redeem(item) {
|
||||
uni.showModal({
|
||||
title: '确认兑换',
|
||||
content: `使用 ${item.cost} 积分兑换「${item.name}」?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
const result = redeemPointsReward(store, item.id)
|
||||
uni.showToast({ title: result.message, icon: result.ok ? 'success' : 'none' })
|
||||
if (result.ok) {
|
||||
persistMemberStore(store)
|
||||
const data = getPointsPageData(store)
|
||||
this.balance = data.balance
|
||||
this.rewards = data.rewards
|
||||
this.redeemRecords = data.redeemRecords
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="邀请好友" @back="goBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="mi-mod-referral-hero">
|
||||
<text class="mi-mod-referral-hero__title">邀请好友一起健身</text>
|
||||
<text class="mi-mod-referral-hero__desc">好友注册/购课,双方均可获得积分奖励</text>
|
||||
<view class="mi-mod-referral-code">
|
||||
<text class="mi-mod-referral-code__label">我的邀请码</text>
|
||||
<text class="mi-mod-referral-code__value">{{ data.code }}</text>
|
||||
</view>
|
||||
<view class="bt-hero__actions">
|
||||
<view
|
||||
class="bt-btn bt-btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="copyCode"
|
||||
>
|
||||
<text class="bt-btn__text">复制邀请码</text>
|
||||
</view>
|
||||
<view
|
||||
class="bt-btn bt-btn--ghost"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="shareInvite"
|
||||
>
|
||||
<image class="bt-btn__icon" src="/static/images/share2.png" mode="aspectFit" />
|
||||
<text class="bt-btn__text">分享给好友</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">邀请成果</text>
|
||||
<view class="mi-mod-referral-stats">
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--orange">{{ data.invited }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已推荐</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--green">{{ data.registered }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已注册</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--amber">{{ data.purchased }}</text>
|
||||
<text class="mi-mod-referral-stat__label">已购课</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">已获得奖励</text>
|
||||
<view class="mi-mod-referral-stats">
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--orange">{{ data.rewardSummary?.totalPoints || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">积分</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--green">{{ data.rewardSummary?.totalCoupons || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">优惠券</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-stat">
|
||||
<text class="mi-mod-referral-stat__num mi-mod-referral-stat__num--amber">{{ data.rewardSummary?.pendingCount || 0 }}</text>
|
||||
<text class="mi-mod-referral-stat__label">待发放</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">邀请记录</text>
|
||||
<view
|
||||
v-for="item in data.records"
|
||||
:key="item.id"
|
||||
class="mi-mod-referral-row"
|
||||
>
|
||||
<view class="mi-mod-referral-row__info">
|
||||
<text class="mi-mod-referral-row__name">{{ item.name }}</text>
|
||||
<text class="mi-mod-referral-row__time">{{ item.time }}</text>
|
||||
</view>
|
||||
<view class="mi-mod-referral-row__right">
|
||||
<text class="mi-mod-referral-row__status">{{ item.statusLabel }}</text>
|
||||
<text class="mi-mod-referral-row__reward">{{ item.reward }}</text>
|
||||
<text class="mi-mod-referral-row__reward-status">{{ item.rewardStatus }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">活动规则</text>
|
||||
<view class="bt-advice-list">
|
||||
<view
|
||||
v-for="(rule, idx) in data.rules"
|
||||
:key="idx"
|
||||
class="bt-advice-item"
|
||||
>
|
||||
<view class="bt-advice-item__dot"></view>
|
||||
<text class="bt-advice-item__text">{{ rule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { getReferralPageData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
data: { code: '', invited: 0, registered: 0, purchased: 0, records: [], rules: [] }
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const store = loadMemberStore()
|
||||
this.data = getReferralPageData(store)
|
||||
},
|
||||
methods: {
|
||||
copyCode() {
|
||||
uni.setClipboardData({
|
||||
data: this.data.code,
|
||||
success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
shareInvite() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享给微信好友', '生成邀请海报', '复制分享链接'],
|
||||
success: (res) => {
|
||||
const msgs = ['已唤起分享', '海报已生成', '链接已复制']
|
||||
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
.mi-mod-referral-hero .bt-hero__actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="训练报告" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="p in periods"
|
||||
:key="p.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': period === p.key }"
|
||||
@tap="switchPeriod(p.key)"
|
||||
>
|
||||
<text class="bt-tab__text">{{ p.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-metrics">
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.sessions }}</text><text class="bt-metric__label">完成课程</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.hours }}</text><text class="bt-metric__label">运动时长(h)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.calories }}</text><text class="bt-metric__label">消耗(kcal)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ report.summary.visits }}</text><text class="bt-metric__label">到店次数</text></view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">运动时长趋势</text>
|
||||
<BodyTestTrendChart :points="report.trendHours" unit="h" :width="chartWidth" :height="150" />
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">消耗卡路里趋势</text>
|
||||
<BodyTestTrendChart :points="report.trendCalories" unit="" :width="chartWidth" :height="150" />
|
||||
</view>
|
||||
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="t in typeFilters"
|
||||
:key="t.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': typeFilter === t.key }"
|
||||
@tap="typeFilter = t.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ t.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">课程完成列表</text>
|
||||
<view
|
||||
v-for="item in sessions"
|
||||
:key="item.id"
|
||||
class="mi-mod-session"
|
||||
hover-class="mi-tap-row--hover"
|
||||
@tap="goSession(item)"
|
||||
>
|
||||
<view class="mi-mod-session__head">
|
||||
<text class="mi-mod-session__title">{{ item.title }}</text>
|
||||
<view class="mi-mod-session__tag" :class="'mi-mod-session__tag--' + item.type">
|
||||
<text class="mi-mod-session__tag-text">{{ item.typeLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mi-mod-session__meta">{{ item.date }} · {{ item.time }} · {{ item.coach }}</text>
|
||||
<view class="mi-mod-session__footer">
|
||||
<text class="mi-mod-session__stat">{{ item.duration }}</text>
|
||||
<text class="mi-mod-session__stat">{{ item.calories }} kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import BodyTestTrendChart from '@/components/memberInfo/BodyTestTrendChart.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getTrainingReportData, filterTrainingSessions } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav, BodyTestTrendChart },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
period: 'week',
|
||||
periods: [
|
||||
{ key: 'week', label: '本周' },
|
||||
{ key: 'month', label: '本月' }
|
||||
],
|
||||
typeFilter: 'all',
|
||||
typeFilters: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'group', label: '团课' },
|
||||
{ key: 'private', label: '私教' },
|
||||
{ key: 'free', label: '自由' }
|
||||
],
|
||||
report: { summary: {}, trendHours: [], trendCalories: [], sessions: [] },
|
||||
sessions: [],
|
||||
chartWidth: 300
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.chartWidth = uni.getSystemInfoSync().windowWidth - 64
|
||||
this.refresh()
|
||||
},
|
||||
onShow() { this.refresh() },
|
||||
methods: {
|
||||
refresh() {
|
||||
const store = loadMemberStore()
|
||||
this.report = getTrainingReportData(store, this.period)
|
||||
this.sessions = filterTrainingSessions(store, { type: this.typeFilter })
|
||||
},
|
||||
switchPeriod(key) {
|
||||
this.period = key
|
||||
this.refresh()
|
||||
},
|
||||
goSession(item) {
|
||||
navigateToPage(`${PAGE.TRAIN_SESSION}?id=${item.id}`)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
typeFilter() {
|
||||
this.sessions = filterTrainingSessions(loadMemberStore(), { type: this.typeFilter })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page" v-if="session">
|
||||
<MemberInfoSubNav title="训练详情" @back="onBack" />
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-score-card">
|
||||
<view class="bt-score-card__info">
|
||||
<text class="bt-score-card__title">{{ session.title }}</text>
|
||||
<text class="bt-score-card__date">{{ session.date }} {{ session.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">训练数据</text>
|
||||
<view class="bt-metrics">
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.duration }}</text><text class="bt-metric__label">时长</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.calories }}</text><text class="bt-metric__label">消耗(kcal)</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.heartRate }}</text><text class="bt-metric__label">平均心率</text></view>
|
||||
<view class="bt-metric"><text class="bt-metric__value">{{ session.typeLabel }}</text><text class="bt-metric__label">类型</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">签到时间</text>
|
||||
<text class="bt-card__desc">{{ session.checkInTime }}</text>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">教练评语</text>
|
||||
<text class="bt-card__desc">{{ session.comment }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, goBackOrTab } from '@/common/constants/routes.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getTrainingSessionById } from '@/common/memberInfo/moduleStore.js'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
data() { return { session: null } },
|
||||
onLoad(options) {
|
||||
this.session = getTrainingSessionById(loadMemberStore(), options?.id)
|
||||
},
|
||||
methods: {
|
||||
onBack() { goBackOrTab(PAGE.TRAIN_REPORT) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
</style>
|
||||
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="Pixso-frame-2_791">
|
||||
<MemberInfoSubNav title="个人信息" @back="goBack" />
|
||||
<view class="Pixso-frame-2_802">
|
||||
<view class="frame-content-2_802">
|
||||
<view class="avatar-block">
|
||||
<view class="avatar-block__inner">
|
||||
<image
|
||||
class="avatar-block__photo"
|
||||
:key="avatarKey"
|
||||
:src="avatarSrc"
|
||||
mode="aspectFill"
|
||||
@tap="previewAvatar"
|
||||
/>
|
||||
<view
|
||||
class="avatar-block__change"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="changeAvatar"
|
||||
>
|
||||
<image
|
||||
class="avatar-block__icon"
|
||||
src="/static/images/camera.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="avatar-block__text">更换</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_810">
|
||||
<view class="frame-content-2_810">
|
||||
<view
|
||||
class="Pixso-frame-2_811 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editName"
|
||||
>
|
||||
<view class="frame-content-2_811">
|
||||
<text class="Pixso-paragraph-2_812">姓名</text>
|
||||
<text class="Pixso-paragraph-2_813">{{ name }}</text>
|
||||
<image
|
||||
class="Pixso-vector-2_814"
|
||||
src="/static/images/chevronright1.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_816"></view>
|
||||
<view
|
||||
class="Pixso-frame-2_817 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="rebindPhone"
|
||||
>
|
||||
<view class="frame-content-2_817">
|
||||
<text class="Pixso-paragraph-2_818">手机号</text>
|
||||
<view class="Pixso-frame-2_819">
|
||||
<view class="frame-content-2_819">
|
||||
<text class="Pixso-paragraph-2_820">{{ displayPhone }}</text>
|
||||
<view
|
||||
class="Pixso-frame-2_821"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="rebindPhone"
|
||||
>
|
||||
<text class="Pixso-paragraph-2_822">换绑</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
class="Pixso-vector-2_823"
|
||||
src="/static/images/chevronright.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_825"></view>
|
||||
<view class="Pixso-frame-2_826">
|
||||
<view class="frame-content-2_826">
|
||||
<text class="Pixso-paragraph-2_827">性别</text>
|
||||
<view class="Pixso-frame-2_828">
|
||||
<view class="frame-content-2_828">
|
||||
<view
|
||||
class="gender-btn"
|
||||
:class="{ 'gender-btn--active': gender === 'female' }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="selectGender('female')"
|
||||
>
|
||||
<image
|
||||
class="gender-btn__icon"
|
||||
src="/static/images/venus.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="gender-btn__text">女</text>
|
||||
</view>
|
||||
<view
|
||||
class="gender-btn"
|
||||
:class="{ 'gender-btn--active': gender === 'male' }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="selectGender('male')"
|
||||
>
|
||||
<image
|
||||
class="gender-btn__icon"
|
||||
src="/static/images/mars.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="gender-btn__text">男</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_841"></view>
|
||||
<picker mode="date" :value="birthdayValue" @change="onBirthdayChange">
|
||||
<view
|
||||
class="Pixso-frame-2_842 user-info-row"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<view class="frame-content-2_842">
|
||||
<text class="Pixso-paragraph-2_843">生日</text>
|
||||
<text class="Pixso-paragraph-2_844">{{ birthday }}</text>
|
||||
<image
|
||||
class="Pixso-vector-2_845"
|
||||
src="/static/images/chevronright0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="Pixso-frame-2_847"></view>
|
||||
<view class="Pixso-frame-2_848">
|
||||
<view class="frame-content-2_848">
|
||||
<text class="Pixso-paragraph-2_849">身高</text>
|
||||
<view
|
||||
class="Pixso-frame-2_850 user-info-measure"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editHeight"
|
||||
>
|
||||
<view class="frame-content-2_850">
|
||||
<text class="Pixso-paragraph-2_851">{{ height }}</text>
|
||||
<text class="Pixso-paragraph-2_852">cm</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_853"></view>
|
||||
<text class="Pixso-paragraph-2_854">体重</text>
|
||||
<view
|
||||
class="Pixso-frame-2_855 user-info-measure"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="editWeight"
|
||||
>
|
||||
<view class="frame-content-2_855">
|
||||
<text class="Pixso-paragraph-2_856">{{ weight }}</text>
|
||||
<text class="Pixso-paragraph-2_857">kg</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_858"></view>
|
||||
<view class="Pixso-frame-2_859">
|
||||
<view class="frame-content-2_859">
|
||||
<text class="Pixso-paragraph-2_860">微信</text>
|
||||
<text class="Pixso-paragraph-2_861">已授权绑定</text>
|
||||
<view class="stroke-wrapper-2_862">
|
||||
<view class="Pixso-frame-2_862">
|
||||
<text class="Pixso-paragraph-2_863">已绑定</text>
|
||||
</view>
|
||||
<view class="stroke-2_862"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="Pixso-frame-2_864">
|
||||
<view class="frame-content-2_864">
|
||||
<text class="Pixso-paragraph-2_865">健身目标</text>
|
||||
<view class="goal-tags">
|
||||
<view
|
||||
v-for="goal in fitnessGoalOptions"
|
||||
:key="goal"
|
||||
class="goal-tag"
|
||||
:class="{ 'goal-tag--selected': isGoalSelected(goal) }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleGoal(goal)"
|
||||
>
|
||||
<text class="goal-tag__text">{{ goal }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info-save-bar">
|
||||
<view
|
||||
class="user-info-save-bar__btn"
|
||||
hover-class="mi-tap-save--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="handleSave"
|
||||
>
|
||||
<text class="user-info-save-bar__text">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { fitnessGoalOptions } from '@/common/memberInfo/mockData.js'
|
||||
import { loadMemberStore, saveUserProfile } from '@/common/memberInfo/store.js'
|
||||
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
||||
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
import {
|
||||
validateName,
|
||||
validatePhoneForRebind,
|
||||
validateHeight,
|
||||
validateWeight,
|
||||
validateBirthday,
|
||||
validateFitnessGoals,
|
||||
validateUserProfile,
|
||||
showValidationError
|
||||
} from '@/common/memberInfo/validate.js'
|
||||
|
||||
const DEFAULT_AVATAR = '/static/images/AvatarEditWrap.png'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
phone: '',
|
||||
gender: 'female',
|
||||
birthday: '',
|
||||
height: '',
|
||||
weight: '',
|
||||
fitnessGoals: [],
|
||||
avatar: DEFAULT_AVATAR,
|
||||
avatarKey: 0,
|
||||
avatarDirty: false,
|
||||
fitnessGoalOptions
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarSrc() {
|
||||
return this.avatar || DEFAULT_AVATAR
|
||||
},
|
||||
displayPhone() {
|
||||
return maskPhone(this.phone)
|
||||
},
|
||||
birthdayValue() {
|
||||
const match = String(this.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||
if (match) {
|
||||
return `${match[1]}-${match[2]}-${match[3]}`
|
||||
}
|
||||
return '1995-06-15'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 从相册/相机返回会再次触发 onShow,不能覆盖刚选未保存的头像
|
||||
this.loadProfile({ preserveLocalAvatar: true })
|
||||
},
|
||||
methods: {
|
||||
loadProfile(options = {}) {
|
||||
const store = loadMemberStore()
|
||||
const profile = store.profile
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...(profile.fitnessGoals || [])]
|
||||
|
||||
const storedAvatar = profile.avatar || DEFAULT_AVATAR
|
||||
const hasUnsavedLocalAvatar =
|
||||
options.preserveLocalAvatar &&
|
||||
this.avatarDirty &&
|
||||
this.avatar &&
|
||||
this.avatar !== storedAvatar
|
||||
|
||||
if (!hasUnsavedLocalAvatar) {
|
||||
this.setAvatar(storedAvatar)
|
||||
this.avatarDirty = false
|
||||
}
|
||||
},
|
||||
setAvatar(path) {
|
||||
const next = path || DEFAULT_AVATAR
|
||||
if (this.avatar !== next) {
|
||||
this.avatar = next
|
||||
this.avatarKey += 1
|
||||
}
|
||||
},
|
||||
getProfilePayload() {
|
||||
return {
|
||||
name: this.name,
|
||||
phone: normalizePhoneForStore(this.phone),
|
||||
gender: this.gender,
|
||||
birthday: this.birthday,
|
||||
height: this.height,
|
||||
weight: this.weight,
|
||||
fitnessGoals: [...this.fitnessGoals],
|
||||
avatar: this.avatar
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
const result = validateUserProfile(
|
||||
this.getProfilePayload(),
|
||||
this.fitnessGoalOptions
|
||||
)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
|
||||
const store = loadMemberStore()
|
||||
saveUserProfile(store, result.value)
|
||||
this.applyValidatedProfile(result.value)
|
||||
this.avatarDirty = false
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => this.goBack(), 600)
|
||||
},
|
||||
applyValidatedProfile(profile) {
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...profile.fitnessGoals]
|
||||
},
|
||||
changeAvatar() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempPath =
|
||||
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
||||
if (!tempPath) return
|
||||
|
||||
// 真机先用 tempFilePath 立即展示,避免 saveFile 异步期间被 onShow 覆盖
|
||||
this.setAvatar(tempPath)
|
||||
this.avatarDirty = true
|
||||
uni.showToast({ title: '头像已选择', icon: 'success' })
|
||||
|
||||
persistChosenImage(tempPath).then((savedPath) => {
|
||||
if (savedPath && savedPath !== this.avatar) {
|
||||
this.setAvatar(savedPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
previewAvatar() {
|
||||
previewImage(this.avatarSrc, DEFAULT_AVATAR)
|
||||
},
|
||||
editName() {
|
||||
uni.showModal({
|
||||
title: '修改姓名',
|
||||
editable: true,
|
||||
placeholderText: '请输入姓名(2-8字)',
|
||||
content: this.name,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateName(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.name = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
rebindPhone() {
|
||||
uni.showModal({
|
||||
title: '换绑手机号',
|
||||
editable: true,
|
||||
placeholderText: '请输入11位手机号',
|
||||
content: normalizePhoneForStore(this.phone) || this.phone,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validatePhoneForRebind(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.phone = result.value
|
||||
uni.showToast({ title: '手机号已更新', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
selectGender(gender) {
|
||||
this.gender = gender
|
||||
},
|
||||
onBirthdayChange(e) {
|
||||
const value = e.detail.value
|
||||
const [y, m, d] = value.split('-')
|
||||
const formatted = `${y}年${m}月${d}日`
|
||||
const result = validateBirthday(formatted)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.birthday = result.value
|
||||
},
|
||||
editHeight() {
|
||||
uni.showModal({
|
||||
title: '修改身高',
|
||||
editable: true,
|
||||
placeholderText: '50-250,单位 cm',
|
||||
content: String(this.height),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateHeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.height = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
editWeight() {
|
||||
uni.showModal({
|
||||
title: '修改体重',
|
||||
editable: true,
|
||||
placeholderText: '20-300,单位 kg',
|
||||
content: String(this.weight),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateWeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.weight = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleGoal(goal) {
|
||||
const index = this.fitnessGoals.indexOf(goal)
|
||||
if (index >= 0) {
|
||||
this.fitnessGoals.splice(index, 1)
|
||||
return
|
||||
}
|
||||
const preview = [...this.fitnessGoals, goal]
|
||||
const result = validateFitnessGoals(preview, this.fitnessGoalOptions)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.fitnessGoals.push(goal)
|
||||
},
|
||||
isGoalSelected(goal) {
|
||||
return this.fitnessGoals.includes(goal)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/user-info-page.css';
|
||||
@import '@/common/style/memberInfo/pages/user-info-pixso.css';
|
||||
|
||||
.user-info-row,
|
||||
.user-info-measure {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">训练</text>
|
||||
<text class="tab-page__subtitle">记录每一次进步</text>
|
||||
</view>
|
||||
|
||||
<view class="train-cards">
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goTrainReport">
|
||||
<text class="train-card__title">训练报告</text>
|
||||
<text class="train-card__desc">查看本周/本月运动数据与趋势</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goBodyTest">
|
||||
<text class="train-card__title">智能体测</text>
|
||||
<text class="train-card__desc">连接设备,获取专业体测报告</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goBodyHistory">
|
||||
<text class="train-card__title">体测报告</text>
|
||||
<text class="train-card__desc">历史记录与对比分析</text>
|
||||
</view>
|
||||
<view class="train-card" hover-class="train-card--hover" @tap="goCheckIn">
|
||||
<text class="train-card__title">签到记录</text>
|
||||
<text class="train-card__desc">到店打卡与训练频次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
<TabBar :active="2" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
function goTrainReport() {
|
||||
navigateToPage(PAGE.TRAIN_REPORT)
|
||||
}
|
||||
|
||||
function goBodyTest() {
|
||||
navigateToPage(PAGE.BODY_TEST_HOME)
|
||||
}
|
||||
|
||||
function goBodyHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
}
|
||||
|
||||
function goCheckIn() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f0f4f8;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.train-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.train-card {
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.train-card__title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1e2a3a;
|
||||
}
|
||||
|
||||
.train-card__desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user