- 健康评分补"分"单位 - 健康评价展示等级文案(如"亚健康")替代数值 - BMI名称后跟体型状态胶囊,颜色取底部刻度色 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
// pages/detailedReport/detailedReport.ts
|
||
import { get } from '../../utils/request/index'
|
||
|
||
/** 身体成分单项指标(bodyDataList 元素) */
|
||
interface LefuBodyDatum {
|
||
/** 指标 key,如 ppBMI、ppFat、ppMuscleKgLeftArm */
|
||
bodyParamKey: string
|
||
/** 指标中文名称 */
|
||
bodyParamName: string
|
||
/** UI 渲染颜色数组 */
|
||
colorArray?: string[]
|
||
/** 当前标准等级 */
|
||
currentStandard?: number
|
||
/** 当前测量值(字符串) */
|
||
currentValue: string
|
||
/** 指标介绍 */
|
||
introduction?: string
|
||
/** 标准范围数组 */
|
||
standardArray?: number[]
|
||
/** 当前等级标题 */
|
||
standardTitle?: string
|
||
/** 标准标题数组 */
|
||
standardTitleArray?: string[]
|
||
/** 标准颜色 */
|
||
standColor?: string
|
||
/** 标准评估结果 */
|
||
standeEvaluation?: string
|
||
/** 标准建议 */
|
||
standSuggestion?: string
|
||
/** 单位 */
|
||
unit?: string
|
||
}
|
||
|
||
/** 用户信息 */
|
||
interface MeasureUserInfo {
|
||
userId?: string
|
||
realname?: string
|
||
sex?: number
|
||
height?: number
|
||
birthday?: string
|
||
avatar?: string | null
|
||
[key: string]: any
|
||
}
|
||
|
||
/** 测量历史详情 */
|
||
interface MeasureHistoryVO {
|
||
recordId?: string
|
||
/** 身高(cm) */
|
||
height?: number
|
||
/** 体重(kg) */
|
||
weight?: number
|
||
/** 与上次体重增减量(kg) */
|
||
weightChange?: number
|
||
bmi?: number
|
||
/** BMI 类型:消瘦/正常/超重/肥胖 */
|
||
bmiType?: string
|
||
standardWeight?: number
|
||
fatRate?: number
|
||
fatValue?: number
|
||
basalMetabolism?: number
|
||
/** 测量时间 */
|
||
examTime?: string
|
||
sn?: string
|
||
userInfo?: MeasureUserInfo
|
||
/** 电极类型:0=4电极,1/5=8电极 */
|
||
scaleType?: number
|
||
bodyDataList?: LefuBodyDatum[]
|
||
}
|
||
|
||
/** 查询测量历史详情(recordId 不传时返回最近一次) */
|
||
const getMeasureHistory = (params: { userId?: string; recordId?: string }) =>
|
||
get<MeasureHistoryVO>('weighingScale/v3/getMeasureHistory', params)
|
||
|
||
/** 指标行(身体成分/脂肪分析等卡片) */
|
||
interface MetricRow {
|
||
label: string
|
||
value: string
|
||
unit: string
|
||
status: string
|
||
color: string
|
||
}
|
||
|
||
/** 基础指标网格单元 */
|
||
interface BasicCell {
|
||
label: string
|
||
/** 名称后跟的状态文字(如 BMI 的"超重") */
|
||
labelExtra: string
|
||
/** 状态文字背景色 */
|
||
labelExtraColor: string
|
||
value: string
|
||
unit: string
|
||
}
|
||
|
||
/** 躯干/四肢节段单元(同时含质量与占比) */
|
||
interface BodyPart {
|
||
label: string
|
||
massValue: string
|
||
massUnit: string
|
||
rateValue: string
|
||
rateUnit: string
|
||
}
|
||
|
||
/** 指标卡片 */
|
||
interface MetricCard {
|
||
title: string
|
||
rows: MetricRow[]
|
||
}
|
||
|
||
/** 躯干/四肢卡片(每行 1~2 个节段单元) */
|
||
interface BodyPartsCard {
|
||
title: string
|
||
rows: BodyPart[][]
|
||
}
|
||
|
||
const EMPTY_BASIC_CELL: BasicCell = { label: '', labelExtra: '', labelExtraColor: '', value: '', unit: '' }
|
||
|
||
const DEFAULT_STATUS_COLOR = '#B6B6B6'
|
||
|
||
/** BMI 体型状态对应的刻度色 */
|
||
const BMI_TYPE_COLOR: Record<string, string> = {
|
||
消瘦: '#1385FA',
|
||
正常: '#2AC79F',
|
||
超重: '#FFA151',
|
||
肥胖: '#F24439',
|
||
}
|
||
|
||
/** 将 bodyDataList 转为 key -> 指标 的映射 */
|
||
const toMap = (list: LefuBodyDatum[] = []): Record<string, LefuBodyDatum> => {
|
||
const map: Record<string, LefuBodyDatum> = {}
|
||
list.forEach((d) => {
|
||
if (d && d.bodyParamKey) map[d.bodyParamKey] = d
|
||
})
|
||
return map
|
||
}
|
||
|
||
/** 按 key 取指标并转成行,缺失返回 null */
|
||
const pick = (map: Record<string, LefuBodyDatum>, key: string): MetricRow | null => {
|
||
const d = map[key]
|
||
if (!d) return null
|
||
return {
|
||
label: d.bodyParamName || key,
|
||
value: d.currentValue ?? '',
|
||
unit: d.unit || '',
|
||
status: d.standardTitle || '',
|
||
color: d.standColor || DEFAULT_STATUS_COLOR,
|
||
}
|
||
}
|
||
|
||
/** 按一组 key 构建行,自动跳过缺失项 */
|
||
const buildRows = (map: Record<string, LefuBodyDatum>, keys: string[]): MetricRow[] =>
|
||
keys.map((k) => pick(map, k)).filter((r): r is MetricRow => r !== null)
|
||
|
||
/** 构建节段单元(质量 + 占比) */
|
||
const buildPart = (
|
||
map: Record<string, LefuBodyDatum>,
|
||
label: string,
|
||
massKey: string,
|
||
rateKey: string,
|
||
): BodyPart => ({
|
||
label,
|
||
massValue: map[massKey]?.currentValue ?? '--',
|
||
massUnit: map[massKey]?.unit ?? 'kg',
|
||
rateValue: map[rateKey]?.currentValue ?? '--',
|
||
rateUnit: map[rateKey]?.unit ?? '%',
|
||
})
|
||
|
||
/** 体重拆分整数 + 小数 */
|
||
const splitWeight = (weight: number): { int: string; decimal: string } => {
|
||
const str = weight.toFixed(2)
|
||
const [int, dec] = str.split('.')
|
||
return { int, decimal: `.${dec}` }
|
||
}
|
||
|
||
/**
|
||
* 刻度轨道结构(rpx):屏宽 750 - basicIndicators 左右 padding 30*2 - 左右 border 4*2 = 682。
|
||
* BLOCK_GAP 与 less 中 basicIndicatorsTrack 的 gap: 6rpx 保持同步。
|
||
*/
|
||
const TRACK_WIDTH = 682
|
||
const BLOCK_GAP = 6
|
||
const BLOCK_WIDTH = (TRACK_WIDTH - BLOCK_GAP * 3) / 4
|
||
|
||
/** 根据 BMI 计算刻度圆圈圆心位置(百分比),对齐消瘦/正常/超重/肥胖四段分界 18.5/24/28,并计入色块间隙 */
|
||
const calcBmiLeft = (bmi?: number): number => {
|
||
if (bmi == null || isNaN(bmi)) return 0
|
||
let left: number
|
||
if (bmi < 18.5) {
|
||
left = (bmi / 18.5) * BLOCK_WIDTH
|
||
} else if (bmi < 24) {
|
||
left = BLOCK_WIDTH + BLOCK_GAP + ((bmi - 18.5) / (24 - 18.5)) * BLOCK_WIDTH
|
||
} else if (bmi < 28) {
|
||
left = BLOCK_WIDTH * 2 + BLOCK_GAP * 2 + ((bmi - 24) / (28 - 24)) * BLOCK_WIDTH
|
||
} else {
|
||
left = BLOCK_WIDTH * 3 + BLOCK_GAP * 3 + ((bmi - 28) / (40 - 28)) * BLOCK_WIDTH
|
||
}
|
||
return Math.min(Math.max(left / TRACK_WIDTH, 0), 1) * 100
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
// 导航栏背景色,根据滚动位置动态变化
|
||
navBgColor: 'rgba(255,255,255,0)',
|
||
|
||
header: {
|
||
date: '',
|
||
weightInt: '--',
|
||
weightDecimal: '',
|
||
bmiText: '',
|
||
changeText: '',
|
||
},
|
||
|
||
bmiCircleLeft: 0,
|
||
|
||
basicRows: [] as BasicCell[][],
|
||
|
||
bodyComposition: { title: '身体成分', rows: [] } as MetricCard,
|
||
fatAnalysis: { title: '脂肪分析', rows: [] } as MetricCard,
|
||
physique: { title: '体态管理', rows: [] } as MetricCard,
|
||
weightControl: { title: '体重管控', rows: [] } as MetricCard,
|
||
|
||
// 以下卡片为 8 电极专属
|
||
cellWater: { title: '细胞液分析', rows: [] } as MetricCard,
|
||
advancedComposition: { title: '进阶成分', rows: [] } as MetricCard,
|
||
bodyFat: { title: '躯干/四肢脂肪', rows: [] } as BodyPartsCard,
|
||
bodyMuscle: { title: '躯干/四肢肌肉', rows: [] } as BodyPartsCard,
|
||
},
|
||
|
||
onLoad(options: Record<string, string | undefined>) {
|
||
const recordId = options.recordId || ''
|
||
const userInfo = (wx.getStorageSync('userInfo') || {}) as Record<string, any>
|
||
const userId = options.userId || userInfo.userId || ''
|
||
this.fetchHistory(userId, recordId)
|
||
},
|
||
|
||
async fetchHistory(userId: string, recordId: string) {
|
||
if (!userId && !recordId) return
|
||
try {
|
||
const res = await getMeasureHistory({ userId, recordId })
|
||
this.applyResult(res.result)
|
||
} catch {
|
||
// 请求层已弹错误提示
|
||
}
|
||
},
|
||
|
||
applyResult(data: MeasureHistoryVO) {
|
||
const map = toMap(data.bodyDataList)
|
||
const isEight = data.scaleType === 1 || data.scaleType === 5
|
||
|
||
// 头部
|
||
const hasWeight = data.weight != null && !isNaN(data.weight)
|
||
const weight = hasWeight ? splitWeight(data.weight as number) : { int: '--', decimal: '' }
|
||
const wc = data.weightChange
|
||
const changeText =
|
||
wc != null && wc !== 0 ? `较上次${wc > 0 ? '+' : ''}${wc}kg` : ''
|
||
const header = {
|
||
date: data.examTime || '--',
|
||
weightInt: weight.int,
|
||
weightDecimal: weight.decimal,
|
||
bmiText: data.bmiType ? `BMI${data.bmiType}` : '',
|
||
changeText,
|
||
}
|
||
|
||
// 基础指标(2 行 x 3 列,第 6 格留空保持布局)
|
||
const basicKeys = ['ppWeightKg', 'ppBMI', 'ppFat', 'ppBodyScore', 'ppBodyHealth']
|
||
const basicCells: BasicCell[] = basicKeys.map((k) => {
|
||
const d = map[k]
|
||
if (!d) return { ...EMPTY_BASIC_CELL }
|
||
const isScore = k === 'ppBodyScore'
|
||
const isHealth = k === 'ppBodyHealth'
|
||
const isBmi = k === 'ppBMI'
|
||
return {
|
||
label: d.bodyParamName || k,
|
||
// BMI 名称后跟体型状态(消瘦/正常/超重/肥胖)
|
||
labelExtra: isBmi ? data.bmiType || '' : '',
|
||
labelExtraColor: isBmi ? BMI_TYPE_COLOR[data.bmiType || ''] || DEFAULT_STATUS_COLOR : '',
|
||
// 健康评价展示等级文案(如"亚健康"),其余展示测量数值
|
||
value: isHealth ? d.standardTitle || d.currentValue || '' : d.currentValue || '',
|
||
// 健康评分补"分"
|
||
unit: isScore ? d.unit || '分' : isHealth ? '' : d.unit || '',
|
||
}
|
||
})
|
||
basicCells.push({ ...EMPTY_BASIC_CELL })
|
||
const basicRows = [basicCells.slice(0, 3), basicCells.slice(3, 6)]
|
||
|
||
// 身体成分
|
||
const bodyComposition = {
|
||
title: '身体成分',
|
||
rows: buildRows(map, [
|
||
'ppWaterPercentage',
|
||
'ppWaterKg',
|
||
'ppProteinPercentage',
|
||
'ppProteinKg',
|
||
'ppBodyfatKg',
|
||
'ppMuscleKg',
|
||
'ppMusclePercentage',
|
||
'ppBodySkeletalKg',
|
||
'ppBodySkeletal',
|
||
'ppBoneKg',
|
||
]),
|
||
}
|
||
|
||
// 脂肪分析
|
||
const fatAnalysis = {
|
||
title: '脂肪分析',
|
||
rows: buildRows(map, [
|
||
'ppVisceralFat',
|
||
'ppBodyFatSubCutPercentage',
|
||
'ppBodyFatSubCutKg',
|
||
]),
|
||
}
|
||
|
||
// 体态管理(理想体重 key 随电极类型变化,推测腰臀比为 8 电极专属)
|
||
const idealWeightKey = isEight ? 'ppBodyStandardWeightKg' : 'ppIdealWeightKg'
|
||
const physiqueKeys = [idealWeightKey, 'ppLoseFatWeightKg', 'ppBodyAge', 'ppBodyType']
|
||
if (isEight) physiqueKeys.push('ppWHR')
|
||
const physique = { title: '体态管理', rows: buildRows(map, physiqueKeys) }
|
||
|
||
// 体重管控
|
||
const weightControl = {
|
||
title: '体重管控',
|
||
rows: buildRows(map, [
|
||
'ppControlWeightKg',
|
||
'ppFatControlKg',
|
||
'ppBodyMuscleControl',
|
||
]),
|
||
}
|
||
|
||
// 8 电极专属卡片
|
||
const cellWater = isEight
|
||
? { title: '细胞液分析', rows: buildRows(map, ['ppWaterICWKg', 'ppWaterECWKg']) }
|
||
: { title: '细胞液分析', rows: [] }
|
||
|
||
const advancedComposition = isEight
|
||
? {
|
||
title: '进阶成分',
|
||
rows: buildRows(map, ['ppMineralKg', 'ppCellMassKg', 'ppSmi']),
|
||
}
|
||
: { title: '进阶成分', rows: [] }
|
||
|
||
// 节段两列一行:[[左臂,右臂],[左腿,右腿],[躯干]]
|
||
const chunkParts = (parts: BodyPart[]): BodyPart[][] => [
|
||
[parts[0], parts[1]],
|
||
[parts[2], parts[3]],
|
||
[parts[4]],
|
||
]
|
||
|
||
const fatPartLabels: [string, string, string][] = [
|
||
['左臂', 'ppBodyFatKgLeftArm', 'ppBodyFatRateLeftArm'],
|
||
['右臂', 'ppBodyFatKgRightArm', 'ppBodyFatRateRightArm'],
|
||
['左腿', 'ppBodyFatKgLeftLeg', 'ppBodyFatRateLeftLeg'],
|
||
['右腿', 'ppBodyFatKgRightLeg', 'ppBodyFatRateRightLeg'],
|
||
['躯干', 'ppBodyFatKgTrunk', 'ppBodyFatRateTrunk'],
|
||
]
|
||
const bodyFat = isEight
|
||
? {
|
||
title: '躯干/四肢脂肪',
|
||
rows: chunkParts(
|
||
fatPartLabels.map(([label, massKey, rateKey]) =>
|
||
buildPart(map, label, massKey, rateKey),
|
||
),
|
||
),
|
||
}
|
||
: { title: '躯干/四肢脂肪', rows: [] }
|
||
|
||
const musclePartLabels: [string, string, string][] = [
|
||
['左臂', 'ppMuscleKgLeftArm', 'ppMuscleRateLeftArm'],
|
||
['右臂', 'ppMuscleKgRightArm', 'ppMuscleRateRightArm'],
|
||
['左腿', 'ppMuscleKgLeftLeg', 'ppMuscleRateLeftLeg'],
|
||
['右腿', 'ppMuscleKgRightLeg', 'ppMuscleRateRightLeg'],
|
||
['躯干', 'ppMuscleKgTrunk', 'ppMuscleRateTrunk'],
|
||
]
|
||
const bodyMuscle = isEight
|
||
? {
|
||
title: '躯干/四肢肌肉',
|
||
rows: chunkParts(
|
||
musclePartLabels.map(([label, massKey, rateKey]) =>
|
||
buildPart(map, label, massKey, rateKey),
|
||
),
|
||
),
|
||
}
|
||
: { title: '躯干/四肢肌肉', rows: [] }
|
||
|
||
this.setData({
|
||
header,
|
||
bmiCircleLeft: calcBmiLeft(data.bmi),
|
||
basicRows,
|
||
bodyComposition,
|
||
fatAnalysis,
|
||
physique,
|
||
weightControl,
|
||
cellWater,
|
||
advancedComposition,
|
||
bodyFat,
|
||
bodyMuscle,
|
||
})
|
||
},
|
||
|
||
// 滚动时导航栏背景渐显
|
||
onScroll(e: WechatMiniprogram.ScrollViewScroll) {
|
||
const scrollTop = e.detail.scrollTop
|
||
const opacity = Math.min(Math.max(scrollTop / 300, 0), 1)
|
||
this.setData({
|
||
navBgColor: `rgba(255,255,255,${opacity})`,
|
||
})
|
||
},
|
||
})
|