feat:增加运动健康模块

This commit is contained in:
2025-11-04 20:05:31 +08:00
parent ed0c8e6664
commit 5f3a7ca5b4
20 changed files with 513 additions and 139 deletions

View File

@@ -1,49 +0,0 @@
import { cloudService } from './index'
import { IBudgetInfo } from '@/types/budget'
export const addBudgetApi = (budget: IBudgetInfo): Promise<any> =>
cloudService({
url: '/home-api/budget',
method: 'post',
data: budget
})
export const updateBudgetApi = (id: number, budget: IBudgetInfo): Promise<any> =>
cloudService({
url: `/home-api/budget/${id}`,
method: 'put',
data: budget
})
export const deleteBudgetApi = (id: number): Promise<any> =>
cloudService({
url: `/home-api/budget/${id}`,
method: 'DELETE'
})
export const queryBudgetApi = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget',
method: 'get',
params: { category }
})
export const queryBudgetCategory = (): Promise<any> =>
cloudService({
url: '/home-api/budget/category',
method: 'get'
})
export const queryBudgetRankStats = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget/stats/rank',
method: 'get',
params: { category }
})
export const queryBudgetProjectStats = (category: string): Promise<any> =>
cloudService({
url: '/home-api/budget/stats/project',
method: 'get',
params: { category }
})

29
src/apis/exercise.ts Normal file
View File

