feat: 增加统计卡片
This commit is contained in:
38
src/components/Stats/SummaryCard.vue
Normal file
38
src/components/Stats/SummaryCard.vue
Normal 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>
|
||||
@@ -36,9 +36,16 @@
|
||||
@click="addClick(item.type)"/>
|
||||
</view>
|
||||
|
||||
<text class="mt-2 text-base text-gray-800 font-bold tracking-tight">
|
||||
{{ item.value }}
|
||||
</text>
|
||||
<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>
|
||||
@@ -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` : '未记录',
|
||||
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)}` : '',
|
||||
})
|
||||
}
|
||||
@@ -253,7 +286,9 @@ const overviewItems = computed(() => {
|
||||
type: 'sport',
|
||||
icon: '🏃',
|
||||
label: '运动',
|
||||
value: sportTotal > 0 ? `${sportTotal}min` : '未记录',
|
||||
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}` : '',
|
||||
})
|
||||
}
|
||||
@@ -271,16 +308,32 @@ const overviewItems = computed(() => {
|
||||
type: 'bloodSugar',
|
||||
icon: '🩸',
|
||||
label: '血糖',
|
||||
value: bloodSugar ? `${bloodSugar.value}mmol/L` : '未记录',
|
||||
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}℃` : '未记录',
|
||||
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[]
|
||||
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface IOverview {
|
||||
icon: string
|
||||
label: string
|
||||
value: string
|
||||
trendColor: string
|
||||
trend: string
|
||||
sub: string
|
||||
}
|
||||
|
||||
@@ -26,4 +28,4 @@ export interface IDefaultRecordValue {
|
||||
bloodPressureHeartRate: number
|
||||
bloodSugarValue: number
|
||||
temperatureValue: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) || []
|
||||
|
||||
Reference in New Issue
Block a user