feat:增加健康统计图表功能
This commit is contained in:
84
src/components/Health/HealthChart.vue
Normal file
84
src/components/Health/HealthChart.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bill-chart common-chart"
|
||||||
|
v-loading="healthStore.isLoading"
|
||||||
|
element-loading-text="正在加载中...">
|
||||||
|
<div class="chart-container card-item">
|
||||||
|
<div class="content">
|
||||||
|
<span class="title">📊体重趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart" ref="weightDailyChartRef"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container card-item">
|
||||||
|
<div class="content">
|
||||||
|
<span class="title">📊运动时长统计</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart" ref="healthDurationChartRef"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container card-item">
|
||||||
|
<div class="content">
|
||||||
|
<span class="title">📊运动类别统计</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart" ref="healthCategoryChartRef"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { lineChartOptions, barChartOptions, pieChartOptions } from 'vue3-common/utils/eChartsUtil'
|
||||||
|
import { useHealthStore } from '@/store/health'
|
||||||
|
import { useAppStore } from '@/store/app.ts'
|
||||||
|
|
||||||
|
const healthStore = useHealthStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const weightDailyChartRef = ref<HTMLElement>()
|
||||||
|
const healthDurationChartRef = ref<HTMLElement>()
|
||||||
|
const healthCategoryChartRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
let weightDailyChart: echarts.ECharts
|
||||||
|
let healthDurationChart: echarts.ECharts
|
||||||
|
let healthCategoryChart: echarts.ECharts
|
||||||
|
|
||||||
|
watch(() => healthStore.isRefresh, () => {
|
||||||
|
updateChart()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
weightDailyChart = echarts.init(weightDailyChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||||
|
healthDurationChart = echarts.init(healthDurationChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||||
|
healthCategoryChart = echarts.init(healthCategoryChartRef.value as HTMLElement, appStore.isDark ? 'dark' : null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateChart = () => {
|
||||||
|
const MAX_CATEGORY_COUNT = 5
|
||||||
|
|
||||||
|
// 体重趋势统计图
|
||||||
|
const weightXAxis = healthStore.healthRecordList.map((item) => item.date).reverse()
|
||||||
|
const weightYAxis = healthStore.healthRecordList.map((item) => item.weight).reverse()
|
||||||
|
weightDailyChart.setOption(lineChartOptions('日期', weightXAxis, '体重(千克)', weightYAxis))
|
||||||
|
|
||||||
|
// 运动量统计图
|
||||||
|
const sportRecordList = healthStore.healthRecordList.filter((item) => item.sportProject !== '')
|
||||||
|
const durationXAxis = sportRecordList.map((item) => item.date).reverse()
|
||||||
|
const durationYAxis = sportRecordList.map((item) => item.sportDuration).reverse()
|
||||||
|
healthDurationChart.setOption(barChartOptions('日期', durationXAxis, '时间(分钟)', durationYAxis))
|
||||||
|
|
||||||
|
// 类别统计图
|
||||||
|
const healthCategoryList = healthStore.healthRecordList.reduce((acc, item) => {
|
||||||
|
if (item.sportProject && item.sportDuration > 0) {
|
||||||
|
const existing = acc.find((project) => project.name === item.sportProject)
|
||||||
|
if (existing) {
|
||||||
|
existing.value += item.sportDuration
|
||||||
|
} else {
|
||||||
|
acc.push({ name: item.sportProject, value: item.sportDuration })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
healthCategoryChart.setOption(pieChartOptions('名称', '分钟', healthCategoryList.slice(0, MAX_CATEGORY_COUNT)))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
36
src/components/Health/HealthStats.vue
Normal file
36
src/components/Health/HealthStats.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div class="common-statistics">
|
||||||
|
<stats-card
|
||||||
|
title="平均体重" color="#CA6C65" unit="千克" icon="fa-solid fa-weight-scale"
|
||||||
|
:value="formatAmount(healthStats.averageWeight)" />
|
||||||
|
|
||||||
|
<stats-card
|
||||||
|
title="运动时长" color="#6FBB69" unit="分钟" icon="fa-solid fa-person-walking"
|
||||||
|
:value="healthStats.totalDuration" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import StatsCard from '@/components/StatsCard.vue'
|
||||||
|
import { useHealthStore } from '@/store/health'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||||
|
|
||||||
|
const healthStore = useHealthStore()
|
||||||
|
|
||||||
|
watch(() => healthStore.isRefresh, () => {
|
||||||
|
updateStats()
|
||||||
|
})
|
||||||
|
|
||||||
|
const healthStats = ref({
|
||||||
|
averageWeight: 0,
|
||||||
|
totalDuration: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateStats = () => {
|
||||||
|
const recordList = healthStore.healthRecordList
|
||||||
|
const totalWeight = recordList.reduce((sum, item) => sum + item.weight, 0)
|
||||||
|
healthStats.value.averageWeight = totalWeight / recordList.length
|
||||||
|
healthStats.value.totalDuration = recordList.reduce((sum, item) => sum + item.sportDuration, 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -53,6 +53,11 @@
|
|||||||
<i class="fa-solid fa-calendar-days"></i>
|
<i class="fa-solid fa-calendar-days"></i>
|
||||||
<span>记录</span>
|
<span>记录</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link v-if="path.includes('health')" to="/health/chart"
|
||||||
|
@click="routerClick()" class="item">
|
||||||
|
<i class="fa-solid fa-chart-pie"></i>
|
||||||
|
<span>统计</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<router-link v-if="path.includes('plan')" to="/plan/calendar"
|
<router-link v-if="path.includes('plan')" to="/plan/calendar"
|
||||||
@click="routerClick()" class="item">
|
@click="routerClick()" class="item">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { IHandleApi } from 'vue3-common/types'
|
|||||||
import { queryHealthApi, updateHealthApi } from '@/apis/health.ts'
|
import { queryHealthApi, updateHealthApi } from '@/apis/health.ts'
|
||||||
import { IHealth, IHealthQuery } from '@/types/health'
|
import { IHealth, IHealthQuery } from '@/types/health'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getCurrentMonth, formatDate, getStartEndDate } from 'vue3-common/utils/dateUtil'
|
||||||
|
|
||||||
export const useHealthStore = defineStore('health', {
|
export const useHealthStore = defineStore('health', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -11,6 +12,7 @@ export const useHealthStore = defineStore('health', {
|
|||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: ''
|
endDate: ''
|
||||||
} as IHealthQuery,
|
} as IHealthQuery,
|
||||||
|
queryMonth: getCurrentMonth(),
|
||||||
healthApiType: 'ADD' as IHandleApi,
|
healthApiType: 'ADD' as IHandleApi,
|
||||||
currentHealth: {
|
currentHealth: {
|
||||||
sportProject: '',
|
sportProject: '',
|
||||||
@@ -20,7 +22,8 @@ export const useHealthStore = defineStore('health', {
|
|||||||
} as IHealth,
|
} as IHealth,
|
||||||
selectDate: '',
|
selectDate: '',
|
||||||
isScrollEnd: false,
|
isScrollEnd: false,
|
||||||
isRefresh: false
|
isRefresh: false,
|
||||||
|
isLoading: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async queryHealthRecordList() {
|
async queryHealthRecordList() {
|
||||||
@@ -41,8 +44,12 @@ export const useHealthStore = defineStore('health', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async refreshInfo() {
|
async refreshInfo() {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = getStartEndDate(formatDate(this.queryMonth), 'month')
|
||||||
|
this.queryInfo = { startDate: result[0], endDate: result[1] }
|
||||||
await this.queryHealthRecordList()
|
await this.queryHealthRecordList()
|
||||||
this.isRefresh = !this.isRefresh
|
this.isRefresh = !this.isRefresh
|
||||||
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
getEmptyHealth(date: string): IHealth {
|
getEmptyHealth(date: string): IHealth {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,14 +12,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import HealthForm from '@/components/Health/HealthForm.vue'
|
import HealthForm from '@/components/Health/HealthForm.vue'
|
||||||
import { CalendarDay } from 'v-calendar/dist/types/src/utils/page'
|
import { CalendarDay } from 'v-calendar/dist/types/src/utils/page'
|
||||||
import { formatDate, getStartEndDate } from 'vue3-common/utils/dateUtil'
|
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||||
import { onMounted, reactive, watch } from 'vue'
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useHealthStore } from '@/store/health'
|
import { useHealthStore } from '@/store/health'
|
||||||
import { useAppStore } from '@/store/app.ts'
|
import { useAppStore } from '@/store/app.ts'
|
||||||
import { IHealth } from '@/types/health'
|
import { IHealth } from '@/types/health'
|
||||||
|
|
||||||
const healthStore = useHealthStore()
|
const healthStore = useHealthStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const calendarRef = ref()
|
||||||
|
|
||||||
const dailyInfo = reactive({
|
const dailyInfo = reactive({
|
||||||
calendarDate: '',
|
calendarDate: '',
|
||||||
@@ -32,10 +33,9 @@ watch(() => healthStore.isRefresh, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const result = getStartEndDate(formatDate(new Date()), 'month')
|
calendarRef.value.move(new Date(healthStore.queryMonth))
|
||||||
healthStore.queryInfo = { startDate: result[0], endDate: result[1] }
|
|
||||||
|
|
||||||
await healthStore.refreshInfo()
|
await healthStore.refreshInfo()
|
||||||
|
|
||||||
healthStore.selectDate = formatDate(new Date())
|
healthStore.selectDate = formatDate(new Date())
|
||||||
healthStore.currentHealth = healthStore.getHealthDay(formatDate(new Date()))
|
healthStore.currentHealth = healthStore.getHealthDay(formatDate(new Date()))
|
||||||
})
|
})
|
||||||
@@ -58,9 +58,7 @@ const dateClick = (day: CalendarDay) => {
|
|||||||
* 选择月份事件
|
* 选择月份事件
|
||||||
*/
|
*/
|
||||||
const changeMonthClick = async (value) => {
|
const changeMonthClick = async (value) => {
|
||||||
const result = getStartEndDate(value[0].id, 'month')
|
healthStore.queryMonth = value[0].id
|
||||||
healthStore.queryInfo = { startDate: result[0], endDate: result[1] }
|
|
||||||
|
|
||||||
await healthStore.refreshInfo()
|
await healthStore.refreshInfo()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
31
src/views/health/chart.vue
Normal file
31
src/views/health/chart.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-bill-view common-mobile-view">
|
||||||
|
<el-date-picker v-model="healthStore.queryMonth"
|
||||||
|
type="month" :editable="false" style="width: 120px;"
|
||||||
|
@change="changeDateEvent"/>
|
||||||
|
|
||||||
|
<health-stats />
|
||||||
|
<health-chart />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import HealthStats from '@/components/Health/HealthStats.vue'
|
||||||
|
import HealthChart from '@/components/Health/HealthChart.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useHealthStore } from '@/store/health'
|
||||||
|
|
||||||
|
const healthStore = useHealthStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await healthStore.refreshInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeDateEvent = async () => {
|
||||||
|
await healthStore.refreshInfo()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "@/styles/home/bill.mobile.module";
|
||||||
|
</style>
|
||||||
@@ -89,6 +89,10 @@ export const mobileHomeMeta: IRouteMetaConfig = {
|
|||||||
title: '记录',
|
title: '记录',
|
||||||
icon: ''
|
icon: ''
|
||||||
},
|
},
|
||||||
|
'/health/chart': {
|
||||||
|
title: '统计',
|
||||||
|
icon: ''
|
||||||
|
},
|
||||||
'/plan': {
|
'/plan': {
|
||||||
title: '时光日程',
|
title: '时光日程',
|
||||||
icon: 'home-calendar',
|
icon: 'home-calendar',
|
||||||
|
|||||||
Reference in New Issue
Block a user