@@ -0,0 +1,29 @@
import { cloudService } from './index'
import { IExercise } from '@/types/health'
export const addExerciseApi = (exercise: IExercise): Promise<boolean> =>
cloudService({
url: '/home-api/exercise',
method: 'post',
data: exercise
})
export const updateExerciseApi = (id: number, exercise: IExercise): Promise<boolean> =>
cloudService({
url: `/home-api/exercise/${id}`,
method: 'put',
data: exercise
})
export const deleteExerciseApi = (id: number): Promise<boolean> =>
cloudService({
url: `/home-api/exercise/${id}`,
method: 'delete'
})
export const queryExerciseApi = (query: IExerciseQuery): Promise<IExercise[]> =>
cloudService({
url: '/home-api/exercise',
method: 'get',
params: query
})

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IPassword, IQueryPassword } from '@/types/password'
import { IPassword, IPasswordQuery } from '@/types/password'
export const addPasswordApi = (password: IPassword): Promise<boolean> =>
cloudService({
@@ -21,7 +21,7 @@ export const deletePasswordApi = (id: number): Promise<boolean> =>
method: 'delete'
})
export const queryPasswordApi = (query: IQueryPassword): Promise<IPassword[]> =>
export const queryPasswordApi = (query: IPasswordQuery): Promise<IPassword[]> =>
cloudService({
url: '/home-api/password',
method: 'get',

View File

@@ -1,5 +1,5 @@
import { cloudService } from './index'
import { IProduct, IQueryProduct } from '@/types/product.ts'
import { IProduct, IProductQuery } from '@/types/product.ts'
export const addProductApi = (product: IProduct): Promise<boolean> =>
cloudService({
@@ -27,7 +27,7 @@ export const queryProductApi = (id: number): Promise<IProduct> =>
method: 'get'
})
export const queryProductListApi = (query: IQueryProduct): Promise<IProduct[]> =>
export const queryProductListApi = (query: IProductQuery): Promise<IProduct[]> =>
cloudService({
url: '/home-api/product',
method: 'get',

29
src/apis/weight.ts Normal file
View File

@@ -0,0 +1,29 @@
import { cloudService } from './index'
import { IWeight } from '@/types/health'
export const addWeightApi = (weight: IWeight): Promise<boolean> =>
cloudService({
url: '/home-api/weight',
method: 'post',
data: weight
})
export const updateWeightApi = (id: number, weight: IWeight): Promise<boolean> =>
cloudService({
url: `/home-api/weight/${id}`,
method: 'put',
data: weight
})
export const deleteWeightApi = (id: number): Promise<boolean> =>
cloudService({
url: `/home-api/weight/${id}`,
method: 'delete'
})
export const queryWeightApi = (query: IWeightQuery): Promise<IWeight[]> =>
cloudService({
url: '/home-api/weight',
method: 'get',
params: query
})

View File

@@ -0,0 +1,100 @@
<template>
<el-form :model="healthStore.currentHealth" class="health-form card-item">
<el-form-item>
<template #label>
<i class="fa-regular fa-calendar-check"></i>
<span>日期</span>
</template>
<el-date-picker v-model="healthStore.currentHealth.date"
type="date" :disabled="true"
style="width: 150px;"/>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-person-running"></i>
<span>运动项目</span>
</template>
<el-select v-model="healthStore.currentHealth.project"
placeholder="请选择运行项目" style="width: 150px;">
<el-option v-for="(item, index) in projectList" :key="index"
:label="item" :value="item"/>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-clock"></i>
<span>运动时长</span>
</template>
<el-input-number v-model="healthStore.currentHealth.duration"
style="width: 120px;"></el-input-number>
<span style="margin-left: 5px;">分钟</span>
</el-form-item>
<el-form-item>
<template #label>
<i class="fa-solid fa-weight-scale"></i>
<span>体重</span>
</template>
<el-input-number v-model="healthStore.currentHealth.value"
style="width: 120px;"></el-input-number>
<span style="margin-left: 5px;">千克</span>
</el-form-item>
<el-form-item style="align-self: center;">
<el-button type="primary" @click="onSubmit"
:disabled="isBeforeDate(formatDate(new Date()), healthStore.selectDate)">
确认
</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { useHealthStore } from '@/store/health'
import { formatDate, isBeforeDate } from 'vue3-common/utils/dateUtil'
const healthStore = useHealthStore()
const projectList = ['羽毛球', '乒乓球', '跑步', '其他']
const onSubmit = async () => {
// await periodStore.updatePeriodDay()
// periodStore.currentPeriodDay = periodStore.getPeriodDay(periodStore.selectDate)
}
</script>
<style lang="scss">
.health-form {
padding: 10px;
flex-grow: 1;
display: flex;
flex-direction: column;
.el-form-item {
justify-content: space-between;
.el-form-item__label {
display: grid;
grid-template-columns: 20px 1fr;
align-items: center;
gap: 5px;
i {
font-size: 1.2rem;
color: #DE3163;
}
}
.el-form-item__content {
flex: none;
}
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="health-legend card-item">
<div class="legend-item">
<div class="small-circle weight"></div>
<span>体重</span>
</div>
<div class="legend-item">
<div class="small-circle exercise"></div>
<span>运动</span>
</div>
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss">
.health-legend {
padding: 10px;
display: flex;
gap: 5px;
.legend-item {
display: flex;
align-items: center;
gap: 5px;
.small-circle {
width: 6px;
height: 6px;
border-radius: 50%;
}
}
.weight {
background-color: #DC2626;
}
.exercise {
background-color: #16A34A;
}
}
</style>

View File

@@ -43,6 +43,12 @@
<span>记录</span>
</router-link>
<router-link v-if="path.includes('health')" to="/health/calendar"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>
<span>记录</span>
</router-link>
<router-link v-if="path.includes('plan')" to="/plan/calendar"
@click="routerClick()" class="item">
<i class="fa-solid fa-calendar-days"></i>

130
src/store/health.ts Normal file
View File

@@ -0,0 +1,130 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import {
addExerciseApi,
deleteExerciseApi,
queryExerciseApi,
updateExerciseApi
} from '@/apis/exercise'
import { addWeightApi, deleteWeightApi, queryWeightApi, updateWeightApi } from '@/apis/weight'
import { IExercise, IExerciseQuery, IHealth, IWeight } from '@/types/health'
export const useHealthStore = defineStore('health', {
state: () => ({
weightRecordList: [] as IWeight[],
exerciseRecordList: [] as IExercise[],
queryInfo: {
startDate: '',
endDate: ''
} as IExerciseQuery,
weightApiType: 'ADD' as IHandleApi,
exerciseApiType: 'ADD' as IHandleApi,
currentExercise: {
project: '',
duration: 0,
date: ''
} as IExercise,
currentHealth: {
project: '',
duration: 0,
value: 0,
date: ''
} as IHealth,
currentWeight: {
value: '',
date: ''
} as IWeight,
selectDate: '',
isScrollEnd: false,
isRefresh: false
}),
actions: {
async queryExerciseRecordList() {
this.exerciseRecordList = await queryExerciseApi(this.queryInfo)
},
async queryWeightRecordList() {
this.weightRecordList = await queryWeightApi(this.queryInfo)
},
async handleExerciseApi(exercise: IExercise) {
switch (this.exerciseApiType) {
case 'ADD':
await addExerciseApi(exercise)
ElMessage.success('新增锻炼记录成功')
break
case 'UPDATE':
await updateExerciseApi(exercise.id as number, exercise)
ElMessage.success('更新锻炼记录成功')
break
case 'DELETE':
await deleteExerciseApi(exercise.id as number)
ElMessage.success('删除锻炼记录成功')
break
default:
break
}
await this.refreshInfo()
},
async handleWeightApi(weight: IWeight) {
switch (this.weightApiType) {
case 'ADD':
await addWeightApi(weight)
ElMessage.success('新增体重记录成功')
break
case 'UPDATE':
await updateWeightApi(weight.id as number, weight)
ElMessage.success('更新体重记录成功')
break
case 'DELETE':
await deleteWeightApi(weight.id as number)
ElMessage.success('删除体重记录成功')
break
default:
break
}
await this.refreshInfo()
},
getHealthDay(date: string): IHealth {
const exercise = this.exerciseRecordList.find((item) => item.date === date)
const weight = this.weightRecordList.find((item) => item.date === date)
this.currentHealth = this.getEmptyHealth()
if (exercise) {
this.currentHealth.project = exercise.project
this.currentHealth.duration = exercise.duration
}
if (weight) {
this.currentHealth.value = weight.value
}
this.currentHealth.date = date
},
async refreshInfo() {
await this.queryExerciseRecordList()
await this.queryWeightRecordList()
this.isRefresh = !this.isRefresh
},
getEmptyExercise(): IExercise {
return {
project: '',
duration: 0,
date: ''
}
},
getEmptyWeight(): IWeight {
return {
value: 0,
date: ''
}
},
getEmptyHealth(): IHealth {
return {
date: '',
duration: 0,
project: '',
value: 0
}
}
}
})

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { IPassword, IQueryPassword } from '@/types/password.ts'
import { IPassword, IPasswordQuery } from '@/types/password.ts'
import {
addPasswordApi,
deletePasswordApi,
@@ -15,7 +15,7 @@ export const usePasswordStore = defineStore('password', {
queryInfo: {
category: '',
owner: '哥哥'
} as IQueryPassword,
} as IPasswordQuery,
passwordApiType: 'ADD' as IHandleApi,
categoryList: [] as string[],
currentPassword: {

View File

@@ -82,4 +82,4 @@ span, input {
font-style: normal;
}
@import "//at.alicdn.com/t/c/font_4793264_6bh098pu52x.css";
@import "//at.alicdn.com/t/c/font_4793264_jis1w9io0s8.css";

View File

@@ -1,3 +1,5 @@
export type IBillType = 'expensive' | 'income' | ''
/**
* 账单记录
*/
@@ -70,5 +72,3 @@ export interface IBillSummaryStats {
balance: number;
rate: number;
}
export type IBillType = 'expensive' | 'income' | ''

View File

@@ -1,42 +0,0 @@
/**
* 预算信息
*/
export interface IBudgetInfo {
id?: number;
/**
* 预算类别
*/
category: string;
/**
* 预算项目
*/
project: string;
/**
* 预算内容
*/
content: string;
/**
* 预算供应商
*/
vendor: string;
/**
* 预算支出
*/
budget: number;
/**
* 实际支出
*/
actual: number;
/**
* 支付时间
*/
payTime: string;
/**
* 备注
*/
remake: string;
}
export interface IBudgetQuery {
category: string;
}

View File

@@ -1,38 +1,3 @@
/**
* 菜谱信息
*/
export interface IFoodRecipe {
id?: number;
/**
* 美食名称
*/
name: string;
/**
* 美食菜系
*/
category: string;
/**
* 推荐指数
*/
recommendRate: number;
/**
* 备注
*/
remark: string;
/**
* 食材
*/
materialList: IFoodMaterial[];
/**
* 步骤
*/
stepList: IFoodStep[];
/**
* 成果
*/
recordList: IFoodRecord[];
}
/**
* 食材信息
*/
@@ -72,6 +37,41 @@ export interface IFoodStats {
workCount: number;
}
/**
* 菜谱信息
*/
export interface IFoodRecipe {
id?: number;
/**
* 美食名称
*/
name: string;
/**
* 美食菜系
*/
category: string;
/**
* 推荐指数
*/
recommendRate: number;
/**
* 备注
*/
remark: string;
/**
* 食材
*/
materialList: IFoodMaterial[];
/**
* 步骤
*/
stepList: IFoodStep[];
/**
* 成果
*/
recordList: IFoodRecord[];
}
export interface IFoodQuery {
category: string;
}

30
src/types/health.ts Normal file
View File

@@ -0,0 +1,30 @@
export interface IHealth {
id?: number;
sportProject: string;
sportDuration: number;
weight: number;
date: string;
}
export interface IExercise {
id?: number;
project: string;
duration: number;
date: string;
}
export interface IHealthQuery {
startDate: string;
endDate: string;
}
export interface IWeight {
id?: number;
value: number;
date: string;
}
export interface IWeightQuery {
startDate: string;
endDate: string;
}

View File

@@ -30,7 +30,7 @@ export interface IPassword {
remark: string;
}
export interface IQueryPassword {
export interface IPasswordQuery {
category: string;
owner: string;
}

View File

@@ -42,7 +42,7 @@ export interface IProduct {
imageUrl: string;
}
export interface IQueryProduct {
export interface IProductQuery {
category: string;
order: string;
}

View File

@@ -0,0 +1,84 @@
<template>
<div class="mobile-health-view common-mobile-view">
<v-calendar ref="calendarRef" :attributes='dailyInfo.attrs'
@dayclick="dateClick" @did-move="changeMonthClick"
:is-dark="appStore.isDark"
class="health-calendar card-item"/>
<health-legend />
<health-form />
</div>
</template>
<script lang="ts" setup>
import HealthLegend from '@/components/Health/HealthLegend.vue'
import HealthForm from '@/components/Health/HealthForm.vue'
import { CalendarDay } from 'v-calendar/dist/types/src/utils/page'
import { formatDate, getStartEndDate } from 'vue3-common/utils/dateUtil'
import { onMounted, reactive } from 'vue'
import { useHealthStore } from '@/store/health'
import { useAppStore } from '@/store/app.ts'
import { IExercise, IWeight } from '@/types/health'
const healthStore = useHealthStore()
const appStore = useAppStore()
const dailyInfo = reactive({
calendarDate: '',
attrs: [] as any[]
})
onMounted(async () => {
const result = getStartEndDate(formatDate(new Date()), 'month')
healthStore.queryInfo = { startDate: result[0], endDate: result[1] }
await healthStore.refreshInfo()
updateRecordList()
healthStore.getHealthDay(formatDate(new Date()))
})
const updateRecordList = () => {
healthStore.weightRecordList.forEach((item: IWeight) => {
dailyInfo.attrs.push({
dot: 'red',
dates: item.date
})
})
healthStore.exerciseRecordList.forEach((item: IExercise) => {
dailyInfo.attrs.push({
dot: 'green',
dates: item.date
})
})
}
const dateClick = (day: CalendarDay) => {
healthStore.selectDate = formatDate(day.date)
healthStore.getHealthDay(formatDate(day.date))
}
/**
* 选择月份事件
*/
const changeMonthClick = async (value) => {
const result = getStartEndDate(value[0].id, 'month')
healthStore.queryInfo = { startDate: result[0], endDate: result[1] }
await healthStore.refreshInfo()
}
</script>
<style lang="scss">
.mobile-health-view {
height: 100%;
display: flex;
flex-direction: column;
gap: 1vh;
.health-calendar {
justify-self: center;
align-self: center;
}
}
</style>

View File

@@ -36,6 +36,10 @@
title="物品记录" sub-title="收纳生活琐碎"
router="/product/list"/>
<module-card icon="iconfont icon-yundongjiankang" color="#FD644C"
title="运动健康" sub-title="乐享健康生活"
router="/health/calendar"/>
<module-card icon="iconfont icon-paiban" color="#9933FF"
title="日程安排" sub-title="规划每日时光"
router="/plan/calendar"/>

View File

@@ -29,7 +29,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
},
'/bill': {
title: '收支记录',
icon: 'home-bill',
icon: '',
order: 2,
redirect: '/bill/data'
},
@@ -51,7 +51,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
},
'/travel': {
title: '旅行记录',
icon: 'home-food',
icon: '',
order: 4,
redirect: '/travel/spot'
},
@@ -75,6 +75,16 @@ export const mobileHomeMeta: IRouteMetaConfig = {
title: '记录表单',
icon: ''
},
'/health': {
title: '运动健康',
icon: '',
order: 4,
redirect: '/health/calendar'
},
'/health/calendar': {
title: '记录',
icon: ''
},
'/plan': {
title: '时光日程',
icon: 'home-calendar',