feat: 优化AI报告显示模块
This commit is contained in:
@@ -6,14 +6,105 @@
|
||||
:close-on-click-modal="false"
|
||||
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
|
||||
>
|
||||
<view class="flex flex-col align-center gap-2 max-h-[70vh] overflow-y-auto">
|
||||
<wd-text :text="recordStore.aiResult" />
|
||||
<view class="flex flex-col align-center gap-3 max-h-[80vh] overflow-y-auto">
|
||||
<!-- ========== 标题 ========== -->
|
||||
<view class="text-center pb-[24rpx] border-b-2 border-[#f0f2f5]">
|
||||
<!-- 标题文字 -->
|
||||
<wd-text
|
||||
:text="recordStore.aiResult.title"
|
||||
type="primary"
|
||||
size="40rpx"
|
||||
bold
|
||||
custom-class="block"
|
||||
/>
|
||||
<!-- 副标题/日期 -->
|
||||
<view class="mt-[8rpx]">
|
||||
<wd-text text="—— 月度健康报告 · 为你定制 ——" size="24rpx" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="p-[24rpx_28rpx] bg-gradient-to-r from-[#f0f6ff] to-[#f8faff] rounded-[16rpx] border-l-4 border-[#4A90D9] shadow-[0_2rpx_12rpx_rgba(74,144,217,0.12)]">
|
||||
<wd-text
|
||||
:text="recordStore.aiResult.greeting"
|
||||
size="28rpx"
|
||||
line-height="48rpx"
|
||||
color="#2c3e50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- ========== 各指标卡片 ========== -->
|
||||
<view
|
||||
v-for="section in recordStore.aiResult.sections"
|
||||
:key="section.type"
|
||||
class="p-[24rpx_28rpx] rounded-[16rpx] border-l-4 flex flex-col gap-3"
|
||||
:class="[section.hasAbnormal ? 'bg-[#fdf8f7] border-[#e74c3c]' : 'bg-[#fafbfc] border-[#4A90D9]']"
|
||||
>
|
||||
<!-- 标题行:图标 + 标题 + 状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-1">
|
||||
<wd-text :text="section.emoji" size="36rpx" custom-class="mr-[12rpx]" />
|
||||
<wd-text :text="section.title" size="32rpx" bold color="#1a2a3a" />
|
||||
</view>
|
||||
<wd-tag :type="section.hasAbnormal ? 'danger' : 'primary'" round>
|
||||
{{ section.hasAbnormal ? '需关注' : '正常' }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<wd-text :text="section.data" size="28rpx" line-height="44rpx" color="#34495e" custom-class="block"/>
|
||||
|
||||
<!-- 建议 -->
|
||||
<view class="p-[14rpx_18rpx] rounded-[10rpx]" :class="[section.hasAbnormal ? 'bg-[#fef0ef]' : 'bg-[#eef5ff]']">
|
||||
<wd-text
|
||||
:text="(section.hasAbnormal ? '⚠️ ' : '💡 ') + section.suggestion"
|
||||
:type="section.hasAbnormal ? 'error' : 'primary'"
|
||||
size="26rpx"
|
||||
line-height="44rpx"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 下月健康建议 ========== -->
|
||||
<view class="flex flex-col gap-3 p-[28rpx_24rpx] bg-gradient-to-r from-[#fafffe] to-[#f5fcf9] rounded-[16rpx] border-2 border-[#e8f5ee]">
|
||||
<wd-text text="📋 健康建议" type="success" size="32rpx" bold />
|
||||
|
||||
<!-- 建议列表 -->
|
||||
<view
|
||||
v-for="(tip, index) in recordStore.aiResult.tips"
|
||||
:key="index"
|
||||
class="flex items-start py-[14rpx] border-b border-[#f0f5f2] last:border-none"
|
||||
>
|
||||
<!-- 序号圆点 -->
|
||||
<view class="shrink-0 w-[40rpx] h-[40rpx] mr-[16rpx] mt-[4rpx] bg-[#4A90D9] rounded-full text-center leading-[40rpx]">
|
||||
<wd-text :text="String(index + 1)" size="22rpx" bold color="#ffffff" />
|
||||
</view>
|
||||
<!-- 建议内容 -->
|
||||
<wd-text
|
||||
:text="tip"
|
||||
size="26rpx"
|
||||
line-height="44rpx"
|
||||
color="#2c3e50"
|
||||
custom-class="flex-1"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 结束语 ========== -->
|
||||
<view class="p-[28rpx_32rpx] bg-gradient-to-r from-[#f0f7ff] to-[#e8f0fe] rounded-[16rpx] text-center border border-[#dce8f5]">
|
||||
<wd-text
|
||||
:text="recordStore.aiResult.ending"
|
||||
size="28rpx"
|
||||
line-height="48rpx"
|
||||
color="#2c3e50"
|
||||
custom-class="block"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="flex w-full items-center justify-evenly">
|
||||
<wd-button type="info" @click="cancelClick">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button type="primary">
|
||||
<wd-button type="primary" open-type="share">
|
||||
分享
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -23,6 +114,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRecordStore } from '@/store/record'
|
||||
import { onShareAppMessage } from '@dcloudio/uni-app'
|
||||
|
||||
const recordStore = useRecordStore()
|
||||
|
||||
@@ -30,4 +122,13 @@ function cancelClick() {
|
||||
recordStore.showAIResultPopup = false
|
||||
recordStore.showAIButton = true
|
||||
}
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const id = 123
|
||||
return {
|
||||
title: recordStore.aiResult?.title,
|
||||
path: `/pages/report/index?reportId=${id}`,
|
||||
imageUrl: '/static/tabs/record-active.png'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
{
|
||||
"path": "pages/user/index",
|
||||
"style": { "navigationBarTitleText": "我的" }
|
||||
},
|
||||
{
|
||||
"path": "pages/report/index",
|
||||
"style": { "navigationBarTitleText": "健康报告" }
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
|
||||
@@ -121,7 +121,7 @@ import { useDialog } from '@wot-ui/ui'
|
||||
import AiResultPopup from "@/components/Record/AiResultPopup.vue";
|
||||
import { useRecordStore } from '@/store/record'
|
||||
import { getProfile } from "@/utils/profile";
|
||||
import { preprocessHealthData } from "@/utils/ai";
|
||||
import { generateMonthlyReport } from "@/utils/report";
|
||||
|
||||
const { onShareAppMessage } = share()
|
||||
onShareAppMessage()
|
||||
@@ -256,7 +256,7 @@ const AIClick = () => {
|
||||
recordStore.showAIButton = false
|
||||
const profile = getProfile()
|
||||
if (profile.name && profile.gender && profile.height && profile.birthYear) {
|
||||
const preprocess = JSON.stringify(preprocessHealthData(getProfile(), getMonthRecords(selectMonth.value)))
|
||||
const report = JSON.stringify(generateMonthlyReport(getMonthRecords(selectMonth.value), selectMonth.value, profile))
|
||||
dialog
|
||||
.confirm({
|
||||
title: `开始分析${selectMonth.value}记录数据,本次分析将消耗 100 积分,是否继续?`,
|
||||
@@ -264,14 +264,15 @@ const AIClick = () => {
|
||||
.then(async () => {
|
||||
try {
|
||||
recordStore.isAnalyzing = true
|
||||
recordStore.aiResult = ''
|
||||
// recordStore.aiResult = {}
|
||||
// const data = await getReport(report)
|
||||
// recordStore.aiResult = JSON.parse(data)
|
||||
|
||||
const data = await getReport(preprocess)
|
||||
recordStore.aiResult = data
|
||||
// TODO 结果上传到服务器 返回报告id
|
||||
recordStore.showAIResultPopup = true
|
||||
} catch (error) {
|
||||
recordStore.showAIButton = true
|
||||
recordStore.aiResult = ''
|
||||
recordStore.aiResult = {}
|
||||
recordStore.showAIResultPopup = false
|
||||
await dialog.alert({
|
||||
title: '提示',
|
||||
|
||||
165
src/pages/report/index.vue
Normal file
165
src/pages/report/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view class="p-[24rpx_28rpx] flex flex-col align-center gap-3 overflow-y-auto pb-[140rpx]">
|
||||
<!-- ========== 标题 ========== -->
|
||||
<view class="text-center pb-[24rpx] border-b-2 border-[#f0f2f5]">
|
||||
<!-- 标题文字 -->
|
||||
<wd-text
|
||||
:text="report.title"
|
||||
type="primary"
|
||||
size="40rpx"
|
||||
bold
|
||||
custom-class="block"
|
||||
/>
|
||||
<!-- 副标题/日期 -->
|
||||
<view class="mt-[8rpx]">
|
||||
<wd-text text="—— 月度健康报告 · 为你定制 ——" size="24rpx" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="p-[24rpx_28rpx] bg-gradient-to-r from-[#f0f6ff] to-[#f8faff] rounded-[16rpx] border-l-4 border-[#4A90D9] shadow-[0_2rpx_12rpx_rgba(74,144,217,0.12)]">
|
||||
<wd-text
|
||||
:text="report.greeting"
|
||||
size="28rpx"
|
||||
line-height="48rpx"
|
||||
color="#2c3e50"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- ========== 各指标卡片 ========== -->
|
||||
<view
|
||||
v-for="section in report.sections"
|
||||
:key="section.type"
|
||||
class="p-[24rpx_28rpx] rounded-[16rpx] border-l-4 flex flex-col gap-3"
|
||||
:class="[section.hasAbnormal ? 'bg-[#fdf8f7] border-[#e74c3c]' : 'bg-[#fafbfc] border-[#4A90D9]']"
|
||||
>
|
||||
<!-- 标题行:图标 + 标题 + 状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-1">
|
||||
<wd-text :text="section.emoji" size="36rpx" custom-class="mr-[12rpx]" />
|
||||
<wd-text :text="section.title" size="32rpx" bold color="#1a2a3a" />
|
||||
</view>
|
||||
<wd-tag :type="section.hasAbnormal ? 'danger' : 'primary'" round>
|
||||
{{ section.hasAbnormal ? '需关注' : '正常' }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<wd-text :text="section.data" size="28rpx" line-height="44rpx" color="#34495e" custom-class="block"/>
|
||||
|
||||
<!-- 建议 -->
|
||||
<view class="p-[14rpx_18rpx] rounded-[10rpx]" :class="[section.hasAbnormal ? 'bg-[#fef0ef]' : 'bg-[#eef5ff]']">
|
||||
<wd-text
|
||||
:text="(section.hasAbnormal ? '⚠️ ' : '💡 ') + section.suggestion"
|
||||
:type="section.hasAbnormal ? 'error' : 'primary'"
|
||||
size="26rpx"
|
||||
line-height="44rpx"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 下月健康建议 ========== -->
|
||||
<view class="flex flex-col gap-3 p-[28rpx_24rpx] bg-gradient-to-r from-[#fafffe] to-[#f5fcf9] rounded-[16rpx] border-2 border-[#e8f5ee]">
|
||||
<wd-text text="📋 健康建议" type="success" size="32rpx" bold />
|
||||
|
||||
<!-- 建议列表 -->
|
||||
<view
|
||||
v-for="(tip, index) in report.tips"
|
||||
:key="index"
|
||||
class="flex items-start py-[14rpx] border-b border-[#f0f5f2] last:border-none"
|
||||
>
|
||||
<!-- 序号圆点 -->
|
||||
<view class="shrink-0 w-[40rpx] h-[40rpx] mr-[16rpx] mt-[4rpx] bg-[#4A90D9] rounded-full text-center leading-[40rpx]">
|
||||
<wd-text :text="String(index + 1)" size="22rpx" bold color="#ffffff" />
|
||||
</view>
|
||||
<!-- 建议内容 -->
|
||||
<wd-text
|
||||
:text="tip"
|
||||
size="26rpx"
|
||||
line-height="44rpx"
|
||||
color="#2c3e50"
|
||||
custom-class="flex-1"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ========== 结束语 ========== -->
|
||||
<view class="p-[28rpx_32rpx] bg-gradient-to-r from-[#f0f7ff] to-[#e8f0fe] rounded-[16rpx] text-center border border-[#dce8f5]">
|
||||
<wd-text
|
||||
:text="report.ending"
|
||||
size="28rpx"
|
||||
line-height="48rpx"
|
||||
color="#2c3e50"
|
||||
custom-class="block"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="fixed bottom-0 left-0 right-0 p-[32rpx] bg-white border-t border-[#f0f2f5]">
|
||||
<wd-button type="primary" block @click="goHome">
|
||||
返回主页
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { IAiReport } from "@/type/report";
|
||||
|
||||
const report = ref<IAiReport>({
|
||||
title: "Cxx的健康月报 · 2026年9月",
|
||||
greeting: "Cxx你好!这个月你完成了3天的健康数据记录,虽然覆盖还不算全面,但坚持记录就是好的开始。当前体重约69.9kg,BMI约24.2,略高于标准上限,结合你的均衡饮食目标,可以先从调整饮食和增加活动入手。本月亮点是睡眠达标且空腹血糖正常,但血压呈现升高趋势,需要多加留意。",
|
||||
sections: [
|
||||
{
|
||||
type: "weight",
|
||||
emoji: "⚖️",
|
||||
title: "体重",
|
||||
hasAbnormal: false,
|
||||
data: "体重从70kg降至69.7kg,整体平稳,平均69.9kg,波动幅度仅0.3kg。根据身高170cm计算,BMI约24.2,略超标准范围上限。",
|
||||
suggestion: "结合均衡饮食目标,继续保持清淡饮食,减少高油高糖食物,并配合适量运动。"
|
||||
},
|
||||
{
|
||||
type: "sleep",
|
||||
emoji: "😴",
|
||||
title: "睡眠",
|
||||
hasAbnormal: false,
|
||||
data: "本月记录1次睡眠,时长8小时,达标率100%,睡眠质量表现很好。",
|
||||
suggestion: "保持规律的作息时间,尽量在固定时间就寝和起床,让好习惯延续下去。"
|
||||
},
|
||||
{
|
||||
type: "sport",
|
||||
emoji: "🏃",
|
||||
title: "运动",
|
||||
hasAbnormal: true,
|
||||
data: "运动记录7次,实际运动1天,总时长30分钟,运动频率14.3%,最长连续不运动2天。考虑到你久坐少动的状态,运动量还需要提升。",
|
||||
suggestion: "建议每周累计至少150分钟中等强度运动,可以从快走、跑步等开始,每小时起身活动5分钟。"
|
||||
},
|
||||
{
|
||||
type: "bloodPressure",
|
||||
emoji: "❤️",
|
||||
title: "血压",
|
||||
hasAbnormal: true,
|
||||
data: "收缩压从120升至145mmHg,舒张压从70升至97mmHg,平均血压约132.5/83.5mmHg,异常率50%;心率平均80次/分。血压升高明显,需要重视。",
|
||||
suggestion: "请尽快复测血压并咨询医生,同时注意低盐饮食、避免熬夜、减轻压力,并坚持每日监测。"
|
||||
},
|
||||
{
|
||||
type: "bloodSugar",
|
||||
emoji: "🩸",
|
||||
title: "血糖",
|
||||
hasAbnormal: false,
|
||||
data: "空腹血糖5.4mmol/L,处于正常范围,餐前、餐后及睡前血糖暂无记录。",
|
||||
suggestion: "继续保持均衡饮食,控制精制糖和甜食摄入,后续可完善餐后血糖记录。"
|
||||
}
|
||||
],
|
||||
tips: [
|
||||
"血压是本月的重点预警项:高压和低压均有明显上升,建议尽快就医复测,并在医生指导下规律监测。",
|
||||
"结合久坐少动的工作特点,每坐1小时就起身活动几分钟,下班后安排30分钟快走或慢跑,逐渐增加运动频率。",
|
||||
"把均衡饮食落到实处:减少盐和油,多吃蔬菜水果,少吃腌制食品和外卖,帮助体重和血压一起改善。"
|
||||
],
|
||||
ending: "健康管理是一场长跑,不必追求完美,只要持续用心就好。下个月继续一起关注血压和运动,期待你更好的状态!"
|
||||
})
|
||||
|
||||
function goHome() {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { addBloodPressure, addBloodSugar, addSleep, addSport, addTemperature, addWeight } from '@/utils/record'
|
||||
import {getDefaultRecordValue} from "@/utils/profile";
|
||||
import { getDefaultRecordValue } from "@/utils/profile";
|
||||
import type { IAiReport } from "@/type/report";
|
||||
|
||||
export const useRecordStore = defineStore('record', {
|
||||
state: () => ({
|
||||
@@ -53,7 +54,58 @@ export const useRecordStore = defineStore('record', {
|
||||
isAnalyzing: false,
|
||||
showAIResultPopup: false,
|
||||
showAIButton: true,
|
||||
aiResult: ''
|
||||
aiResult: {
|
||||
title: "Cxx的健康月报 · 2026年9月",
|
||||
greeting: "Cxx你好!这个月你完成了3天的健康数据记录,虽然覆盖还不算全面,但坚持记录就是好的开始。当前体重约69.9kg,BMI约24.2,略高于标准上限,结合你的均衡饮食目标,可以先从调整饮食和增加活动入手。本月亮点是睡眠达标且空腹血糖正常,但血压呈现升高趋势,需要多加留意。",
|
||||
sections: [
|
||||
{
|
||||
type: "weight",
|
||||
emoji: "⚖️",
|
||||
title: "体重",
|
||||
hasAbnormal: false,
|
||||
data: "体重从70kg降至69.7kg,整体平稳,平均69.9kg,波动幅度仅0.3kg。根据身高170cm计算,BMI约24.2,略超标准范围上限。",
|
||||
suggestion: "结合均衡饮食目标,继续保持清淡饮食,减少高油高糖食物,并配合适量运动。"
|
||||
},
|
||||
{
|
||||
type: "sleep",
|
||||
emoji: "😴",
|
||||
title: "睡眠",
|
||||
hasAbnormal: false,
|
||||
data: "本月记录1次睡眠,时长8小时,达标率100%,睡眠质量表现很好。",
|
||||
suggestion: "保持规律的作息时间,尽量在固定时间就寝和起床,让好习惯延续下去。"
|
||||
},
|
||||
{
|
||||
type: "sport",
|
||||
emoji: "🏃",
|
||||
title: "运动",
|
||||
hasAbnormal: true,
|
||||
data: "运动记录7次,实际运动1天,总时长30分钟,运动频率14.3%,最长连续不运动2天。考虑到你久坐少动的状态,运动量还需要提升。",
|
||||
suggestion: "建议每周累计至少150分钟中等强度运动,可以从快走、跑步等开始,每小时起身活动5分钟。"
|
||||
},
|
||||
{
|
||||
type: "bloodPressure",
|
||||
emoji: "❤️",
|
||||
title: "血压",
|
||||
hasAbnormal: true,
|
||||
data: "收缩压从120升至145mmHg,舒张压从70升至97mmHg,平均血压约132.5/83.5mmHg,异常率50%;心率平均80次/分。血压升高明显,需要重视。",
|
||||
suggestion: "请尽快复测血压并咨询医生,同时注意低盐饮食、避免熬夜、减轻压力,并坚持每日监测。"
|
||||
},
|
||||
{
|
||||
type: "bloodSugar",
|
||||
emoji: "🩸",
|
||||
title: "血糖",
|
||||
hasAbnormal: false,
|
||||
data: "空腹血糖5.4mmol/L,处于正常范围,餐前、餐后及睡前血糖暂无记录。",
|
||||
suggestion: "继续保持均衡饮食,控制精制糖和甜食摄入,后续可完善餐后血糖记录。"
|
||||
}
|
||||
],
|
||||
tips: [
|
||||
"血压是本月的重点预警项:高压和低压均有明显上升,建议尽快就医复测,并在医生指导下规律监测。",
|
||||
"结合久坐少动的工作特点,每坐1小时就起身活动几分钟,下班后安排30分钟快走或慢跑,逐渐增加运动频率。",
|
||||
"把均衡饮食落到实处:减少盐和油,多吃蔬菜水果,少吃腌制食品和外卖,帮助体重和血压一起改善。"
|
||||
],
|
||||
ending: "健康管理是一场长跑,不必追求完美,只要持续用心就好。下个月继续一起关注血压和运动,期待你更好的状态!"
|
||||
} as IAiReport
|
||||
}),
|
||||
actions: {
|
||||
openWeight() {
|
||||
|
||||
@@ -6,6 +6,43 @@ export interface IHealthRecord {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface WeightRecord extends IHealthRecord {
|
||||
type: 'weight'
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface SleepRecord extends IHealthRecord {
|
||||
type: 'sleep'
|
||||
startTime: number
|
||||
endTime: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface SportRecord extends IHealthRecord {
|
||||
type: 'sport'
|
||||
category: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface BloodPressureRecord extends IHealthRecord {
|
||||
type: 'bloodPressure'
|
||||
systolic: number
|
||||
diastolic: number
|
||||
heartRate: number
|
||||
}
|
||||
|
||||
export interface BloodSugarRecord extends IHealthRecord {
|
||||
type: 'bloodSugar'
|
||||
value: number
|
||||
period: '空腹' | '餐前' | '餐后2h' | '睡前' | '随机'
|
||||
}
|
||||
|
||||
export interface TemperatureRecord extends IHealthRecord {
|
||||
type: 'temperature'
|
||||
value: number
|
||||
location: string
|
||||
}
|
||||
|
||||
export interface IRecordMeta {
|
||||
icon: string
|
||||
label: string
|
||||
|
||||
146
src/type/report.ts
Normal file
146
src/type/report.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type {IProfile} from "@/type/profile";
|
||||
|
||||
export interface SummaryReport {
|
||||
totalDays: number
|
||||
recordDays: number
|
||||
coverageRate: number
|
||||
}
|
||||
|
||||
export interface WeightReport {
|
||||
totalRecords: number
|
||||
startValue: number
|
||||
endValue: number
|
||||
maxValue: number
|
||||
minValue: number
|
||||
avgValue: number
|
||||
netChange: number
|
||||
fluctuation: number
|
||||
}
|
||||
|
||||
export interface SleepReport {
|
||||
totalRecords: number
|
||||
maxDuration: number
|
||||
minDuration: number
|
||||
avgDuration: number
|
||||
qualifiedDays: number
|
||||
qualifiedRate: number
|
||||
}
|
||||
|
||||
export interface SportReport {
|
||||
totalRecords: number
|
||||
sportDays: number
|
||||
frequency: number
|
||||
totalDuration: number
|
||||
avgDailyDuration: number
|
||||
maxSingleDuration: number
|
||||
categoryDistribution: Record<string, number>
|
||||
maxConsecutiveNoSport: number
|
||||
}
|
||||
|
||||
export interface VitalReport {
|
||||
totalRecords: number
|
||||
startValue: number
|
||||
endValue: number
|
||||
maxValue: number
|
||||
minValue: number
|
||||
avgValue: number
|
||||
netChange: number
|
||||
normalDays?: number
|
||||
abnormalDays?: number
|
||||
abnormalRate?: number
|
||||
maxConsecutiveAbnormal?: number
|
||||
}
|
||||
|
||||
export interface BloodPressureReport {
|
||||
totalRecords: number
|
||||
systolic: VitalReport & {
|
||||
normalDays: number
|
||||
abnormalDays: number
|
||||
abnormalRate: number
|
||||
maxConsecutiveAbnormal: number
|
||||
}
|
||||
diastolic: VitalReport & {
|
||||
normalDays: number
|
||||
abnormalDays: number
|
||||
abnormalRate: number
|
||||
maxConsecutiveAbnormal: number
|
||||
}
|
||||
heartRate: VitalReport
|
||||
}
|
||||
|
||||
export interface BloodSugarPeriodReport {
|
||||
totalRecords: number
|
||||
startValue: number
|
||||
endValue: number
|
||||
maxValue: number
|
||||
minValue: number
|
||||
avgValue: number
|
||||
netChange: number
|
||||
normalCount: number
|
||||
abnormalCount: number
|
||||
abnormalRate: number
|
||||
maxConsecutiveAbnormal: number
|
||||
}
|
||||
|
||||
export interface BloodSugarReport {
|
||||
fasting: BloodSugarPeriodReport
|
||||
premeal: BloodSugarPeriodReport
|
||||
postmeal: BloodSugarPeriodReport
|
||||
bedtime: BloodSugarPeriodReport
|
||||
}
|
||||
|
||||
export interface TemperatureReport {
|
||||
totalRecords: number
|
||||
startValue: number
|
||||
endValue: number
|
||||
maxValue: number
|
||||
minValue: number
|
||||
avgValue: number
|
||||
netChange: number
|
||||
normalDays: number
|
||||
lowFeverDays: number
|
||||
highFeverDays: number
|
||||
maxConsecutiveAbnormal: number
|
||||
}
|
||||
|
||||
export interface MonthlyReport {
|
||||
userInfo: IProfile,
|
||||
month: string
|
||||
summary: SummaryReport
|
||||
weight: WeightReport | null
|
||||
sleep: SleepReport | null
|
||||
sport: SportReport | null
|
||||
bloodPressure: BloodPressureReport | null
|
||||
bloodSugar: BloodSugarReport | null
|
||||
temperature: TemperatureReport | null
|
||||
}
|
||||
|
||||
/** 报告指标项 */
|
||||
export interface IReportSection {
|
||||
/** 指标类型:weight | sleep | sport | bloodPressure | bloodSugar | temperature */
|
||||
type: string
|
||||
/** 指标对应的 Emoji 图标 */
|
||||
emoji: string
|
||||
/** 指标标题,如:体重 */
|
||||
title: string
|
||||
/** 是否存在异常 */
|
||||
hasAbnormal: boolean
|
||||
/** 指标数据描述,包含数值、变化、趋势等 */
|
||||
data: string
|
||||
/** 建议内容,1-2句话 */
|
||||
suggestion: string
|
||||
}
|
||||
|
||||
/** 完整报告数据 */
|
||||
export interface IAiReport {
|
||||
/** 报告标题,如:Cxx的健康月报 · 2026年9月 */
|
||||
title: string
|
||||
/** 开篇问候语,包含称呼、鼓励语、本月总结、BMI分析等,2-4句话 */
|
||||
greeting: string
|
||||
/** 各指标详情列表 */
|
||||
sections: IReportSection[]
|
||||
/** 下月健康建议列表,2-3条 */
|
||||
tips: string[]
|
||||
/** 结尾鼓励语,1-2句话 */
|
||||
ending: string
|
||||
}
|
||||
@@ -1,179 +1,343 @@
|
||||
import type { IHealthRecord } from "@/type/record";
|
||||
/**
|
||||
* 月度健康报告生成工具
|
||||
* 使用方法:
|
||||
* const report = generateMonthlyReport(records, '2026-09', 30)
|
||||
*/
|
||||
import type {
|
||||
BloodPressureRecord,
|
||||
BloodSugarRecord,
|
||||
IHealthRecord,
|
||||
SleepRecord,
|
||||
SportRecord, TemperatureRecord,
|
||||
WeightRecord
|
||||
} from "@/type/record";
|
||||
import type {
|
||||
BloodPressureReport,
|
||||
BloodSugarReport, MonthlyReport,
|
||||
SleepReport,
|
||||
SportReport, TemperatureReport,
|
||||
VitalReport,
|
||||
WeightReport
|
||||
} from "@/type/report";
|
||||
import type {IProfile} from "@/type/profile";
|
||||
|
||||
const avg = (arr) => arr.reduce((s, v) => s + v, 0) / arr.length
|
||||
const max = (arr) => Math.max(...arr)
|
||||
const min = (arr) => Math.min(...arr)
|
||||
const round = (v, d = 1) => Math.round(v * Math.pow(10, d)) / Math.pow(10, d)
|
||||
function getDateStringFromTimestamp(timestamp: number): string {
|
||||
const d = new Date(timestamp)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day
|
||||
}
|
||||
|
||||
function sortByTime<T extends IHealthRecord>(arr: T[]): T[] {
|
||||
return [...arr].sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
|
||||
function computeWeight(records: IHealthRecord[], height: number) {
|
||||
const list = records.filter(r => r.type === 'weight').sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0))
|
||||
function avg(arr: number[]): number {
|
||||
if (!arr || arr.length === 0) return 0
|
||||
return Math.round((arr.reduce((s, v) => s + v, 0) / arr.length) * 10) / 10
|
||||
}
|
||||
|
||||
if (!list.length) return undefined
|
||||
function max(arr: number[]): number {
|
||||
if (!arr || arr.length === 0) return 0
|
||||
return Math.max(...arr)
|
||||
}
|
||||
|
||||
const values = list.map(r => r.value)
|
||||
const first = values[0]
|
||||
const last = values[values.length - 1]
|
||||
const h = height / 100
|
||||
const firstBMI = first / (h * h)
|
||||
const lastBMI = last / (h * h)
|
||||
function min(arr: number[]): number {
|
||||
if (!arr || arr.length === 0) return 0
|
||||
return Math.min(...arr)
|
||||
}
|
||||
|
||||
// 理想体重范围:BMI 18.5 ~ 23.9
|
||||
const idealMin = round(18.5 * h * h, 1)
|
||||
const idealMax = round(23.9 * h * h, 1)
|
||||
function round1(v: number): number {
|
||||
return Math.round(v * 10) / 10
|
||||
}
|
||||
|
||||
// BMI 分类
|
||||
let bmiCategory = ''
|
||||
if (lastBMI < 18.5) bmiCategory = '偏瘦'
|
||||
else if (lastBMI < 24.0) bmiCategory = '正常'
|
||||
else if (lastBMI < 28.0) bmiCategory = '超重'
|
||||
else bmiCategory = '肥胖'
|
||||
function percent(part: number, total: number): number {
|
||||
if (total === 0) return 0
|
||||
return Math.round((part / total) * 1000) / 10
|
||||
}
|
||||
|
||||
function netChange(start: number, end: number): number {
|
||||
return round1(end - start)
|
||||
}
|
||||
|
||||
function maxConsecutive(arr: number[], isAbnormal: (v: number) => boolean): number {
|
||||
let maxCount = 0
|
||||
let current = 0
|
||||
arr.forEach(v => {
|
||||
if (isAbnormal(v)) {
|
||||
current++
|
||||
maxCount = Math.max(maxCount, current)
|
||||
} else {
|
||||
current = 0
|
||||
}
|
||||
})
|
||||
return maxCount
|
||||
}
|
||||
|
||||
function calcVitalReport(
|
||||
values: number[],
|
||||
isAbnormal: (v: number) => boolean
|
||||
): VitalReport & {
|
||||
normalDays: number
|
||||
abnormalDays: number
|
||||
abnormalRate: number
|
||||
maxConsecutiveAbnormal: number
|
||||
} {
|
||||
const len = values.length
|
||||
if (len === 0) {
|
||||
return {
|
||||
totalRecords: 0,
|
||||
startValue: 0,
|
||||
endValue: 0,
|
||||
maxValue: 0,
|
||||
minValue: 0,
|
||||
avgValue: 0,
|
||||
netChange: 0,
|
||||
normalDays: 0,
|
||||
abnormalDays: 0,
|
||||
abnormalRate: 0,
|
||||
maxConsecutiveAbnormal: 0,
|
||||
}
|
||||
}
|
||||
const abnormalValues = values.filter(v => isAbnormal(v))
|
||||
return {
|
||||
count: list.length,
|
||||
firstValue: first,
|
||||
lastValue: last,
|
||||
change: round(last - first, 1),
|
||||
avg: round(avg(values), 1),
|
||||
max: max(values),
|
||||
min: min(values),
|
||||
fluctuation: round(max(values) - min(values), 1),
|
||||
firstBMI: round(firstBMI, 1),
|
||||
lastBMI: round(lastBMI, 1),
|
||||
avgBMI: round(avg(values) / (h * h), 1),
|
||||
bmiChange: round(lastBMI - firstBMI, 1),
|
||||
bmiCategory,
|
||||
idealWeightRange: [idealMin, idealMax]
|
||||
totalRecords: len,
|
||||
startValue: values[0],
|
||||
endValue: values[len - 1],
|
||||
maxValue: max(values),
|
||||
minValue: min(values),
|
||||
avgValue: avg(values),
|
||||
netChange: netChange(values[0], values[len - 1]),
|
||||
normalDays: len - abnormalValues.length,
|
||||
abnormalDays: abnormalValues.length,
|
||||
abnormalRate: percent(abnormalValues.length, len),
|
||||
maxConsecutiveAbnormal: maxConsecutive(values, isAbnormal),
|
||||
}
|
||||
}
|
||||
|
||||
function computeSleep(records: IHealthRecord[]) {
|
||||
const list = records.filter(r => r.type === 'sleep')
|
||||
if (!list.length) return undefined
|
||||
// ============ 各指标计算函数 ============
|
||||
|
||||
const hours = list.map(r => r.duration / 60)
|
||||
const bedtimes = list.map(r => {
|
||||
const d = new Date(r.startTime || r.timestamp || 0)
|
||||
return d.getHours() * 60 + d.getMinutes()
|
||||
function calcWeightReport(records: IHealthRecord[]): WeightReport | null {
|
||||
const filtered = records.filter(r => r.type === 'weight') as WeightRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
const sorted = sortByTime(filtered)
|
||||
const values = sorted.map(r => r.value)
|
||||
return {
|
||||
totalRecords: values.length,
|
||||
startValue: values[0],
|
||||
endValue: values[values.length - 1],
|
||||
maxValue: max(values),
|
||||
minValue: min(values),
|
||||
avgValue: avg(values),
|
||||
netChange: netChange(values[0], values[values.length - 1]),
|
||||
fluctuation: round1(max(values) - min(values)),
|
||||
}
|
||||
}
|
||||
|
||||
function calcSleepReport(records: IHealthRecord[]): SleepReport | null {
|
||||
const filtered = records.filter(r => r.type === 'sleep') as SleepRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
const sorted = sortByTime(filtered)
|
||||
const values = sorted.map(r => r.duration)
|
||||
const qualified = values.filter(v => v >= 7)
|
||||
return {
|
||||
totalRecords: values.length,
|
||||
maxDuration: max(values),
|
||||
minDuration: min(values),
|
||||
avgDuration: avg(values),
|
||||
qualifiedDays: qualified.length,
|
||||
qualifiedRate: percent(qualified.length, values.length),
|
||||
}
|
||||
}
|
||||
|
||||
function calcSportReport(records: IHealthRecord[]): SportReport | null {
|
||||
const filtered = records.filter(r => r.type === 'sport') as SportRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
const sorted = sortByTime(filtered)
|
||||
const durations = sorted.map(r => r.duration)
|
||||
const totalDur = durations.reduce((s, v) => s + v, 0)
|
||||
|
||||
const categoryDist: Record<string, number> = {}
|
||||
sorted.forEach(r => {
|
||||
const cat = r.category || '其他'
|
||||
categoryDist[cat] = (categoryDist[cat] || 0) + 1
|
||||
})
|
||||
|
||||
return {
|
||||
count: list.length,
|
||||
avgDurationHours: round(avg(hours), 1),
|
||||
maxDurationHours: round(max(hours), 1),
|
||||
minDurationHours: round(min(hours), 1),
|
||||
enoughCount: hours.filter(v => v >= 7).length,
|
||||
insufficientCount: hours.filter(v => v < 6).length,
|
||||
avgBedtimeMinutes: round(avg(bedtimes), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 4. 运动 ====================
|
||||
|
||||
function computeSport(records:IHealthRecord[]) {
|
||||
const list = records.filter(r => r.type === 'sport')
|
||||
if (!list.length) return undefined
|
||||
|
||||
const totalMinutes = list.reduce((s, r) => s + (r.duration || 0), 0)
|
||||
const byCategory = {}
|
||||
list.forEach(r => { byCategory[r.category] = (byCategory[r.category] || 0) + 1 })
|
||||
|
||||
const activeDays = new Set(list.map(r => r.date || '')).size
|
||||
|
||||
return {
|
||||
count: list.length,
|
||||
totalMinutes,
|
||||
totalHours: round(totalMinutes / 60, 1),
|
||||
avgMinutes: round(totalMinutes / list.length, 0),
|
||||
activeDays,
|
||||
byCategory,
|
||||
meetsRecommendation: totalMinutes >= 150
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 5. 血压 ====================
|
||||
|
||||
function computeBloodPressure(records:IHealthRecord[]) {
|
||||
const list = records.filter(r => r.type === 'bloodPressure')
|
||||
if (!list.length) return undefined
|
||||
|
||||
const sys = list.map(r => r.systolic)
|
||||
const dia = list.map(r => r.diastolic)
|
||||
const hr = list.map(r => r.heartRate)
|
||||
const abnormal = list.filter(r => r.systolic >= 140 || r.diastolic >= 90).length
|
||||
|
||||
return {
|
||||
count: list.length,
|
||||
avgSys: round(avg(sys), 0),
|
||||
avgDia: round(avg(dia), 0),
|
||||
avgHR: round(avg(hr), 0),
|
||||
maxSys: max(sys),
|
||||
maxDia: max(dia),
|
||||
minSys: min(sys),
|
||||
minDia: min(dia),
|
||||
abnormalCount: abnormal,
|
||||
normalRate: round(1 - abnormal / list.length, 3)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 6. 血糖 ====================
|
||||
|
||||
function computeBloodSugar(records:IHealthRecord[]) {
|
||||
const list = records.filter(r => r.type === 'bloodSugar')
|
||||
if (!list.length) return undefined
|
||||
|
||||
const periodStats = {}
|
||||
const groups = {}
|
||||
|
||||
list.forEach(r => {
|
||||
groups[r.period] = groups[r.period] || []
|
||||
groups[r.period].push(r.value)
|
||||
// 最长连续不运动天数
|
||||
const allDates = new Set<string>()
|
||||
records.forEach(r => {
|
||||
allDates.add(getDateStringFromTimestamp(r.timestamp))
|
||||
})
|
||||
|
||||
Object.keys(groups).forEach(period => {
|
||||
const values = groups[period]
|
||||
const threshold = period === '空腹' ? 7.0 : 11.1
|
||||
periodStats[period] = {
|
||||
count: values.length,
|
||||
avg: round(avg(values), 1),
|
||||
abnormal: values.filter(v => v > threshold).length
|
||||
const sportDates = new Set<string>()
|
||||
sorted.forEach(r => {
|
||||
sportDates.add(getDateStringFromTimestamp(r.timestamp))
|
||||
})
|
||||
const sortedDates = [...allDates].sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
|
||||
let maxNoSport = 0
|
||||
let currentNoSport = 0
|
||||
sortedDates.forEach(date => {
|
||||
if (sportDates.has(date)) {
|
||||
currentNoSport = 0
|
||||
} else {
|
||||
currentNoSport++
|
||||
maxNoSport = Math.max(maxNoSport, currentNoSport)
|
||||
}
|
||||
})
|
||||
|
||||
const allValues = list.map(r => r.value)
|
||||
|
||||
return {
|
||||
count: list.length,
|
||||
periodStats,
|
||||
fluctuation: round(max(allValues) - min(allValues), 1),
|
||||
overallStable: round(max(allValues) - min(allValues), 1) < 3
|
||||
totalRecords: records.length,
|
||||
sportDays: sorted.length,
|
||||
frequency: percent(sorted.length, records.length),
|
||||
totalDuration: totalDur,
|
||||
avgDailyDuration: round1(totalDur / records.length),
|
||||
maxSingleDuration: max(durations),
|
||||
categoryDistribution: categoryDist,
|
||||
maxConsecutiveNoSport: maxNoSport,
|
||||
}
|
||||
}
|
||||
|
||||
function calcBloodPressureReport(records: IHealthRecord[]): BloodPressureReport | null {
|
||||
const filtered = records.filter(r => r.type === 'bloodPressure') as BloodPressureRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
const sorted = sortByTime(filtered)
|
||||
|
||||
function computeTemperature(records: IHealthRecord[]) {
|
||||
const list = records.filter(r => r.type === 'temperature')
|
||||
if (!list.length) return undefined
|
||||
|
||||
const values = list.map(r => r.value)
|
||||
const systolicValues = sorted.map(r => r.systolic)
|
||||
const diastolicValues = sorted.map(r => r.diastolic)
|
||||
const heartRateValues = sorted.map(r => r.heartRate)
|
||||
|
||||
return {
|
||||
count: list.length,
|
||||
avg: round(avg(values), 1),
|
||||
max: max(values),
|
||||
min: min(values),
|
||||
abnormalCount: values.filter(v => v >= 37.3).length,
|
||||
highFeverCount: values.filter(v => v >= 38.5).length
|
||||
totalRecords: sorted.length,
|
||||
systolic: calcVitalReport(systolicValues, v => v >= 140),
|
||||
diastolic: calcVitalReport(diastolicValues, v => v >= 90),
|
||||
heartRate: {
|
||||
totalRecords: heartRateValues.length,
|
||||
startValue: heartRateValues[0] || 0,
|
||||
endValue: heartRateValues[heartRateValues.length - 1] || 0,
|
||||
maxValue: max(heartRateValues),
|
||||
minValue: min(heartRateValues),
|
||||
avgValue: avg(heartRateValues),
|
||||
netChange: netChange(heartRateValues[0] || 0, heartRateValues[heartRateValues.length - 1] || 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMonthlyReport(records: IHealthRecord[], height: number) {
|
||||
return {
|
||||
weight: computeWeight(records, height),
|
||||
sleep: computeSleep(records),
|
||||
sport: computeSport(records),
|
||||
bloodPressure: computeBloodPressure(records),
|
||||
bloodSugar: computeBloodSugar(records),
|
||||
temperature: computeTemperature(records)
|
||||
function calcBloodSugarReport(records: IHealthRecord[]): BloodSugarReport | null {
|
||||
const filtered = records.filter(r => r.type === 'bloodSugar') as BloodSugarRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
|
||||
const periodMap: Record<string, string> = {
|
||||
'空腹': 'fasting',
|
||||
'餐前': 'premeal',
|
||||
'餐后2h': 'postmeal',
|
||||
'睡前': 'bedtime',
|
||||
}
|
||||
}
|
||||
|
||||
const normalThresholds: Record<string, number> = {
|
||||
fasting: 5.6,
|
||||
premeal: 6.1,
|
||||
postmeal: 7.8,
|
||||
bedtime: 6.7,
|
||||
}
|
||||
|
||||
const result: any = {}
|
||||
|
||||
Object.entries(periodMap).forEach(([chineseKey, englishKey]) => {
|
||||
const periodRecords = filtered.filter(r => r.period === chineseKey)
|
||||
if (periodRecords.length === 0) {
|
||||
result[englishKey] = {
|
||||
totalRecords: 0,
|
||||
startValue: 0,
|
||||
endValue: 0,
|
||||
maxValue: 0,
|
||||
minValue: 0,
|
||||
avgValue: 0,
|
||||
netChange: 0,
|
||||
normalCount: 0,
|
||||
abnormalCount: 0,
|
||||
abnormalRate: 0,
|
||||
maxConsecutiveAbnormal: 0,
|
||||
}
|
||||
return
|
||||
}
|
||||
const sorted = sortByTime(periodRecords)
|
||||
const values = sorted.map(r => r.value)
|
||||
const threshold = normalThresholds[englishKey]
|
||||
const abnormalCount = values.filter(v => v >= threshold).length
|
||||
|
||||
result[englishKey] = {
|
||||
totalRecords: values.length,
|
||||
startValue: values[0],
|
||||
endValue: values[values.length - 1],
|
||||
maxValue: max(values),
|
||||
minValue: min(values),
|
||||
avgValue: avg(values),
|
||||
netChange: netChange(values[0], values[values.length - 1]),
|
||||
normalCount: values.length - abnormalCount,
|
||||
abnormalCount: abnormalCount,
|
||||
abnormalRate: percent(abnormalCount, values.length),
|
||||
maxConsecutiveAbnormal: maxConsecutive(values, v => v >= threshold),
|
||||
}
|
||||
})
|
||||
|
||||
return result as BloodSugarReport
|
||||
}
|
||||
|
||||
function calcTemperatureReport(records: IHealthRecord[]): TemperatureReport | null {
|
||||
const filtered = records.filter(r => r.type === 'temperature') as TemperatureRecord[]
|
||||
if (filtered.length === 0) return null
|
||||
const sorted = sortByTime(filtered)
|
||||
const values = sorted.map(r => r.value)
|
||||
|
||||
const normal = values.filter(v => v >= 36.0 && v <= 37.2)
|
||||
const lowFever = values.filter(v => v >= 37.3 && v <= 38.0)
|
||||
const highFever = values.filter(v => v >= 38.1)
|
||||
|
||||
return {
|
||||
totalRecords: values.length,
|
||||
startValue: values[0],
|
||||
endValue: values[values.length - 1],
|
||||
maxValue: max(values),
|
||||
minValue: min(values),
|
||||
avgValue: avg(values),
|
||||
netChange: netChange(values[0], values[values.length - 1]),
|
||||
normalDays: normal.length,
|
||||
lowFeverDays: lowFever.length,
|
||||
highFeverDays: highFever.length,
|
||||
maxConsecutiveAbnormal: maxConsecutive(values, v => v >= 37.3),
|
||||
}
|
||||
}
|
||||
|
||||
function getDaysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成月度报告
|
||||
* @param records 记录
|
||||
* @param month 月份 YYYY-MM
|
||||
* @param userInfo 用户信息
|
||||
*/
|
||||
export function generateMonthlyReport(records: IHealthRecord[], month: string, userInfo: IProfile): MonthlyReport {
|
||||
const recordDates = new Set<string>()
|
||||
records.forEach(r => {
|
||||
recordDates.add(getDateStringFromTimestamp(r.timestamp))
|
||||
})
|
||||
const parts = month.split('-')
|
||||
const totalDays = getDaysInMonth(parseInt(parts[0]), parseInt(parts[1]))
|
||||
return {
|
||||
userInfo: userInfo,
|
||||
month,
|
||||
summary: {
|
||||
totalDays,
|
||||
recordDays: recordDates.size,
|
||||
coverageRate: percent(recordDates.size, totalDays),
|
||||
},
|
||||
weight: calcWeightReport(records),
|
||||
sleep: calcSleepReport(records),
|
||||
sport: calcSportReport(records),
|
||||
bloodPressure: calcBloodPressureReport(records),
|
||||
bloodSugar: calcBloodSugarReport(records),
|
||||
temperature: calcTemperatureReport(records),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user