// 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('weighingScale/v3/getMeasureHistory', params) /** 指标行(身体成分/脂肪分析等卡片) */ interface MetricRow { label: string value: string unit: string status: string color: string } /** 基础指标网格单元 */ interface BasicCell { label: 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: '', value: '', unit: '' } const DEFAULT_STATUS_COLOR = '#B6B6B6' /** 将 bodyDataList 转为 key -> 指标 的映射 */ const toMap = (list: LefuBodyDatum[] = []): Record => { const map: Record = {} list.forEach((d) => { if (d && d.bodyParamKey) map[d.bodyParamKey] = d }) return map } /** 按 key 取指标并转成行,缺失返回 null */ const pick = (map: Record, 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, keys: string[]): MetricRow[] => keys.map((k) => pick(map, k)).filter((r): r is MetricRow => r !== null) /** 构建节段单元(质量 + 占比) */ const buildPart = ( map: Record, 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}` } } /** 根据 BMI 计算刻度圆圈位置(百分比) */ const calcBmiLeft = (bmi?: number): number => { if (bmi == null || isNaN(bmi)) return 0 const pct = (bmi - 14) / (40 - 14) return Math.min(Math.max(pct, 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 电极专属 isEightElectrode: false, cellWater: { title: '细胞液分析', rows: [] } as MetricCard, advancedComposition: { title: '进阶成分', rows: [] } as MetricCard, bodyFat: { title: '躯干/四肢脂肪', rows: [] } as BodyPartsCard, bodyMuscle: { title: '躯干/四肢肌肉', rows: [] } as BodyPartsCard, }, onLoad(options: Record) { const recordId = options.recordId || '' const userInfo = (wx.getStorageSync('userInfo') || {}) as Record 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 } return { label: d.bodyParamName || k, value: d.currentValue ?? '', unit: 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, isEightElectrode: isEight, 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})`, }) }, })