284 lines
9.5 KiB
JavaScript
284 lines
9.5 KiB
JavaScript
import { bodyTestMock } from './mockData.js'
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value))
|
|
}
|
|
|
|
function formatRecordTime(date) {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
const h = String(date.getHours()).padStart(2, '0')
|
|
const min = String(date.getMinutes()).padStart(2, '0')
|
|
return `${y}-${m}-${d} ${h}:${min}`
|
|
}
|
|
|
|
function formatIsoDate(date) {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${d}`
|
|
}
|
|
|
|
function formatTime(date) {
|
|
const h = String(date.getHours()).padStart(2, '0')
|
|
const min = String(date.getMinutes()).padStart(2, '0')
|
|
return `${h}:${min}`
|
|
}
|
|
|
|
export function getDefaultBodyTestState() {
|
|
return {
|
|
settings: { ...bodyTestMock.settings },
|
|
device: { ...bodyTestMock.device },
|
|
records: clone(bodyTestMock.records)
|
|
}
|
|
}
|
|
|
|
export function mergeBodyTestState(saved) {
|
|
const defaults = getDefaultBodyTestState()
|
|
if (!saved) return defaults
|
|
return {
|
|
settings: { ...defaults.settings, ...(saved.settings || {}) },
|
|
device: { ...defaults.device, ...(saved.device || {}) },
|
|
records: saved.records?.length ? saved.records : defaults.records
|
|
}
|
|
}
|
|
|
|
export function getLatestBodyTestRecord(store) {
|
|
const records = store.bodyTest?.records || []
|
|
return records.length ? { ...records[0] } : null
|
|
}
|
|
|
|
export function getBodyTestRecordById(store, id) {
|
|
const numId = Number(id)
|
|
const record = (store.bodyTest?.records || []).find((item) => item.id === numId)
|
|
return record ? { ...record } : null
|
|
}
|
|
|
|
export function getBodyTestHistory(store, year) {
|
|
let list = (store.bodyTest?.records || []).map((item) => ({ ...item }))
|
|
if (year && year !== 'all') {
|
|
list = list.filter((r) => r.date.startsWith(String(year)))
|
|
}
|
|
return list
|
|
}
|
|
|
|
export function getBodyTestChangeBadge(record, previous) {
|
|
if (!previous?.metrics || !record?.metrics) return null
|
|
const diff = Math.round((record.metrics.bodyFat - previous.metrics.bodyFat) * 10) / 10
|
|
if (diff === 0) return null
|
|
const sign = diff > 0 ? '+' : ''
|
|
return { key: 'bodyFat', text: `体脂率${sign}${diff}%`, good: diff < 0 }
|
|
}
|
|
|
|
export function getBodyTestYears(store) {
|
|
const years = new Set((store.bodyTest?.records || []).map((r) => r.date.slice(0, 4)))
|
|
return ['all', ...Array.from(years).sort().reverse()]
|
|
}
|
|
|
|
export function computeGrade(score) {
|
|
if (score >= 90) return { grade: 'A', gradeLabel: '优秀' }
|
|
if (score >= 80) return { grade: 'B+', gradeLabel: '良好' }
|
|
if (score >= 70) return { grade: 'B', gradeLabel: '中等' }
|
|
if (score >= 60) return { grade: 'C', gradeLabel: '一般' }
|
|
return { grade: 'D', gradeLabel: '需改善' }
|
|
}
|
|
|
|
export function computeScore(metrics) {
|
|
const bmi = metrics.bmi || 22
|
|
const bodyFat = metrics.bodyFat || 25
|
|
const muscle = metrics.muscleMass || 22
|
|
const bmiScore = bmi >= 18.5 && bmi <= 24 ? 90 : bmi >= 17 && bmi <= 27 ? 75 : 60
|
|
const fatScore = bodyFat <= 22 ? 92 : bodyFat <= 26 ? 80 : bodyFat <= 30 ? 68 : 55
|
|
const muscleScore = muscle >= 22 ? 88 : muscle >= 20 ? 76 : 62
|
|
return Math.round((bmiScore + fatScore + muscleScore) / 3)
|
|
}
|
|
|
|
export function computeChanges(current, previous) {
|
|
if (!previous?.metrics) return {}
|
|
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr', 'bodyWater', 'boneMass']
|
|
const changes = {}
|
|
keys.forEach((key) => {
|
|
const cur = Number(current.metrics[key])
|
|
const prev = Number(previous.metrics[key])
|
|
if (Number.isFinite(cur) && Number.isFinite(prev)) {
|
|
const diff = Math.round((cur - prev) * 10) / 10
|
|
changes[key] = diff
|
|
}
|
|
})
|
|
return changes
|
|
}
|
|
|
|
export function formatChangeValue(key, diff, unitSystem = 'metric') {
|
|
if (diff === undefined || diff === null) return '--'
|
|
const sign = diff > 0 ? '+' : ''
|
|
const units = {
|
|
weight: unitSystem === 'metric' ? 'kg' : 'lb',
|
|
bodyFat: '%',
|
|
muscleMass: 'kg',
|
|
bmi: '',
|
|
visceralFat: '级',
|
|
bmr: 'kcal',
|
|
bodyWater: '%',
|
|
boneMass: 'kg'
|
|
}
|
|
const unit = units[key] || ''
|
|
return `${sign}${diff}${unit}`
|
|
}
|
|
|
|
export function buildBodyReportSummary(record, previous) {
|
|
if (!record) {
|
|
return {
|
|
date: '--',
|
|
weight: '--',
|
|
bmi: '--',
|
|
bodyFat: '--',
|
|
bmr: '--',
|
|
status: '暂无数据',
|
|
change: '--'
|
|
}
|
|
}
|
|
const changes = computeChanges(record, previous)
|
|
const weightChange = changes.weight
|
|
let changeText = '--'
|
|
if (weightChange !== undefined) {
|
|
const sign = weightChange > 0 ? '+' : ''
|
|
changeText = `${sign}${weightChange}kg`
|
|
}
|
|
return {
|
|
date: record.date,
|
|
weight: String(record.metrics.weight),
|
|
bmi: String(record.metrics.bmi),
|
|
bodyFat: `${record.metrics.bodyFat}%`,
|
|
bmr: String(record.metrics.bmr),
|
|
status: record.status,
|
|
change: changeText,
|
|
recordId: record.id
|
|
}
|
|
}
|
|
|
|
export function getBodyTestTrendData(store, metricKey, limit = 6) {
|
|
const records = [...(store.bodyTest?.records || [])].reverse().slice(-limit)
|
|
return records.map((item) => ({
|
|
id: item.id,
|
|
date: item.date,
|
|
label: item.date.slice(5),
|
|
value: Number(item.metrics[metricKey]) || 0
|
|
}))
|
|
}
|
|
|
|
export function getCompareData(store, idA, idB) {
|
|
const a = getBodyTestRecordById(store, idA)
|
|
const b = getBodyTestRecordById(store, idB)
|
|
if (!a || !b) return null
|
|
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
|
const metrics = keys.map((key) => ({
|
|
key,
|
|
label: bodyTestMock.metricDefs.find((m) => m.key === key)?.label || key,
|
|
valueA: a.metrics[key],
|
|
valueB: b.metrics[key],
|
|
diff: Math.round((a.metrics[key] - b.metrics[key]) * 10) / 10
|
|
}))
|
|
return { recordA: a, recordB: b, metrics }
|
|
}
|
|
|
|
export function getRecommendedCourses(record) {
|
|
const ids = record?.recommendedCourseIds || []
|
|
return bodyTestMock.recommendedCourses.filter((c) => ids.includes(c.id))
|
|
}
|
|
|
|
export function updateBodyTestSettings(store, patch) {
|
|
store.bodyTest.settings = { ...store.bodyTest.settings, ...patch }
|
|
return store
|
|
}
|
|
|
|
export function connectBodyTestDevice(store) {
|
|
store.bodyTest.device = {
|
|
...store.bodyTest.device,
|
|
connected: true,
|
|
battery: Math.min(100, (store.bodyTest.device.battery || 80) + Math.floor(Math.random() * 5)),
|
|
lastConnected: formatRecordTime(new Date())
|
|
}
|
|
return store
|
|
}
|
|
|
|
export function disconnectBodyTestDevice(store) {
|
|
store.bodyTest.device = { ...store.bodyTest.device, connected: false }
|
|
return store
|
|
}
|
|
|
|
function nextRecordId(records) {
|
|
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
|
}
|
|
|
|
/** 模拟一次完整体测并写入记录 */
|
|
export function saveSimulatedBodyTestRecord(store, finalMetrics) {
|
|
const now = new Date()
|
|
const previous = getLatestBodyTestRecord(store)
|
|
const metrics = { ...finalMetrics }
|
|
const heightCm = Number(store.profile?.height) || 165
|
|
const heightM = heightCm / 100
|
|
metrics.bmi = Math.round((metrics.weight / (heightM * heightM)) * 10) / 10
|
|
|
|
const score = computeScore(metrics)
|
|
const { grade, gradeLabel } = computeGrade(score)
|
|
const status = score >= 80 ? '比较健康' : score >= 70 ? '需关注' : '建议改善'
|
|
|
|
const radar = {
|
|
weight: Math.min(95, Math.round(score * 0.9 + Math.random() * 5)),
|
|
bodyFat: Math.min(95, Math.round(100 - metrics.bodyFat * 2.5)),
|
|
muscle: Math.min(95, Math.round(metrics.muscleMass * 3.2)),
|
|
bone: Math.min(95, Math.round(metrics.boneMass * 32)),
|
|
water: Math.min(95, Math.round(metrics.bodyWater * 1.4)),
|
|
bmr: Math.min(95, Math.round(metrics.bmr / 16))
|
|
}
|
|
|
|
const record = {
|
|
id: nextRecordId(store.bodyTest.records),
|
|
date: formatIsoDate(now),
|
|
time: formatTime(now),
|
|
score,
|
|
grade,
|
|
gradeLabel,
|
|
status,
|
|
metrics,
|
|
radar,
|
|
bodySegments: clone(bodyTestMock.records[0].bodySegments),
|
|
advice: clone(bodyTestMock.records[0].advice),
|
|
recommendedCourseIds: [1, 2]
|
|
}
|
|
|
|
if (previous) {
|
|
record.changes = computeChanges(record, previous)
|
|
}
|
|
|
|
store.bodyTest.records.unshift(record)
|
|
store.bodyReport = buildBodyReportSummary(record, previous)
|
|
return record
|
|
}
|
|
|
|
/** 测量过程实时数据插值 */
|
|
export function interpolateMeasuringMetrics(progress, profile) {
|
|
const baseWeight = Number(profile?.weight) || 64
|
|
const target = {
|
|
weight: baseWeight - 0.3 + Math.random() * 0.2,
|
|
bodyFat: 24.5 + Math.random() * 0.8,
|
|
muscleMass: 22.4 + Math.random() * 0.3,
|
|
visceralFat: 6,
|
|
bmr: 1380 + Math.floor(Math.random() * 20),
|
|
bodyWater: 52.5 + Math.random(),
|
|
boneMass: 2.4,
|
|
protein: 16.2
|
|
}
|
|
const ratio = Math.min(1, progress / 100)
|
|
return {
|
|
weight: Math.round((baseWeight + (target.weight - baseWeight) * ratio) * 10) / 10,
|
|
bodyFat: Math.round((26 + (target.bodyFat - 26) * ratio) * 10) / 10,
|
|
muscleMass: Math.round((21.5 + (target.muscleMass - 21.5) * ratio) * 10) / 10,
|
|
bmr: Math.round(1340 + (target.bmr - 1340) * ratio),
|
|
bodyWater: Math.round((51 + (target.bodyWater - 51) * ratio) * 10) / 10
|
|
}
|
|
}
|
|
|
|
export { bodyTestMock }
|