Files
gym-manage/gym-manage-uniapp/pages/memberInfo/bodyTestReport.vue
T

329 lines
13 KiB
Vue

<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="https://gymfuture.oss-cn-chengdu.aliyuncs.com/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="https://gymfuture.oss-cn-chengdu.aliyuncs.com/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="https://gymfuture.oss-cn-chengdu.aliyuncs.com/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 setup>
import { ref, computed } from 'vue'
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
} from '@/common/memberInfo/bodyTestStore.js'
const recordId = ref(null)
const record = ref(null)
const previous = ref(null)
const chartWidth = ref(300)
const radarLabels = computed(() => {
return ['体重', 'BMI', '体脂率', '肌肉量', '内脏脂肪', '基础代谢']
})
const radarKeys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
const radarValues = computed(() => {
if (!record.value?.radar) return []
return radarKeys.map((k) => record.value.radar[k] || 0)
})
const metricDefs = [
{ key: 'weight', label: '体重', unit: 'kg' },
{ key: 'bmi', label: 'BMI' },
{ key: 'bodyFat', label: '体脂率', unit: '%' },
{ key: 'muscleMass', label: '肌肉量', unit: 'kg' },
{ key: 'visceralFat', label: '内脏脂肪', unit: '级' },
{ key: 'bmr', label: '基础代谢', unit: 'kcal' },
{ key: 'bodyWater', label: '体水分', unit: '%' },
{ key: 'boneMass', label: '骨量', unit: 'kg' }
]
const metricCards = computed(() => {
if (!record.value?.metrics) return []
const changesVal = changes.value
const defs = metricDefs.slice(0, 8)
return defs.map((def) => {
const val = record.value.metrics[def.key]
const diff = changesVal[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
}
})
})
const changes = computed(() => {
if (record.value?.changes) return record.value.changes
if (previous.value) return computeChanges(record.value, previous.value)
return {}
})
const trendPreview = computed(() => {
const store = loadMemberStore()
return getBodyTestTrendData(store, 'weight', 4)
})
const courses = computed(() => {
return getRecommendedCourses(record.value)
})
function loadRecord() {
const store = loadMemberStore()
const records = store.bodyTest.records
if (recordId.value) {
record.value = getBodyTestRecordById(store, recordId.value)
} else {
record.value = records.length ? { ...records[0] } : null
}
if (record.value) {
const idx = records.findIndex((r) => r.id === record.value.id)
previous.value = idx >= 0 && records[idx + 1] ? records[idx + 1] : null
}
}
function onBack() {
goBackOrTab(PAGE.BODY_TEST_HOME)
}
function goTrend() {
navigateToPage(`${PAGE.BODY_TEST_TREND}?metric=weight`)
}
function exportReport() {
uni.showLoading({ title: '生成中…' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '报告已保存到相册', icon: 'success' })
}, 1200)
}
function shareReport() {
uni.showActionSheet({
itemList: ['分享给微信好友', '生成分享海报', '复制报告链接'],
success: (res) => {
const msgs = ['已唤起微信分享', '海报已生成', '链接已复制']
uni.showToast({ title: msgs[res.tapIndex] || '分享成功', icon: 'none' })
}
})
}
function onCourseTap(course) {
uni.showModal({
title: course.title,
content: `${course.coach}\n${course.schedule}\n\n是否前往预约?`,
success: (res) => {
if (res.confirm) {
navigateToPage(PAGE.COURSE_LIST)
}
}
})
}
function retest() {
navigateToPage(PAGE.BODY_TEST_HOME)
}
// Initialize
chartWidth.value = uni.getSystemInfoSync().windowWidth - 64
loadRecord()
</script>
<style lang="scss">
@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>