feat: 增加统计卡片

This commit is contained in:
2026-09-01 15:56:34 +08:00
parent bc6512f668
commit 1592d5adaa
6 changed files with 222 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
<template>
<view class="mt-3 flex items-center gap-3 px-1 text-[12px]">
<view class="flex-1 flex items-center gap-1.5 px-2 py-1 rounded bg-blue-50">
<view class="w-15 h-15 rounded-full bg-blue-500"></view>
<text class="text-gray-500">均值</text>
<text class="ml-auto font-semibold text-blue-600">{{ avg.toFixed(2) }}</text>
</view>
<view class="flex-1 flex items-center gap-1.5 px-2 py-1 rounded bg-red-50">
<view class="w-15 h-15 rounded-full bg-red-500"></view>
<text class="text-gray-500">最高</text>
<text class="ml-auto font-semibold text-red-500">{{ max.toFixed(2) }}</text>
</view>
<view class="flex-1 flex items-center gap-1.5 px-2 py-1 rounded bg-green-50">
<view class="w-15 h-15 rounded-full bg-green-500"></view>
<text class="text-gray-500">最低</text>
<text class="ml-auto font-semibold text-green-600">{{ min.toFixed(2) }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
const props = defineProps({
max: {
type: Number,
required: true,
},
min: {
type: Number,
required: true,
},
avg: {
type: Number,
required: true,
}
})
</script>

View File

@@ -36,10 +36,17 @@
@click="addClick(item.type)"/>
</view>
<text class="mt-2 text-base text-gray-800 font-bold tracking-tight">
<view class="mt-2 flex items-center gap-2">
<text class="text-base text-gray-800 font-bold tracking-tight">
{{ item.value }}
</text>
<text v-if="item.trend" class="text-[12px]"
:style="{ color: item.trendColor }">
{{ item.trend }}
</text>
</view>
<view class="mt-auto pt-1.5">
<text class="text-sm text-gray-400">{{ item.sub }}</text>
</view>
@@ -122,7 +129,7 @@ import TemperaturePopup from "@/components/Home/TemperaturePopup.vue";
import { ref, computed, watch } from 'vue'
import { useRecordStore } from '@/store/record'
import { useAppStore } from '@/store/app'
import { getDateStr, getDayRecords, getTimeStr } from '@/utils/record'
import { getDateStr, getDayRecords, getRecentRecords, getTimeStr } from '@/utils/record'
import type { IHealthRecord } from "@/type/record";
import { onShow } from "@dcloudio/uni-app";
import { getOverview, getPlan, getProfile } from "@/utils/profile";
@@ -231,20 +238,46 @@ const overviewItems = computed(() => {
overviewList.forEach((item) => {
if (item === 'weight') {
const profile = getProfile()
const recent = getRecentRecords('weight')
let trendValue = 0
let isTrend = false
if (recent.length > 1 && weight) {
const prev = recent[recent.length - 2].value
const curr = weight.value
trendValue = curr - prev
isTrend = true
}
const absTrend = Math.abs(trendValue).toFixed(1)
result.push({
type: 'weight',
icon: '⚖️',
label: '体重',
value: weight ? `${weight.value} kg` : '未记录',
trend: isTrend ? trendValue > 0 ? `${absTrend} kg` : `${absTrend} kg` : '',
trendColor: `${trendValue > 0 ? 'red' : 'green'}`,
sub: weight ? profile.height ? `BMI${(weight.value / Math.pow(profile.height / 100, 2)).toFixed(2)} ` : '身高未知' : '',
})
}
else if (item === 'sleep') {
const recent = getRecentRecords('sleep')
let trendValue = 0
let isTrend = false
if (recent.length > 1 && sleep) {
const prev = recent[recent.length - 2].duration
const curr = sleep.duration
trendValue = curr - prev
isTrend = true
}
const absTrend = Math.abs(trendValue).toFixed(1)
result.push({
type: 'sleep',
icon: '😴',
label: '睡眠',
value: sleep ? `${sleep.duration} h` : '未记录',
trend: isTrend ? trendValue > 0 ? `${absTrend} h` : `${absTrend} h` : '',
trendColor: `${trendValue > 0 ? 'green' : 'red'}`,
sub: sleep ? `${getTimeStr(sleep.startTime)} ~ ${getTimeStr(sleep.endTime)}` : '',
})
}
@@ -254,6 +287,8 @@ const overviewItems = computed(() => {
icon: '🏃',
label: '运动',
value: sportTotal > 0 ? `${sportTotal} min` : '未记录',
trend: '',
trendColor: '',
sub: sportTotal >= 30 ? '已达标' : '目标 30min',
})
}
@@ -263,6 +298,8 @@ const overviewItems = computed(() => {
icon: '❤️',
label: '血压',
value: bloodPressure ? `${bloodPressure.systolic}/${bloodPressure.diastolic}` : '未记录',
trend: '',
trendColor: '',
sub: bloodPressure ? `心率:${bloodPressure.heartRate}` : '',
})
}
@@ -272,15 +309,31 @@ const overviewItems = computed(() => {
icon: '🩸',
label: '血糖',
value: bloodSugar ? `${bloodSugar.value} mmol/L` : '未记录',
trend: '',
trendColor: '',
sub: bloodSugar ? `${bloodSugar.period}` : '',
})
}
else if (item === 'temperature') {
const recent = getRecentRecords('temperature')
let trendValue = 0
let isTrend = false
if (recent.length > 1 && temperature) {
const prev = recent[recent.length - 2].value
const curr = temperature.value
trendValue = curr - prev
isTrend = true
}
const absTrend = Math.abs(trendValue).toFixed(1)
result.push({
type: 'temperature',
icon: '🌡️',
label: '体温',
value: temperature ? `${temperature.value}` : '未记录',
trend: isTrend ? trendValue > 0 ? `${absTrend}` : `${absTrend}` : '',
trendColor: `${trendValue > 0 ? 'red' : 'green'}`,
sub: temperature ? `${temperature.location}` : '',
})
}
@@ -303,7 +356,6 @@ const plans = computed(() => {
const hasBloodSugar = list.some(r => r.type === 'bloodSugar')
const hasTemperature = list.some(r => r.type === 'temperature')
const planList = getPlan()
const result = [] as IPlan[]

View File

@@ -17,6 +17,9 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="weightChartData.categories?.length"
:avg="avgWeight" :min="minWeight" :max="maxWeight" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('sleep')" title="睡眠">
@@ -28,6 +31,9 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="sleepChartData.categories?.length"
:avg="avgSleep" :min="minSleep" :max="maxSleep" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('sport')" title="运动">
@@ -39,6 +45,9 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="sportChartData.categories?.length"
:avg="avgSport" :min="minSport" :max="maxSport" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('bloodPressure')" title="血压">
@@ -50,6 +59,11 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="bloodPressureChartData.categories?.length"
:avg="avgSystolic" :min="minSystolic" :max="maxSystolic" />
<summary-card v-if="bloodPressureChartData.categories?.length"
:avg="avgDiastolic" :min="minDiastolic" :max="maxDiastolic" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('bloodPressure')" title="心率">
@@ -61,6 +75,9 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="heartRateChartData.categories?.length"
:avg="avgHeartRate" :min="minHeartRate" :max="maxHeartRate" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('bloodSugar')" title="血糖">
@@ -72,6 +89,9 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="bloodSugarChartData.categories?.length"
:avg="avgBloodSugar" :min="minBloodSugar" :max="maxBloodSugar" />
</wd-card>
<wd-card v-if="profileStore.statsList.includes('temperature')" title="体温">
@@ -83,7 +103,12 @@
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
<summary-card v-if="temperatureChartData.categories?.length"
:avg="avgTemperature" :min="minTemperature" :max="maxTemperature" />
</wd-card>
<view class="h-[10px]"></view>
</view>
</template>
@@ -95,6 +120,7 @@ import { useProfileStore } from '@/store/profile'
import { useAppStore } from '@/store/app'
import { getStats } from "@/utils/profile";
import { getThemeStyle } from "@/utils/themes";
import SummaryCard from "@/components/Stats/SummaryCard.vue";
const profileStore = useProfileStore()
const appStore = useAppStore()
@@ -103,30 +129,62 @@ const weightChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxWeight = ref(0)
const minWeight = ref(0)
const avgWeight = ref(0)
const sleepChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxSleep = ref(0)
const minSleep = ref(0)
const avgSleep = ref(0)
const sportChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxSport = ref(0)
const minSport = ref(0)
const avgSport = ref(0)
const bloodPressureChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxSystolic = ref(0)
const minSystolic = ref(0)
const avgSystolic = ref(0)
const maxDiastolic = ref(0)
const minDiastolic = ref(0)
const avgDiastolic = ref(0)
const heartRateChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxHeartRate = ref(0)
const minHeartRate = ref(0)
const avgHeartRate = ref(0)
const bloodSugarChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxBloodSugar = ref(0)
const minBloodSugar = ref(0)
const avgBloodSugar = ref(0)
const temperatureChartData = ref({
categories: [] as string[],
series: [] as any[]
})
const maxTemperature = ref(0)
const minTemperature = ref(0)
const avgTemperature = ref(0)
// 图表配置
const chartOpts = {
@@ -162,6 +220,13 @@ function getWeightChartData() {
},
],
}
const data = weightChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgWeight.value = sum / data.length
maxWeight.value = Math.max(...data)
minWeight.value = Math.min(...data)
} else {
weightChartData.value = {
categories: [], series: []
@@ -184,6 +249,13 @@ function getSleepChartData() {
},
],
}
const data = sleepChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgSleep.value = sum / data.length
maxSleep.value = Math.max(...data)
minSleep.value = Math.min(...data)
} else {
sleepChartData.value = {
categories: [], series: []
@@ -206,6 +278,13 @@ function getSportChartData() {
},
],
}
const data = sportChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgSport.value = sum / data.length
maxSport.value = Math.max(...data)
minSport.value = Math.min(...data)
} else {
sportChartData.value = {
categories: [], series: []
@@ -226,6 +305,20 @@ function getBloodPressureChartData() {
{ name: '舒张压mmHg', data: list.map((r) => r.diastolic) },
],
}
const data1 = list.map((r) => r.systolic)
const sum1 = data1.reduce((a:number, b:number) => a + b, 0)
avgSystolic.value = sum1 / data1.length
maxSystolic.value = Math.max(...data1)
minSystolic.value = Math.min(...data1)
const data2 = list.map((r) => r.diastolic)
const sum2 = data2.reduce((a:number, b:number) => a + b, 0)
avgDiastolic.value = sum2 / data2.length
maxDiastolic.value = Math.max(...data2)
minDiastolic.value = Math.min(...data2)
} else {
bloodPressureChartData.value = {
categories: [], series: []
@@ -245,6 +338,13 @@ function getHeartRateChartData() {
{ name: '心率bpm', data: list.map((r) => r.heartRate) },
],
}
const data = heartRateChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgHeartRate.value = sum / data.length
maxHeartRate.value = Math.max(...data)
minHeartRate.value = Math.min(...data)
} else {
heartRateChartData.value = {
categories: [], series: []
@@ -267,6 +367,13 @@ function getBloodSugarChartData() {
},
],
}
const data = bloodSugarChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgBloodSugar.value = sum / data.length
maxBloodSugar.value = Math.max(...data)
minBloodSugar.value = Math.min(...data)
} else {
bloodSugarChartData.value = {
categories: [], series: []
@@ -289,6 +396,13 @@ function getTemperatureChartData() {
},
],
}
const data = temperatureChartData.value.series[0].data
const sum = data.reduce((a:number, b:number) => a + b, 0)
avgTemperature.value = sum / data.length
maxTemperature.value = Math.max(...data)
minTemperature.value = Math.min(...data)
} else {
temperatureChartData.value = {
categories: [], series: []

View File

@@ -3,7 +3,7 @@ import type { ITip } from "@/type";
export const useAppStore = defineStore('app', {
state: () => ({
version: '1.1.0.26090101',
version: '1.1.0.26090102',
viewRecordType: 'calendar',
points: 0,
recordCount: 0,

View File

@@ -9,6 +9,8 @@ export interface IOverview {
icon: string
label: string
value: string
trendColor: string
trend: string
sub: string
}

View File

@@ -118,6 +118,11 @@ function removeFromRecentIndex(type: string, id: string) {
}
}
/**
* 获取近期记录
* @param type 类型
* @param limit 最大记录数
*/
export function getRecentRecords(type: string, limit = 7): IHealthRecord[] {
const key = `recent_${type}`
const recent = wx.getStorageSync(key) || []