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

This commit is contained in:
时舟年
2026-06-04 14:18:53 +08:00
committed by liwentao
parent c19e0e0181
commit f30514c700
170 changed files with 18092 additions and 35 deletions
@@ -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>