Commit b30f907d authored by spc's avatar spc

feeding

parents 33f625f8 85c42be1
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取首页信息
* @returns
* {
* 返回值
timestamp number
非必须
当前服务器时间戳
lastRecordType number
非必须
上次喂养方式:喂养方式:0-无记录 1-母乳亲喂, 2-母乳瓶喂, 3-奶粉喂养, 4-辅食
lastBreastMilkVolume number
非必须
上次母乳瓶喂喂养量(lastRecordType=2时有值)
lastFormulaVolume number
非必须
上次奶粉瓶喂喂养量(lastRecordType=3时有值)
leftTimerRunning boolean
非必须
左边计时器是否在进行
leftTimerDuration number
非必须
有正在进行中的母乳喂养计时器:左侧累计时长(ms)
rightTimerRunning boolean
非必须
右边计时器是否在进行
rightTimerDuration number
非必须
有正在进行中的母乳喂养计时器:右侧累计时长(ms)
}
*/
export const feedingHome = (babyId) => api.get('/c/feeding/home', { babyId });
/**
*
* @param {*} recordId
* @param {*} babyId
* @param {*} recordTime
* @param {*} feedingType
* @param {*} durationLeftSeconds
* @param {*} durationRightSeconds
* @param {*} totalDurationSeconds
* @param {*} volume
* @param {*} foodDetails
* @returns
*/
export const feedingRecords = (data) => api.post('/c/feeding/records', {
recordId:data.recordId,
babyId:data.babyId,
recordTime:data.recordTime,
feedingType:data.feedingType,//1-母乳亲喂, 2-母乳瓶喂, 3-奶粉喂养, 4-辅食
durationLeftSeconds:data.durationLeftSeconds,//左侧喂养时长(秒)
durationRightSeconds:data.durationRightSeconds,//右侧喂养时长(秒)
totalDurationSeconds:data.totalDurationSeconds,//总时长(秒), 手动记录时使用
volume:data.volume,//容量(ml)
foodDetails:data.foodDetails//辅食详情
});
// 根据宝宝ID和指定日期(格式:YYYY-MM-DD)获取当天的所有喂养记录。
export const feedingRecordsByDate = (data) => api.get('/c/feeding/records', {
babyId:data.babyId,
date:data.date
});
// 获取喂养记录的日历状态
export const feedingRecordsCalendarStatus = (data) => api.get('/c/feeding/records/calendar-status', {
babyId:data.babyId,
month:data.month
});
// 获取喂养记录的统计信息
export const feedingRecordsStatisticsPeriod = (data) => api.get('/c/feeding/statistics/period', {
babyId:data.babyId,
sdate:data.sdate,
edate:data.edate
});
// 获取自定义辅食
/**
*
* @param {*} data
* @returns
* foodId integer
非必须
辅食ID
format: int64
foodName string
非必须
辅食名称
foodType number
必须
1-主食 2-蔬菜 3-水果 4-其它
*/
export const feedingFoodsCustom = (data) => api.get('/c/feeding/foods/custom');
// 添加自定义辅食
export const feedingFoodsCustomAdd = (data) => api.post('/c/feeding/foods/custom/add', {
foodName:data.foodName,
foodType:data.foodType
});
// 删除自定义辅食
export const feedingFoodsCustomDelete = (data) => api.post('/c/feeding/foods/custom/delete', {
foodIds:data.foodIds,//多个ID,逗号分隔
});
// 开始计时
export const feedingTimerStart = (data) => api.post('/c/feeding/timer/start', {
babyId:data.babyId,
side:data.side
});
export const feedingTimerStop = (data) => api.post('/c/feeding/timer/stop', {
babyId:data.babyId,
side:data.side
});
// 上传语音
/**
*
* @param {*} data
* @returns
* taskId
**/
export const feedingVoiceUpload = (data) => api.post('/c/feeding/voice/upload', {
audioData:data.audioData,//base64
});
// 获取语音识别结果
export const feedingVoiceResult = (data) => api.get('/c/feeding/voice/result', {
taskId:data.taskId,
});
// 删除自定义辅食
// export const fetchHomeJSON = () => api.get('/c/front/content',{type:'home'});
\ No newline at end of file
......@@ -17,7 +17,9 @@ const {
// 通常可以吧 baseUrl 单独放在一个 js 文件了
// const baseUrl = "http://172.16.224.178:7777/pmall";
// const baseUrl = "https://momclub-uat.feihe.com/pmall";//测试环境
const baseUrl = "https://momclub.feihe.com/pmall";//生产环境
// let baseUrl = "https://momclub.feihe.com/pmall";//生产环境
// const baseUrl = "https://docs.dui88.com/mock/1956/api";//mock
const baseUrl = "https://feihe.m.duibatest.com.cn/pmall"
const request = (options = {}) => {
// 在这里可以对请求头进行一些设置
......@@ -26,8 +28,9 @@ const request = (options = {}) => {
// "Content-Type": "application/x-www-form-urlencoded"
// }
// if(options.url == '/c/ai/chat/query'){
// baseUrl = "https://docs.dui88.com/mock/1956";
// baseUrl = "https://docs.dui88.com/mock/1956";
// }
return new Promise((resolve, reject) => {
uni
.request({
......@@ -40,7 +43,7 @@ const request = (options = {}) => {
},
})
.then((data) => {
// console.log('request data ===>', data);
console.log('request data6666666 ===>', data);
if (data.statusCode !== HTTP_STATUS.SUCCESS) {
uni.showToast({
title: data.errMsg,
......@@ -48,14 +51,16 @@ const request = (options = {}) => {
});
reject(data);
globalStore.setIsShowLoading(false);
} else if (!data.data?.ok) {
}
else if (!data.data?.ok) {
uni.showToast({
title: data.data?.message,
icon: 'none'
});
reject(data.data);
globalStore.setIsShowLoading(false);
} else {
}
else {
resolve(data.data);
}
})
......@@ -67,6 +72,7 @@ const request = (options = {}) => {
};
const get = (url, data, options = {}) => {
console.log('6666666666');
options.type = "GET";
options.data = data;
options.url = url;
......@@ -74,6 +80,8 @@ const get = (url, data, options = {}) => {
};
const post = (url, data, options = {}) => {
console.log('5555555555=', url, data, options);
options.type = "POST";
options.data = data;
options.url = url;
......
import requestModule from './request.js';
const {
api
} = requestModule;
/**
* 获取首页信息
* @returns
*/
export const growthHome = (babyId) => api.post('/c/growth/home', { babyId });
export const guideCompleted = () => api.post('/c/growth/guide/Completed');
export const assessmentSave = (data) => api.post('/c/growth/assessment/save', data);
// export const fetchHomeJSON = () => api.get('/c/front/content',{type:'home'});
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay">
<view class="popup-content" @click.stop>
<!-- 喂养方式列表 -->
<view class="feed-list">
<view
v-for="(item, index) in feedOptions"
:key="index"
class="feed-item"
:class="{ selected: selectedIndex === index }"
@click="selectFeed(index)"
>
<!-- 选中背景 -->
<image
v-if="selectedIndex === index"
class="feed-item-bg"
src="/static/shengzhangTool/changeFeed/itemBg.png"
mode="aspectFit"
/>
<!-- 喂养方式文本 -->
<text class="feed-text">{{ item.name }}</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-buttons">
<image
class="cancel-btn"
:class="{'cancel-btn-active': isCancelPressed}"
src="/static/shengzhangTool/changeFeed/cancelBtn.png"
@touchstart="handleCancelTouchStart"
@touchend="handleCancelTouchEnd"
mode="aspectFit"
/>
<image
class="ok-btn"
:class="{'ok-btn-active': isOkPressed}"
src="/static/shengzhangTool/changeFeed/okBtn.png"
@touchstart="handleOkTouchStart"
@touchend="handleOkTouchEnd"
mode="aspectFit"
/>
</view>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits, defineProps } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
selectedIndex: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update:visible', 'update:selectedIndex', 'change'])
// 喂养方式选项
const feedOptions = ref([
{ name: '纯母乳', value: 'pure_breast' },
{ name: '母乳+奶粉混合喂养', value: 'mixed_feeding' },
{ name: '纯奶粉', value: 'pure_formula' },
{ name: '奶粉+辅食', value: 'formula_food' },
{ name: '母乳+辅食', value: 'breast_food' }
])
const selectIndex = ref(props.selectedIndex)
// 按钮状态
const isCancelPressed = ref(false)
const isOkPressed = ref(false)
// 添加加载状态
const isLoadingFeed = ref(false)
// 喂养方式相关数据
const showFeedSwitchPopup = ref(false)
const currentFeedIndex = ref(1) // 默认选中"母乳+奶粉混合喂养"
const selectedFeedText = ref('母乳+奶粉混合喂养')
// 选择喂养方式
const selectFeed = (index) => {
selectIndex.value = index
emit('update:selectedIndex', index)
}
// 取消按钮事件
const handleCancelTouchStart = () => {
isCancelPressed.value = true
}
const handleCancelTouchEnd = () => {
isCancelPressed.value = false
closePopup()
}
// 确认按钮事件
const handleOkTouchStart = () => {
isOkPressed.value = true
}
const handleOkTouchEnd = () => {
isOkPressed.value = false
const index = selectIndex.value
const selectedFeed = feedOptions.value[index]
// 发送事件通知主页面
emit('change', selectedFeed, index)//只能传一个参数
closePopup()
}
// 点击喂养方式选择
const openFeedSelector = () => {
console.log('打开喂养方式选择器')
showFeedSwitchPopup.value = true
}
// 处理喂养方式选择变化
const onFeedChange = (feedOption, index) => {
isLoadingFeed.value = true
console.log('选择了喂养方式:', feedOption, index)
selectedFeedText.value = feedOption.name
currentFeedIndex.value = index
// 模拟保存数据
setTimeout(() => {
isLoadingFeed.value = false
}, 300)
}
const closePopup = () => {
emit('update:visible', false)
}
</script>
<style lang="less" scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
.popup-content {
position: absolute;
width: 750rpx;
height: 897rpx;
background-color: #f6f8fa;
overflow: hidden;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
bottom: 0rpx;
padding-top: 50rpx;
.feed-list {
flex: 1;
overflow-y: auto;
padding-left: 30rpx;
padding-right: 30rpx;
.feed-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 106rpx;
margin-bottom: 20rpx;
border-radius: 16rpx;
background-color: #fff;
&.selected {
background-color: transparent;
}
.feed-item-bg {
position: absolute;
width: 689rpx;
height: 108rpx;
z-index: 1;
}
.feed-text {
position: relative;
z-index: 2;
font-size: 28rpx;
color: #1d1e26;
font-weight: 400;
}
}
}
.bottom-buttons {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
padding-left: 30rpx;
padding-right: 30rpx;
.cancel-btn {
width: 334rpx;
height: 97rpx;
transition: transform 0.1s ease-out;
&.cancel-btn-active {
transform: scale(0.95);
}
}
.ok-btn {
width: 334rpx;
height: 97rpx;
transition: transform 0.1s ease-out;
&.ok-btn-active {
transform: scale(0.95);
}
}
}
}
}
.feeding-select {
display: flex;
align-items: center;
cursor: pointer; // 添加手型光标
.feeding-value {
font-size: 28rpx;
color: #666;
margin-right: 8rpx;
}
.dropdown-icon {
width: 20rpx;
height: 20rpx;
transition: transform 0.3s ease; // 添加旋转动画
}
// 可选:添加点击反馈效果
&:active {
opacity: 0.7;
}
}
</style>
<template>
<view v-if="visible" class="popup-overlay">
<view class="popup-content" @click.stop>
<!-- 弹窗头部 -->
<view class="popup-header">
<text class="popup-title">切换宝宝</text>
<image
class="close-btn"
src="/static/shengzhangTool/changeBaby/closeBtn.png"
mode="aspectFit"
@click="closePopup"
/>
</view>
<!-- 宝宝列表 -->
<view class="baby-list">
<view
v-for="(baby, index) in babyList"
:key="index"
class="baby-item"
:class="{ selected: selectedIndex === index }"
@click="selectBaby(index)"
>
<!-- 选中背景 -->
<image
v-if="selectedIndex === index"
class="baby-item-bg"
src="/static/shengzhangTool/changeBaby/babyItemBg.png"
mode="aspectFit"
/>
<!-- 宝宝头像 -->
<image
class="baby-avatar"
:src="baby.avatar || '/static/shengzhangTool/avatar.png'"
mode="aspectFill"
/>
<!-- 宝宝信息 -->
<view class="baby-info">
<view class="baby-name-row">
<text class="baby-name">{{ baby.name }}</text>
<image
class="gender-icon"
:src="baby.gender === 1 ? '/static/shengzhangTool/sex1.png' : '/static/shengzhangTool/sex0.png'"
mode="aspectFit"
/>
</view>
<text class="baby-birthday">宝宝生日: {{ baby.birthday }}</text>
</view>
</view>
</view>
<image
class="ok-btn"
:class="{'ok-btn-active': isOkPressed}"
src="/static/shengzhangTool/changeBaby/okBtn.png"
@touchstart="handleOkTouchStart"
@touchend="handleOkTouchEnd"
mode="aspectFit"
></image>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits, defineProps } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
babyList: {
type: Array,
default: () => []
},
selectedIndex: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update:visible', 'update:selectedIndex', 'change'])
const selectHandle = () => {
}
const isOkPressed = ref(false)
const handleOkTouchStart = () => {
isOkPressed.value = true
}
const handleOkTouchEnd = () => {
isOkPressed.value = false
const index = selectIndex.value;
// 发送事件 通知主页面
emit('change', props.babyList[index], index)
closePopup();
}
const closePopup = () => {
emit('update:visible', false)
}
const selectIndex = ref(0)
const selectBaby = (index) => {
selectIndex.value = index;
emit('update:selectedIndex', index);
}
</script>
<style lang="less" scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
// display: flex;
// align-items: center;
// justify-content: center;
.popup-content {
position: absolute;
width: 750rpx;
height: 719rpx;
background-color: #f6f8fa;
overflow: hidden;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
bottom: 0rpx;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 30rpx 20rpx;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
width: 40rpx;
height: 40rpx;
}
}
.baby-list {
flex: 1;
padding-left: 31rpx;
padding-right: 31rpx;
padding-top: 20rpx;
// padding-bottom: 20rpx;
height: 360rpx;
overflow-y: auto;
.baby-item {
position: relative;
display: flex;
align-items: center;
padding-left: 30rpx;
margin-bottom: 20rpx;
height: 129rpx;
border-radius: 16rpx;
background-color: #fff;
&.selected {
background-color: transparent;
}
.baby-item-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.baby-avatar {
position: relative;
z-index: 2;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.baby-info {
position: relative;
z-index: 2;
flex: 1;
.baby-name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.baby-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-right: 10rpx;
}
.gender-icon {
width: 24rpx;
height: 24rpx;
}
}
.baby-birthday {
font-size: 24rpx;
color: #666;
}
}
}
}
.ok-btn {
position: absolute;
width: 681rpx;
height: 98rpx;
margin-left: 35rpx;
margin-top: 0rpx;
&.ok-btn-active {
transform: scale(0.95);
}
}
}
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay">
<view class="popup-content" @click.stop>
<!-- 问号图标 -->
<view class="question-icon">
<image class="icon" src="/static/shengzhangTool/tipsPopIcon.png" mode="aspectFit"></image>
</view>
<!-- 提示内容区域 -->
<view class="tips-content">
<rich-text class="rich-content" :nodes="tipsContent"></rich-text>
</view>
<!-- 底部按钮 -->
<view class="bottom-buttons">
<image
class="ok-btn"
:class="{'ok-btn-active': isOkPressed}"
src="/static/shengzhangTool/tipsOkBtn.png"
@touchstart="handleOkTouchStart"
@touchend="handleOkTouchEnd"
mode="aspectFit"
/>
</view>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits, defineProps } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible'])
// 按钮状态
const isOkPressed = ref(false)
// 富文本内容
const tipsContent = ref(`
<div style="padding: 20rpx; line-height: 1.6;">
<div style="font-size: 32rpx; font-weight: bold; color: #d3a358;">头围测量</div>
<div style="font-size: 28rpx; font-weight: 300; color: #000000;">将软尺固定于小儿眉毛上缘,软尺紧贴头皮绕过后脑最高点即为头围的长度。</div>
<br>
<div style="font-size: 32rpx; font-weight: bold; color: #d3a358;">身高测量</div>
<div style="font-size: 28rpx; font-weight: 300; color: #000000;">3岁以下的小儿躺着测身长,让小儿躺在桌上或木板床上,按直小儿的双膝,使两下肢伸直,用软尺量取头顶到脚底(注意不是足尖)的长度,3岁以上的小儿可站着测身高。</div>
<br>
<div style="font-size: 32rpx; font-weight: bold; color: #d3a358;">体重测量</div>
<div style="font-size: 28rpx; font-weight: 300; color: #000000;">1)先用小被单将孩子兜住,用称称重,然后减去小被单及包括尿布在内的一切衣物重量,即为婴儿体重;</div>
<div style="font-size: 28rpx; font-weight: 300; color: #000000;">2)家长抱着婴儿站在磅秤上称体重,减去大人的体重,即为婴儿体重。</div>
<div style="font-size: 28rpx; font-weight: 300; color: #000000;">3)测量前最好空腹,排去大小便,或两小时内没有进食,尽量脱去衣裤、鞋帽、尿布等,仅穿单衣裤;所测得的数据应减去婴儿所穿的衣物及尿布的重量。</div>
</div>
`)
// 确认按钮事件
const handleOkTouchStart = () => {
isOkPressed.value = true
}
const handleOkTouchEnd = () => {
isOkPressed.value = false
closePopup()
}
const closePopup = () => {
emit('update:visible', false)
}
</script>
<style lang="less" scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
.popup-content {
position: absolute;
width: 661rpx;
height: 883rpx;
background-color: #ffffff;
overflow: hidden;
border-radius: 52rpx;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
.question-icon {
display: flex;
justify-content: center;
align-items: center;
margin-top: 40rpx;
.icon {
width: 80rpx;
height: 80rpx;
}
}
.tips-content {
margin-top: 20rpx;
flex: 1;
padding-left: 30rpx;
padding-right: 30rpx;
overflow-y: auto;
.rich-content {
width: 100%;
height: 100%;
/deep/ div {
margin-bottom: 20rpx;
}
/deep/ br {
display: block;
margin: 10rpx 0;
}
}
}
.bottom-buttons {
display: flex;
justify-content: center;
align-items: center;
padding: 0 40rpx 40rpx;
margin-top: 30rpx;
.ok-btn {
width: 500rpx;
height: 97rpx;
transition: transform 0.1s ease-out;
&.ok-btn-active {
transform: scale(0.95);
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay">
<view class="popup-content" @click.stop>
<!-- 标题栏 -->
<view class="header">
<text class="title">本次测评日期</text>
<view class="close-btn" @click="closePopup">
<image class="close-icon" src="/static/shengzhangTool/changeBaby/closeBtn.png" mode="aspectFit"></image>
</view>
</view>
<view class="date-picker-container">
<view class="date-picker-bg">
<view class="date-con0">
<image class="time-bg" src="/static/shengzhangTool/timeBg.png" mode="aspectFit"></image>
<text class="time-text"></text>
</view>
<view class="date-con1">
<image class="time-bg" src="/static/shengzhangTool/timeBg.png" mode="aspectFit"></image>
<text class="time-text"></text>
</view>
<view class="date-con2">
<image class="time-bg" src="/static/shengzhangTool/timeBg.png" mode="aspectFit"></image>
<text class="time-text"></text>
</view>
</view>
<!-- 日期选择器 -->
<picker-view
class="date-picker"
:value="datePickerValue"
@change="onDateChange"
:indicator-style="indicatorStyle"
indicator-class="date-picker"
mask-style="opacity: 0.5; border: none;"
>
<!-- 年份选择 -->
<picker-view-column>
<view v-for="(year, index) in yearRange" :key="index" class="picker-item">
<text class="picker-text">{{ year }}</text>
<!-- <text v-if="index === datePickerValue[0]" class="picker-label"></text> -->
</view>
</picker-view-column>
<!-- 月份选择 -->
<picker-view-column>
<view v-for="(month, index) in monthRange" :key="index" class="picker-item">
<text class="picker-text">{{ month }}</text>
<!-- <text v-if="index === datePickerValue[1]" class="picker-label"></text> -->
</view>
</picker-view-column>
<!-- 日期选择 -->
<picker-view-column>
<view v-for="(day, index) in dayRange" :key="index" class="picker-item">
<text class="picker-text">{{ day }}</text>
<!-- <text v-if="index === datePickerValue[2]" class="picker-label"></text> -->
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits, defineProps, watch, computed } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
selectedDate: {
type: String,
default: '2025-06-06'
},
babyBirthday: {
type: String,
default: '2025-06-06'
}
})
const emit = defineEmits(['update:visible', 'update:selectedDate', 'change'])
// 按钮状态
const isCancelPressed = ref(false)
const isOkPressed = ref(false)
// 日期选择器样式
const indicatorStyle = `height: 40px; border: none;`
// const maskStyle = `background: rgba(246, 248, 250, 0.8); `
// 当前选择的日期
const currentDate = ref(new Date(props.selectedDate))
const babyBirthday = ref(new Date(props.babyBirthday))
// 生成年份范围 (2024-2030)
const yearRange = ref([])
for (let i = babyBirthday.value.getFullYear(); i <= 2030; i++) {
yearRange.value.push(i)
}
// 生成月份范围 (1-12)
const monthRange = computed(() => {
const monthRange = []
let startMonth = babyBirthday.value.getMonth() + 1;
if(currentDate.value.getFullYear() == babyBirthday.value.getFullYear()){
startMonth = babyBirthday.value.getMonth() + 1;
}else{
startMonth = 1;
}
for (let i = startMonth; i <= 12; i++) {
monthRange.push(i)
}
return monthRange
})
// 计算当前月份的天数
const dayRange = computed(() => {
const year = babyBirthday.value.getFullYear()
const month = babyBirthday.value.getMonth() + 1
const daysInMonth = new Date(year, month, 0).getDate()
const days = []
// currentDate.value.getFullYear() == babyBirthday.value.getFullYear() &&
let startDay = babyBirthday.value.getDate();
if(currentDate.value.getMonth() == babyBirthday.value.getMonth() && currentDate.value.getFullYear() == babyBirthday.value.getFullYear()){
startDay = babyBirthday.value.getDate();
}else{
startDay = 1;
}
for (let i = startDay; i <= daysInMonth; i++) {
days.push(i)
}
return days
})
// 日期选择器的值
const datePickerValue = computed(() => {
const year = currentDate.value.getFullYear()
const month = currentDate.value.getMonth() + 1
const day = currentDate.value.getDate()
const yearIndex = yearRange.value.indexOf(year)
const monthIndex = monthRange.value.indexOf(month)
const dayIndex = dayRange.value.indexOf(day)
return [yearIndex, monthIndex, dayIndex]
})
// 监听 props.selectedDate 的变化
watch(() => props.selectedDate, (newVal) => {
currentDate.value = new Date(newVal)
}, { immediate: true })
// 日期选择变化处理
const onDateChange = (e) => {
console.log('e11111=', e.detail.value);
const [yearIndex, monthIndex, dayIndex] = e.detail.value
const year = yearRange.value[yearIndex]
const month = monthRange.value[monthIndex]
const day = dayRange.value[dayIndex]
// 创建新日期
const newDate = new Date(year, month - 1, day)
// 检查是否早于2024年1月1日
const minDate = new Date(babyBirthday.value)
console.log('babyBirthday=', babyBirthday.value);
if (newDate < minDate) {
return
}
currentDate.value = newDate
}
// 取消按钮事件
const handleCancelTouchStart = () => {
isCancelPressed.value = true
}
const handleCancelTouchEnd = () => {
isCancelPressed.value = false
// 格式化日期为 YYYY-MM-DD
const year = currentDate.value.getFullYear()
const month = String(currentDate.value.getMonth() + 1).padStart(2, '0')
const day = String(currentDate.value.getDate()).padStart(2, '0')
const formattedDate = `${year}-${month}-${day}`
// 发送事件通知主页面
emit('change', formattedDate)
emit('update:selectedDate', formattedDate)
closePopup()
}
// 确认按钮事件
const handleOkTouchStart = () => {
isOkPressed.value = true
}
const handleOkTouchEnd = () => {
isOkPressed.value = false
// 格式化日期为 YYYY-MM-DD
const year = currentDate.value.getFullYear()
const month = String(currentDate.value.getMonth() + 1).padStart(2, '0')
const day = String(currentDate.value.getDate()).padStart(2, '0')
const formattedDate = `${year}-${month}-${day}`
// 发送事件通知主页面
emit('change', formattedDate)
emit('update:selectedDate', formattedDate)
closePopup()
}
const closePopup = () => {
// 格式化日期为 YYYY-MM-DD
const year = currentDate.value.getFullYear()
const month = String(currentDate.value.getMonth() + 1).padStart(2, '0')
const day = String(currentDate.value.getDate()).padStart(2, '0')
const formattedDate = `${year}-${month}-${day}`
console.log('formattedDate=', formattedDate);
// 发送事件通知主页面
emit('change', formattedDate)
emit('update:selectedDate', formattedDate);
emit('update:visible', false)
}
</script>
<style lang="less" scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
.popup-content {
position: absolute;
width: 750rpx;
height: 564rpx;
background-color: #f6f8fa;
overflow: hidden;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
bottom: 0rpx;
padding-top: 40rpx;
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 30rpx;
margin-left: 30rpx;
.title {
font-size: 36rpx;
font-weight: 400;
color: #1d1e26;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
}
.close-icon {
width: 40rpx;
height: 40rpx;
}
}
.date-picker-container {
position: relative;
width: 100%;
margin-top: 100rpx;
.date-picker-bg{
position: absolute;
top: 96rpx;
left: 0;
width: 100%;
height: 60rpx;
.date-con0{
position: absolute;
// width: 122rpx;
height: 100%;
left: 64rpx;
.time-bg{
position: absolute;
width: 122rpx;
height: 51rpx;
top:50%;
transform: translateY(-50%);
}
.time-text{
position: absolute;
font-size: 28rpx;
color: #1d1e26;
font-weight: bold;
left: 135rpx;
top:50%;
transform: translateY(-50%);
}
}
.date-con1{
position: absolute;
// width: 122rpx;
height: 100%;
left: 313rpx;
.time-bg{
position: absolute;
width: 122rpx;
height: 51rpx;
top:50%;
transform: translateY(-50%);
}
.time-text{
position: absolute;
font-size: 28rpx;
color: #1d1e26;
font-weight: bold;
left: 135rpx;
top:50%;
transform: translateY(-50%);
}
}
.date-con2{
position: absolute;
// width: 122rpx;
height: 100%;
left: 562rpx;
.time-bg{
position: absolute;
top:50%;
transform: translateY(-50%);
width: 122rpx;
height: 51rpx;
}
.time-text{
position: absolute;
font-size: 28rpx;
color: #1d1e26;
font-weight: bold;
left: 135rpx;
top:50%;
transform: translateY(-50%);
}
}
}
.date-picker {
position: relative;
z-index: 2;
width: 100%;
height: 250rpx;
background: transparent;
:deep(.date-picker::before) {
content: none;
}
:deep(.date-picker::after) {
content: none;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 40rpx;
line-height: 40rpx;
.picker-text {
font-size: 32rpx;
color: #1d1e26;
font-weight: bold;
}
.picker-label {
font-size: 28rpx;
color: #1d1e26;
font-weight: bold;
margin-left: 8rpx;
}
}
}
}
}
}
</style>
\ No newline at end of file
......@@ -87,6 +87,12 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/shengzhangTools/shengzhangTools",
"style": {
"navigationStyle": "custom"
}
},
{
"path" : "pages/feedingAnalysis/feedingAnalysis",
"style" :
......@@ -101,6 +107,12 @@
{
"navigationBarTitleText" : "添加喂养记录"
}
},
{
"path": "pages/shengzhangTestResult/shengzhangTestResult",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
......
......@@ -43,6 +43,7 @@
:key="index"
class="chart-bar-wrapper"
:class="{ active: day.isActive }"
@click="onBarClick(day)"
>
<view class="chart-bar">
<view
......@@ -69,13 +70,16 @@
<!-- 宝宝年龄 -->
<view class="baby-age">
<text>宝宝 5个月20天</text>
<text>宝宝 {{ calculateBabyAge(babyInfo.birthday) }}</text>
</view>
</view>
<!-- 喂养记录列表 -->
<scroll-view class="records-container" scroll-y>
<view v-if="todayRecords.length === 0" class="empty-state">
<view v-if="isLoading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="todayRecords.length === 0" class="empty-state">
<text class="empty-text">当日暂无记录</text>
</view>
<view v-else class="record-list">
......@@ -134,13 +138,64 @@
</view>
</scroll-view>
</view>
<!-- 编辑记录弹窗 -->
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup">
<view class="popup-content" @click.stop>
<view class="popup-title">修改喂养记录</view>
<view class="form-item">
<text class="label">时间:</text>
<picker mode="time" :value="editForm.time" @change="onEditTimeChange">
<view class="picker">{{ editForm.time || '请选择时间' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">类型:</text>
<picker :range="feedingTypes" @change="onEditTypeChange">
<view class="picker">{{ editForm.type || '请选择类型' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">喂养详情:</text>
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
</view>
<view class="popup-buttons">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
<button class="confirm-btn" @click="saveEditRecord">保存</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { feedingRecordsStatisticsPeriod, feedingRecordsByDate, feedingRecords as feedingRecordsAPI } from '@/api/feeding.js'
import { useFeedStore } from '@/stores/feed.js'
// API 集成说明:
// 1. 使用 /c/feeding/statistics/period GET 方法查询统计图表数据
// - 参数:babyId (宝宝ID), sdate (开始日期), edate (结束日期)
// - 返回:指定时间段的喂养统计数据
// - 数据格式转换:API返回的统计数据转换为图表需要的格式
// 2. 使用 /c/feeding/records GET 方法查询指定日期的喂养记录
// - 参数:babyId (宝宝ID), date (日期,格式:YYYY-MM-DD)
// - 返回:该日期的所有喂养记录数组
// - 数据格式转换:API返回的记录数据转换为页面显示格式
// 3. 使用 /c/feeding/records POST 方法修改喂养记录
// - 参数:recordId, babyId, recordTime, feedingType, durationLeftSeconds, durationRightSeconds, volume, foodDetails
// - 返回:修改结果
// - 注意:API函数重命名为 feedingRecordsAPI 以避免与本地数据变量冲突
// 4. 实现了缓存机制、错误重试、超时处理、数据验证等功能
// 5. 支持本地数据作为 fallback,确保页面正常显示
// 6. 添加了性能监控和调试开关
// 7. 支持点击柱状图切换日期,按周切换统计
// 8. 支持动态计算宝宝年龄
// 9. 支持修改喂养记录功能
const version = 'v1'
const DEBUG_API = false // 调试开关,控制是否输出详细的API调试信息
// 资源路径
const feedingRecordRes = {
......@@ -150,107 +205,50 @@ const feedingRecordRes = {
icon_pingwei: `/static/feedingRecord/${version}/icon_pingwei.png`,
}
// API 数据存储
const apiStatistics = ref({}) // 存储从API获取的统计数据
const apiRecords = ref({}) // 存储从API获取的记录数据
const isLoading = ref(false) // 加载状态
const loadingStatistics = ref(new Set()) // 正在加载的统计数据集合,避免重复请求
const loadingRecords = ref(new Set()) // 正在加载的记录数据集合,避免重复请求
const statisticsCache = ref(new Map()) // 统计数据缓存
const recordsCache = ref(new Map()) // 记录数据缓存
// 响应式数据
const currentWeek = ref(0) // 当前显示的周数
const selectedDate = ref('2025-07-03') // 当前选中的日期
const chartData = ref([])
// 喂养记录数据
const feedingRecords = ref({
'2025-07-03': [
{
time: '06:20',
type: '母乳亲喂',
leftDuration: '10min',
rightDuration: '10min'
},
{
time: '09:30',
type: '母乳瓶喂',
amount: '约50ml'
},
{
time: '13:50',
type: '奶粉喂养',
amount: '约50ml'
},
{
time: '17:25',
type: '辅食',
content: '小米粥,玉米,馒头,煮面条,包子,奶糊...'
}
],
'2025-07-02': [
{
time: '07:00',
type: '母乳亲喂',
leftDuration: '12min',
rightDuration: '8min'
},
{
time: '11:30',
type: '母乳瓶喂',
amount: '约80ml'
}
],
'2025-07-01': [
{
time: '08:30',
type: '母乳亲喂',
leftDuration: '15min',
rightDuration: '12min'
}
],
'2025-07-04': [
{
time: '06:00',
type: '奶粉喂养',
amount: '约60ml'
},
{
time: '12:00',
type: '辅食',
content: '鸡蛋羹,胡萝卜泥'
}
],
'2025-07-05': [
{
time: '07:30',
type: '母乳亲喂',
leftDuration: '8min',
rightDuration: '10min'
},
{
time: '15:00',
type: '辅食',
content: '苹果泥,香蕉'
}
],
'2025-07-06': [
{
time: '09:00',
type: '辅食',
content: '南瓜粥,土豆泥'
}
],
'2025-07-07': [
{
time: '08:00',
type: '母乳瓶喂',
amount: '约70ml'
},
{
time: '14:00',
type: '奶粉喂养',
amount: '约40ml'
}
]
// 全局状态管理
const feedStore = useFeedStore()
// 计算属性 - 获取宝宝信息
const babyInfo = computed(() => feedStore.getCurrentBaby() || {
id: 1,
name: '默认宝宝',
birthday: '2024-01-15'
})
// 修改记录相关状态
const showEditPopup = ref(false)
const editingRecord = ref(null)
const editForm = ref({
time: '',
type: '',
content: '',
leftDuration: '',
rightDuration: '',
amount: ''
})
// 喂养类型选项
const feedingTypes = ['母乳亲喂', '母乳瓶喂', '奶粉喂养', '辅食']
// 计算属性 - 当前选中日期的记录
const todayRecords = computed(() => {
if (!selectedDate.value) return []
return feedingRecords.value[selectedDate.value] || []
// 优先使用API数据,如果没有则使用本地数据
return apiRecords.value[selectedDate.value] || []
})
// 计算属性 - 当周最大次数
......@@ -314,9 +312,420 @@ function getRecordIconClass(type) {
return classes[type] || ''
}
// API 调用函数
async function loadStatisticsPeriod(startDate, endDate) {
const cacheKey = `${startDate}_${endDate}_${feedStore.getCurrentBabyId()}`
// 避免重复请求
if (loadingStatistics.value.has(cacheKey)) {
console.log('该时间段统计数据正在加载中,跳过重复请求:', cacheKey)
return
}
// 检查缓存
const cachedData = statisticsCache.value.get(cacheKey)
if (cachedData && Date.now() - cachedData.timestamp < 10 * 60 * 1000) { // 10分钟缓存
console.log('使用缓存的统计数据:', cacheKey)
apiStatistics.value[cacheKey] = cachedData.data
return
}
try {
const startTime = Date.now()
loadingStatistics.value.add(cacheKey)
// 只有在没有其他加载中的统计数据时才显示全局加载状态
if (loadingStatistics.value.size === 1) {
isLoading.value = true
}
// Mock 数据模拟
const mockResponse = {
code: '000000',
data: [
{ date: '2025-07-01', totalCount: 1, breastfeedingCount: 1, bottleCount: 0, formulaCount: 0, foodCount: 0 },
{ date: '2025-07-02', totalCount: 2, breastfeedingCount: 1, bottleCount: 1, formulaCount: 0, foodCount: 0 },
{ date: '2025-07-03', totalCount: 4, breastfeedingCount: 1, bottleCount: 1, formulaCount: 1, foodCount: 1 },
{ date: '2025-07-04', totalCount: 2, breastfeedingCount: 0, bottleCount: 0, formulaCount: 1, foodCount: 1 },
{ date: '2025-07-05', totalCount: 2, breastfeedingCount: 1, bottleCount: 0, formulaCount: 0, foodCount: 1 },
{ date: '2025-07-06', totalCount: 1, breastfeedingCount: 0, bottleCount: 0, formulaCount: 0, foodCount: 1 },
{ date: '2025-07-07', totalCount: 2, breastfeedingCount: 0, bottleCount: 1, formulaCount: 1, foodCount: 0 }
]
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsStatisticsPeriod({
babyId: feedStore.getCurrentBabyId(),
sdate: startDate,
edate: endDate
})
// 使用 mock 数据或真实 API
const response = await Promise.race([apiPromise, timeoutPromise])
if (DEBUG_API) {
console.log('获取统计数据:', { startDate, endDate }, response)
console.log('统计数据API返回数据结构:', JSON.stringify(response, null, 2))
}
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
console.error('统计数据API返回的数据格式不正确,期望数组:', response.data)
return
}
// 转换API数据格式为页面需要的格式
const statistics = response.data.map(item => {
// 验证项目格式
if (!item || typeof item !== 'object') {
console.warn('跳过无效的统计项目:', item)
return null
}
return {
date: item.date,
dateString: item.date,
isActive: item.date === selectedDate.value,
totalCount: item.totalCount || 0,
typeCounts: {
'母乳亲喂': item.breastfeedingCount || 0,
'母乳瓶喂': item.bottleCount || 0,
'奶粉喂养': item.formulaCount || 0,
'辅食': item.foodCount || 0
}
}
}).filter(item => item !== null) // 过滤掉无效项目
apiStatistics.value[cacheKey] = statistics
// 添加到缓存
statisticsCache.value.set(cacheKey, {
data: statistics,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取统计数据耗时: ${endTime - startTime}ms`)
} else {
console.log('统计数据API返回空数据')
// 缓存空数据
statisticsCache.value.set(cacheKey, {
data: [],
timestamp: Date.now()
})
}
} catch (error) {
handleApiError(error, '获取统计数据')
// 简单的重试机制(最多重试1次)
if (!loadingStatistics.value.has(`${cacheKey}_retry`)) {
console.log('尝试重试获取统计数据...')
loadingStatistics.value.add(`${cacheKey}_retry`)
setTimeout(() => {
loadStatisticsPeriod(startDate, endDate)
}, 2000) // 2秒后重试
return
}
// 如果API失败,使用本地数据作为fallback
console.warn('API获取统计数据失败,使用本地数据作为fallback')
} finally {
loadingStatistics.value.delete(cacheKey)
// 只有在没有其他加载中的统计数据时才隐藏全局加载状态
if (loadingStatistics.value.size === 0) {
isLoading.value = false
}
}
}
async function loadRecordsByDate(date) {
// 避免重复请求
if (loadingRecords.value.has(date)) {
console.log('该日期记录正在加载中,跳过重复请求:', date)
return
}
// 检查缓存
const cacheKey = `${date}_${babyInfo.value.id}`
const cachedData = recordsCache.value.get(cacheKey)
if (cachedData && Date.now() - cachedData.timestamp < 5 * 60 * 1000) { // 5分钟缓存
console.log('使用缓存的记录数据:', date)
apiRecords.value[date] = cachedData.data
return
}
try {
const startTime = Date.now()
loadingRecords.value.add(date)
// 只有在没有其他加载中的记录时才显示全局加载状态
if (loadingRecords.value.size === 1) {
isLoading.value = true
}
// Mock 数据模拟
const mockResponse = {
code: '000000',
data: [] // 删除所有 feedingRecords.value[date] || []
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsByDate({
babyId: feedStore.getCurrentBabyId(),
date: date
})
// 使用 mock 数据或真实 API
const response = await Promise.race([apiPromise, timeoutPromise])
if (DEBUG_API) {
console.log('获取指定日期记录:', date, response)
console.log('记录API返回数据结构:', JSON.stringify(response, null, 2))
}
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
console.error('记录API返回的数据格式不正确,期望数组:', response.data)
apiRecords.value[date] = []
return
}
// 转换API数据格式为页面需要的格式
const records = response.data.map(record => {
// 验证记录格式
if (!record || typeof record !== 'object') {
console.warn('跳过无效的记录:', record)
return null
}
return {
id: record.id, // 保留原始记录ID,用于修改时传递
time: formatTimeFromTimestamp(record.recordTime),
type: getFeedingTypeLabel(record.feedingType),
leftDuration: record.durationLeftSeconds ? formatDuration(record.durationLeftSeconds) : '',
rightDuration: record.durationRightSeconds ? formatDuration(record.durationRightSeconds) : '',
amount: record.volume ? `${record.volume}ml` : '',
content: record.foodDetails || ''
}
}).filter(record => record !== null) // 过滤掉无效记录
// 按时间排序
records.sort((a, b) => a.time.localeCompare(b.time))
apiRecords.value[date] = records
// 添加到缓存
recordsCache.value.set(cacheKey, {
data: records,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取记录数据耗时: ${endTime - startTime}ms`)
} else {
apiRecords.value[date] = []
console.log('记录API返回空数据')
// 缓存空数据
recordsCache.value.set(cacheKey, {
data: [],
timestamp: Date.now()
})
}
} catch (error) {
handleApiError(error, '获取指定日期记录')
// 简单的重试机制(最多重试1次)
if (!loadingRecords.value.has(`${date}_retry`)) {
console.log('尝试重试获取记录数据...')
loadingRecords.value.add(`${date}_retry`)
setTimeout(() => {
loadRecordsByDate(date)
}, 2000) // 2秒后重试
return
}
// 如果API失败,使用本地数据作为fallback
console.warn('API获取记录失败,使用本地数据作为fallback')
} finally {
loadingRecords.value.delete(date)
// 只有在没有其他加载中的记录时才隐藏全局加载状态
if (loadingRecords.value.size === 0) {
isLoading.value = false
}
}
}
// 统一错误处理函数
function handleApiError(error, context) {
console.error(`${context} 失败:`, error)
// 检查网络状态
uni.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
console.warn('网络连接不可用')
}
}
})
// 根据错误类型提供不同的处理建议
if (error.message && error.message.includes('超时')) {
console.warn('请求超时,可能是网络问题')
} else if (error.message && error.message.includes('API返回错误')) {
console.warn('服务器返回错误,请检查参数')
} else {
console.warn('未知错误,请检查网络连接')
}
}
// 工具函数
function formatTimeFromTimestamp(timestamp) {
if (!timestamp) return ''
let date
// 处理不同的时间戳格式
if (typeof timestamp === 'string') {
// 如果是字符串格式,直接解析
date = new Date(timestamp)
} else if (typeof timestamp === 'number') {
// 如果是数字格式,判断是否为毫秒级时间戳
if (timestamp > 1000000000000) {
// 毫秒级时间戳
date = new Date(timestamp)
} else {
// 秒级时间戳
date = new Date(timestamp * 1000)
}
} else {
date = new Date(timestamp)
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
console.error('无效的时间戳:', timestamp)
return ''
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
function formatDuration(seconds) {
if (!seconds || seconds <= 0) return ''
const minutes = Math.floor(seconds / 60)
return `${minutes}min`
}
function getFeedingTypeLabel(type) {
const typeMap = {
1: '母乳亲喂',
2: '母乳瓶喂',
3: '奶粉喂养',
4: '辅食'
}
return typeMap[type] || '未知'
}
function getFeedingTypeId(type) {
const typeMap = {
'母乳亲喂': 1,
'母乳瓶喂': 2,
'奶粉喂养': 3,
'辅食': 4
}
return typeMap[type] || 1
}
function formatDateTimeString(date, time) {
return `${date}T${time}:00`
}
function parseDurationToSeconds(duration) {
if (!duration) return 0
const match = duration.match(/(\d+)min/)
return match ? parseInt(match[1]) * 60 : 0
}
function parseVolumeToNumber(volume) {
if (!volume) return 0
const match = volume.match(/(\d+)ml/)
return match ? parseInt(match[1]) : 0
}
// 计算宝宝年龄
function calculateBabyAge(birthday) {
const birthDate = new Date(birthday)
const today = new Date()
const diffTime = Math.abs(today - birthDate)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const years = Math.floor(diffDays / 365)
const months = Math.floor((diffDays % 365) / 30)
const days = diffDays % 30
if (years > 0) {
return `${years}${months}个月${days}天`
} else if (months > 0) {
return `${months}个月${days}天`
} else {
return `${days}天`
}
}
// 编辑记录
function editRecord(index) {
console.log('编辑记录:', index)
const record = todayRecords.value[index]
if (!record) {
uni.showToast({
title: '记录不存在',
icon: 'none'
})
return
}
// 设置编辑表单数据
editForm.value = {
time: record.time || '',
type: record.type || '',
content: record.content || '',
leftDuration: record.leftDuration || '',
rightDuration: record.rightDuration || '',
amount: record.amount || ''
}
// 保存当前编辑的记录
editingRecord.value = {
index,
record
}
// 显示编辑弹窗
showEditPopup.value = true
}
// 返回上一页
......@@ -330,11 +739,25 @@ function generateWeekData(weekOffset = 0) {
const baseDate = new Date('2025-07-03')
baseDate.setDate(baseDate.getDate() + weekOffset * 7)
// 计算当前周的开始和结束日期
const startDate = new Date(baseDate)
startDate.setDate(baseDate.getDate() - baseDate.getDay()) // 设置为周日
const endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6) // 设置为周六
const startDateString = startDate.toISOString().split('T')[0]
const endDateString = endDate.toISOString().split('T')[0]
// 加载统计数据
loadStatisticsPeriod(startDateString, endDateString)
for (let i = 0; i < 7; i++) {
const date = new Date(baseDate)
date.setDate(date.getDate() + i)
const date = new Date(startDate)
date.setDate(startDate.getDate() + i)
const dateString = date.toISOString().split('T')[0]
const records = feedingRecords.value[dateString] || []
// 优先使用API数据,如果没有则使用本地数据
const records = apiRecords.value[dateString] || []
// 统计各类型记录数量
const typeCounts = {
......@@ -379,9 +802,161 @@ function updateChartData() {
chartData.value = generateWeekData(currentWeek.value)
}
// 点击柱状图切换日期
function onBarClick(day) {
if (day.dateString) {
selectDate(day.dateString)
}
}
// 编辑弹窗相关方法
function closeEditPopup() {
showEditPopup.value = false
editingRecord.value = null
editForm.value = {
time: '',
type: '',
content: '',
leftDuration: '',
rightDuration: '',
amount: ''
}
}
function onEditTimeChange(event) {
editForm.value.time = event.detail.value
}
function onEditTypeChange(event) {
editForm.value.type = feedingTypes[event.detail.value]
}
function saveEditRecord() {
// 验证表单
if (!editForm.value.time || !editForm.value.type) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
// 验证喂养详情(最多20字)
if (editForm.value.content && editForm.value.content.length > 20) {
uni.showToast({
title: '喂养详情最多20字',
icon: 'none'
})
return
}
if (!editingRecord.value) {
uni.showToast({
title: '编辑记录不存在',
icon: 'none'
})
return
}
// 构建API请求数据
const { index, record } = editingRecord.value
const apiData = {
recordId: record.recordId,
babyId: feedStore.getCurrentBabyId(),
recordTime: formatDateTimeString(selectedDate.value, editForm.value.time),
feedingType: getFeedingTypeId(editForm.value.type)
}
// 根据喂养类型添加相应字段
if (editForm.value.type === '母乳亲喂') {
apiData.durationLeftSeconds = parseDurationToSeconds(editForm.value.leftDuration)
apiData.durationRightSeconds = parseDurationToSeconds(editForm.value.rightDuration)
} else if (editForm.value.type === '母乳瓶喂' || editForm.value.type === '奶粉喂养') {
apiData.volume = parseVolumeToNumber(editForm.value.amount)
} else if (editForm.value.type === '辅食') {
apiData.foodDetails = editForm.value.content
}
// 调用API保存修改
saveRecordToAPI(apiData, index)
}
function getCurrentTime() {
const now = new Date()
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
async function saveRecordToAPI(apiData, index) {
try {
uni.showLoading({
title: '保存中...'
})
// Mock 数据模拟
const mockResponse = {
code: '000000',
message: '保存成功'
}
const response = await feedingRecordsAPI(apiData)
if (response && response.code === '000000') {
// 更新本地数据
updateLocalRecord(index, editForm.value)
uni.showToast({
title: '保存成功',
icon: 'success'
})
closeEditPopup()
// 刷新当前日期的记录
if (selectedDate.value) {
loadRecordsByDate(selectedDate.value)
}
} else {
throw new Error(response?.message || '保存失败')
}
} catch (error) {
console.error('保存记录失败:', error)
uni.showToast({
title: error.message || '保存失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
function updateLocalRecord(index, formData) {
const dateKey = selectedDate.value
if (!dateKey) return
// 更新API数据
if (apiRecords.value[dateKey] && apiRecords.value[dateKey][index]) {
const record = apiRecords.value[dateKey][index]
record.time = formData.time
record.type = formData.type
record.content = formData.content
record.leftDuration = formData.leftDuration
record.rightDuration = formData.rightDuration
record.amount = formData.amount
}
// 更新本地数据
// 本地数据不再需要更新,因为API调用失败时,apiRecords.value[dateKey] 为空
}
// 选择日期
function selectDate(dateString) {
selectedDate.value = dateString
// 加载选中日期的记录
loadRecordsByDate(dateString)
updateChartData()
}
......@@ -421,12 +996,76 @@ function getBarSegments(day) {
// 页面加载
onLoad(() => {
updateChartData()
// 初始化API数据
initializeApiData()
})
// 初始化API数据
async function initializeApiData() {
console.log('初始化API数据...')
// 加载当前选中日期的记录
if (selectedDate.value) {
console.log('加载当前选中日期记录:', selectedDate.value)
await loadRecordsByDate(selectedDate.value)
}
console.log('API数据初始化完成')
}
// 页面显示时刷新数据
onShow(() => {
// 如果已经有选中日期,刷新当前日期的记录
if (selectedDate.value) {
loadRecordsByDate(selectedDate.value)
}
})
// 监听选中日期变化
watch(selectedDate, () => {
updateChartData()
})
// 页面卸载时清理资源
onUnmounted(() => {
// 清理缓存
statisticsCache.value.clear()
recordsCache.value.clear()
loadingStatistics.value.clear()
loadingRecords.value.clear()
})
// 测试函数(开发时使用)
function testApiIntegration() {
console.log('=== 测试API集成 ===')
console.log('当前选中日期:', selectedDate.value)
console.log('当前周数:', currentWeek.value)
console.log('当前宝宝ID:', feedStore.getCurrentBabyId())
console.log('API统计数据:', apiStatistics.value)
console.log('API记录数据:', apiRecords.value)
console.log('缓存状态:', {
statisticsCache: statisticsCache.value.size,
recordsCache: recordsCache.value.size
})
}
// 功能总结:
// 1. 自动加载:页面加载、显示、日期切换时自动调用API
// 2. 智能缓存:避免重复请求,提升性能
// 3. 错误处理:网络错误、超时、服务器错误等
// 4. 重试机制:失败时自动重试,提高成功率
// 5. 数据验证:确保API返回数据的正确性
// 6. 性能监控:记录API调用耗时
// 7. 调试支持:可控制的调试信息输出
// 8. 资源清理:页面卸载时清理缓存和定时器
// 9. 交互功能:点击柱状图切换日期,按周切换统计
// 10. 动态年龄:根据宝宝生日计算当前年龄
// 11. 数据转换:API数据格式转换为页面需要的格式
// 12. 加载状态:智能管理全局和局部加载状态
// 13. Mock数据:提供完整的mock数据支持,便于开发和测试
// 14. 修改功能:支持修改喂养记录,包括时间、类型、详情等
// 15. 统一样式:记录列表样式与feedingRecord页面保持一致
// 16. 命名冲突解决:API函数重命名为 feedingRecordsAPI 以避免与本地数据变量冲突
</script>
<style lang="scss" scoped>
......@@ -556,13 +1195,23 @@ watch(selectedDate, () => {
box-sizing: border-box;
position: relative;
.chart-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
padding: 0 10rpx;
.chart-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
padding: 0 10rpx;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
&.active {
.chart-bar {
......@@ -663,10 +1312,20 @@ watch(selectedDate, () => {
// 记录列表
.records-container {
flex: 1;
background: #FDFBF7; // 与背景色一致
border-radius: 32rpx 32rpx 0 0;
margin-top: 20rpx;
padding: 30rpx 0 0 0;
background: #ffffff;
padding-bottom: 180rpx; // 为浮动按钮留出空间
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.empty-state {
display: flex;
......@@ -681,138 +1340,279 @@ watch(selectedDate, () => {
}
.record-list {
padding: 0 30rpx 30rpx 30rpx;
padding: 20rpx 35rpx 0rpx 20rpx;
.timeline-connector {
width: 1rpx;
height: 156rpx;
background: #d3a358;
margin-left: 16rpx;
margin-top: -125rpx;
margin-bottom: -32rpx;
}
.record-item {
display: flex;
margin-bottom: 50rpx;
position: relative;
margin-bottom: 40rpx; // 增加间距以容纳连接线
.time-dot {
width: 140rpx;
width: 120rpx;
display: flex;
align-items: flex-start;
padding-top: 35rpx;
margin-right: 20rpx;
flex-shrink: 0;
align-items: flex-start; // 改为顶部对齐
margin-right: 0rpx;
margin-left: 10rpx;
padding-top: 30rpx; // 调整顶部内边距,与文案第一行对齐
.dot {
width: 18rpx;
height: 18rpx;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 20rpx;
margin-top: 10rpx;
flex-shrink: 0;
margin-right: 16rpx;
margin-top: 8rpx; // 调整圆点位置,与文案第一行对齐
}
.time {
font-size: 26rpx;
color: #000000; // 黑色文字
line-height: 1.4;
font-weight: bold; // 加粗
font-size: 24rpx;
color: #666;
white-space: nowrap;
line-height: 1.4; // 调整行高,与文案保持一致
}
}
.record-content {
flex: 1;
min-height: 160rpx;
border-radius: 24rpx;
padding: 28rpx;
box-sizing: border-box;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
width: 548rpx;
height: 142rpx;
border-radius: 32rpx;
padding: 24rpx;
box-sizing: border-box; // 确保padding包含在宽高中
overflow: hidden; // 防止内容溢出
.record-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
margin-bottom: 12rpx; // 减少底部间距,适应固定高度
.record-icon-img-muruqinwei {
width: 30rpx;
height: 34rpx;
margin-right: 12rpx;
line-height: 1; // 确保图标行高一致
}
.record-icon-img-pingwei {
width: 26rpx;
height: 37rpx;
margin-right: 12rpx;
line-height: 1; // 确保图标行高一致
}
.record-icon-img-naifen {
width: 30rpx;
height: 34rpx;
margin-right: 12rpx;
line-height: 1; // 确保图标行高一致
}
.record-icon-img-muruqinwei,
.record-icon-img-pingwei,
.record-icon-img-naifen,
.record-icon-img-fushi {
width: 36rpx;
height: 36rpx;
margin-right: 16rpx;
flex-shrink: 0;
height: 31rpx;
margin-right: 12rpx;
line-height: 1; // 确保图标行高一致
}
.record-type {
flex: 1;
font-size: 30rpx;
font-size: 28rpx;
font-weight: 600;
color: #000000; // 黑色文字
line-height: 1.4;
color: #333;
line-height: 1.4; // 与时间文字保持一致的行高
}
.edit-btn {
font-size: 24rpx;
color: #AAAAAA; // 浅灰色文字
padding: 10rpx 20rpx;
border: 1rpx solid #E0E0E0; // 浅灰色边框
color: #1d1e25;
padding: 8rpx 16rpx;
border: 1rpx solid #ffffff;
border-radius: 20rpx;
background: #F5F5F5; // 浅灰色背景
opacity: 0.9;
font-weight: 500;
background: #ffffff;
opacity: 0.8;
}
}
.record-details {
.duration-info,
.amount-info {
.duration-text,
.amount-text {
.duration-info {
display: flex;
gap: 16rpx; // 减少间距
margin-bottom: 6rpx; // 减少底部间距
.duration-text {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 12rpx;
gap: 16rpx; // 进一步增加间距
&:last-child {
margin-bottom: 0;
.duration-label {
font-size: 24rpx;
color: #666;
}
.duration-time {
font-size: 24rpx;
color: #333;
font-weight: 600; // 时间加粗
}
}
}
.amount-info {
margin-bottom: 6rpx; // 减少底部间距
.amount-text {
display: flex;
align-items: center;
gap: 16rpx; // 进一步增加间距
.duration-label,
.amount-label {
font-size: 26rpx;
color: #000000; // 黑色文字
font-weight: 500;
min-width: 60rpx;
font-size: 24rpx;
color: #666;
}
.duration-time,
.amount-value {
font-size: 26rpx;
color: #000000; // 黑色文字
font-weight: 600;
font-size: 24rpx;
color: #333;
font-weight: 600; // 数值加粗
}
}
}
.content-info {
.content-text {
font-size: 26rpx;
color: #000000; // 黑色文字
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
font-size: 24rpx;
color: #333;
font-weight: 600; // 辅食内容加粗
line-height: 1.4;
white-space: nowrap; // 强制一行显示
overflow: hidden; // 隐藏超出部分
text-overflow: ellipsis; // 超出显示...
max-width: 100%; // 确保不超出容器
word-break: keep-all; // 防止单词被截断
}
}
}
}
}
}
}
.timeline-connector {
width: 3rpx;
height: 50rpx;
background: #E0E0E0; // 浅灰色连接线
margin-left: 25rpx;
margin-top: -25rpx;
margin-bottom: -25rpx;
border-radius: 2rpx;
// 弹窗样式
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
}
.popup-content {
background: #fff;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 40rpx 30rpx;
animation: slideUp 0.3s ease-out;
max-height: 80vh;
overflow-y: auto;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.form-item {
margin-bottom: 30rpx;
position: relative;
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
display: block;
}
.picker,
.input {
width: 100%;
padding: 24rpx;
background: #f8f9fa;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
border: 1rpx solid #e9ecef;
box-sizing: border-box;
}
.picker {
display: flex;
align-items: center;
min-height: 88rpx;
}
.char-count {
position: absolute;
right: 0;
bottom: -30rpx;
font-size: 24rpx;
color: #999;
}
}
.popup-buttons {
display: flex;
gap: 20rpx;
margin-top: 50rpx;
button {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
font-weight: 600;
&.cancel-btn {
background: #f8f9fa;
color: #6c757d;
border: 1rpx solid #e9ecef;
}
&.confirm-btn {
background: #D4A574;
color: #fff;
}
&:active {
opacity: 0.8;
}
}
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
// 响应式适配
......
......@@ -13,10 +13,17 @@
<!-- 喂养时间 -->
<view class="feeding-time">
<view class="baby-info">
<view class="baby-info" @click="showBabySwitch" v-if="babyList.length > 0">
<image class="avatar-img" :src="babyList[currentBabyIndex]?.avatar || '/static/feedingIndex/v1/avatar.png'"></image>
<view class="baby-name-section">
<text class="baby-name">{{ babyList[currentBabyIndex]?.name || '加载中...' }}</text>
<image class="dropdown-icon" src="/static/feedingIndex/v1/icon_arrow_yellow_drop.png" />
</view>
</view>
<view class="baby-info" v-else>
<image class="avatar-img" src="/static/feedingIndex/v1/avatar.png"></image>
<view class="baby-name-section">
<text class="baby-name">宝宝名称</text>
<text class="baby-name">加载中...</text>
<image class="dropdown-icon" src="/static/feedingIndex/v1/icon_arrow_yellow_drop.png" />
</view>
</view>
......@@ -150,7 +157,9 @@
<view class="food-categories">
<text class="category-title">辅食分类</text>
<view class="category-actions">
<text v-if="foodSelectionState.isEditMode" class="complete-btn" @click="exitEditMode">完成</text>
<text v-if="foodSelectionState.isEditMode" class="complete-btn" @click="exitEditMode">
完成{{ foodSelectionState.pendingDeletes.length > 0 ? `(${foodSelectionState.pendingDeletes.length})` : '' }}
</text>
<text v-else class="delete-btn" @click="enterEditMode">删除</text>
</view>
</view>
......@@ -219,7 +228,10 @@
<!-- 录音提示 -->
<view class="voice-click-tip">
<text class="click-text" v-if="voiceRecognitionState.isRecording">正在录音...</text>
<text class="click-text" v-if="voiceRecognitionState.isRecording">
正在录音... {{ formatRecordingDuration(voiceRecognitionState.recordingDuration) }}
</text>
<text class="click-text" v-else-if="voiceRecognitionState.isPolling">正在识别...</text>
<text class="click-text" v-else>长按录音</text>
</view>
</view>
......@@ -271,10 +283,10 @@
</view>
</view>
<!-- 底部完成按钮 -->
<view class="bottom_complete-btn" @click="completeRecord">
<image class="complete-btn-bg" src="/static/feedingIndex/v1/complete_btn.png" />
</view>
<!-- 底部完成按钮 -->
<view class="bottom_complete-btn" @click="completeRecord">
<image class="complete-btn-bg" src="/static/feedingIndex/v1/complete_btn.png" />
</view>
</view>
</view>
......@@ -285,11 +297,10 @@
<text class="popup-title">添加辅食</text>
</view>
<view class="popup-content">
<text class="input-label">请输入辅食名称(最多10个字)</text>
<input
class="food-input"
v-model="foodSelectionState.newFoodItem"
placeholder="请输入辅食名称"
placeholder="请输入辅食名称(最多10个字)"
maxlength="10"
/>
</view>
......@@ -318,6 +329,10 @@
<text class="info-label">喂养方式</text>
<text class="info-value">{{ getFeedingTypeLabel() }}</text>
</view>
<view class="info-item">
<text class="info-label">录音时长</text>
<text class="info-value">{{ formatRecordingDuration(voiceRecognitionState.recordingDuration) }}</text>
</view>
<view class="info-item">
<text class="info-label">喂养详情</text>
<input
......@@ -336,10 +351,23 @@
</view>
</view>
</view>
<!-- 切换宝宝弹窗 -->
<BabySwitchPopup
v-model:visible="showBabySwitchPopup"
:baby-list="babyList"
:selected-index="currentBabyIndex"
@change="onBabyChange"
v-if="babyList.length > 0"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import BabySwitchPopup from '@/components/BabySwitchPopup.vue'
import { feedingHome, feedingRecords, feedingFoodsCustom, feedingFoodsCustomAdd, feedingFoodsCustomDelete, feedingTimerStart, feedingTimerStop, feedingVoiceUpload, feedingVoiceResult } from '@/api/feeding.js'
import { useFeedStore } from '@/stores/feed.js'
// 弹窗引用
const addFoodPopup = ref(null)
......@@ -348,11 +376,27 @@ const swiperData = ref([{ bannerImg: '/static/feedingIndex/v1/banner.png' }, { b
const indicatorStyle = `height: 40px; border: none;`
// 当前时间,默认为当前时间
const currentTime = ref(new Date().getTime())
const currentTime = ref(getCurrentTime())
// 日期范围限制
const earliestDateString = ref('2020-01-01')
const latestDateString = ref('2030-12-31')
// 接口返回的数据
const homeData = ref({
timestamp: null,
lastRecordType: 0,
lastBreastMilkVolume: 0,
lastFormulaVolume: 0,
leftTimerRunning: false,
leftTimerDuration: 0,
rightTimerRunning: false,
rightTimerDuration: 0
})
// 记录相关数据
const currentRecordId = ref(null) // 当前记录ID,用于区分新增和修改
const isSubmitting = ref(false) // 提交状态
const bannerHandler = (item) => {
console.log(item);
}
......@@ -364,7 +408,9 @@ const feedingTypes = ref([
{ value: 'food', label: '辅食', icon: '/static/feedingIndex/v1/icon_fushi.png' }
])
// 根据接口数据初始化选中的喂养类型
const selectedType = ref('breastfeeding')
// 为每种喂养方式设置独立的记录方法
const recordMethods = ref({
breastfeeding: 'manual',
......@@ -376,32 +422,44 @@ const isRecording = ref(false)
const isLeftTimerRunning = ref(false)
const isRightTimerRunning = ref(false)
// 辅食数据结构
// 辅食数据结构 - 使用接口数据
const foodCategories = ref({
主食: {
items: ['米粉', '面条', '馒头', '包子', '粥'],
items: [],
customItems: []
},
蔬菜: {
items: ['山药', '萝卜', '土豆', '豆', '青瓜', '白菜', '胡萝卜', '南瓜'],
items: [],
customItems: []
},
水果: {
items: ['香蕉', '苹果', '牛油果', '橙子', '梨'],
items: [],
customItems: []
},
其他: {
items: ['鱼油', '维生素'],
items: [],
customItems: []
}
})
// 辅食ID映射,用于删除操作
const foodIdMap = ref(new Map())
// 辅食类型映射
const foodTypeMap = {
1: '主食',
2: '蔬菜',
3: '水果',
4: '其他'
}
// 辅食选择状态
const foodSelectionState = ref({
isEditMode: false, // 是否处于编辑模式
showAddPopup: false, // 是否显示添加弹窗
currentCategory: '', // 当前添加的分类
newFoodItem: '' // 新添加的辅食项
newFoodItem: '', // 新添加的辅食项
pendingDeletes: [] // 待删除的辅食列表
})
// 语音识别状态
......@@ -409,14 +467,25 @@ const voiceRecognitionState = ref({
isRecording: false, // 是否正在录音
showResultPage: false, // 是否显示识别结果页面
recognizedText: '', // 识别结果文本
recordingDuration: 0 // 录音时长
recordingDuration: 0, // 录音时长
recordingStartTime: null, // 录音开始时间
recordingTimer: null, // 录音时长定时器
taskId: '', // 语音识别任务ID
isPolling: false, // 是否正在轮询识别结果
pollingInterval: null // 轮询定时器
})
// 为每种喂养方式设置独立的数据
// 全局状态管理
const feedStore = useFeedStore()
// 宝宝切换相关状态
const showBabySwitchPopup = ref(false)
// 为每种喂养方式设置独立的数据 - 根据接口数据初始化
const feedingData = ref({
breastfeeding: {
leftDuration: 60,
rightDuration: 5
leftDuration: 0,
rightDuration: 0
},
bottle: {
amount: 0 // 首次默认定位到0ml
......@@ -425,7 +494,7 @@ const feedingData = ref({
amount: 0 // 首次默认定位到0ml
},
food: {
selectedItems: ['米粉', '面条', '苹果', '土豆']
selectedItems: []
}
})
......@@ -440,7 +509,7 @@ const isDragging = ref(false)
const startY = ref(0)
const startAmount = ref(0)
// 计时器数据
// 计时器数据 - 根据接口数据初始化
const timerData = ref({
leftDuration: 0,
rightDuration: 0
......@@ -468,11 +537,287 @@ const pickerValue = computed({
}
})
// 计算属性 - 判断是否为语音模式(辅食除外)
const isVoiceMode = computed(() => {
return recordMethods.value[selectedType.value] === 'voice' && selectedType.value !== 'food'
})
// 计算属性 - 获取宝宝信息
const babyList = computed(() => feedStore.babyList)
const currentBabyIndex = computed(() => feedStore.currentBabyIndex)
const currentBaby = computed(() => feedStore.getCurrentBaby())
const currentBabyId = computed(() => feedStore.getCurrentBabyId())
// 页面加载时获取数据
onMounted(() => {
console.warn('页面加载,开始初始化数据...')
loadBabyInfo()
loadFoodsData()
})
// 页面显示时刷新数据
onShow(() => {
loadBabyInfo()
})
// 页面卸载时清理计时器
onUnmounted(() => {
// 清理左侧计时器
if (leftTimerInterval) {
clearInterval(leftTimerInterval)
leftTimerInterval = null
}
// 清理右侧计时器
if (rightTimerInterval) {
clearInterval(rightTimerInterval)
rightTimerInterval = null
}
// 清理语音识别轮询
clearPollingInterval()
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
})
// 页面参数接收
onLoad((options) => {
console.log('页面参数:', options)
// 如果是修改记录,接收记录ID
if (options.recordId) {
currentRecordId.value = options.recordId
console.log('修改记录ID:', currentRecordId.value)
}
// 如果有其他参数,可以在这里处理
if (options.babyId) {
// 设置选中的宝宝
const babyIndex = feedStore.babyList.findIndex(baby => baby.id == options.babyId)
if (babyIndex > -1) {
feedStore.switchBaby(babyIndex)
}
}
if (options.feedingType) {
// 设置喂养类型
const typeMap = {
'1': 'breastfeeding',
'2': 'bottle',
'3': 'formula',
'4': 'food'
}
selectedType.value = typeMap[options.feedingType] || 'breastfeeding'
}
})
function getCurrentTime() {
const now = new Date()
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
// 获取宝宝信息
async function loadBabyInfo() {
try {
console.log('开始加载宝宝信息...')
// 使用全局状态管理加载宝宝信息
await feedStore.loadBabyInfo()
console.log('宝宝信息加载完成')
// 加载宝宝信息后,加载首页数据
loadHomeData()
} catch (error) {
console.error('获取宝宝信息失败:', error)
// 即使宝宝信息加载失败,也要加载首页数据
loadHomeData()
uni.showToast({
title: '获取宝宝信息失败,使用默认数据',
icon: 'none'
})
}
}
// 获取首页数据
async function loadHomeData() {
try {
const response = await feedingHome(feedStore.getCurrentBabyId())
console.log('首页数据:', response)
if (response && response.data) {
const data = response.data
homeData.value = {
timestamp: data.timestamp,
lastRecordType: data.lastRecordType,
lastBreastMilkVolume: data.lastBreastMilkVolume,
lastFormulaVolume: data.lastFormulaVolume,
leftTimerRunning: data.leftTimerRunning,
leftTimerDuration: data.leftTimerDuration,
rightTimerRunning: data.rightTimerRunning,
rightTimerDuration: data.rightTimerDuration
}
}
// 根据接口数据初始化页面状态
initializePageData()
} catch (error) {
console.error('获取首页数据失败:', error)
// 使用模拟数据作为fallback
homeData.value = {
timestamp: Date.now(),
lastRecordType: 2,
lastBreastMilkVolume: 120,
lastFormulaVolume: 0,
leftTimerRunning: false,
leftTimerDuration: 0,
rightTimerRunning: false,
rightTimerDuration: 0
}
initializePageData()
uni.showToast({
title: '获取数据失败,使用默认数据',
icon: 'none'
})
}
}
// 根据接口数据初始化页面状态
function initializePageData() {
const data = homeData.value
// 设置当前时间
if (data.timestamp) {
currentTime.value = data.timestamp
}
// 根据上次记录类型设置默认选中的喂养类型
if (data.lastRecordType > 0) {
const typeMap = {
1: 'breastfeeding',
2: 'bottle',
3: 'formula',
4: 'food'
}
selectedType.value = typeMap[data.lastRecordType] || 'breastfeeding'
}
// 设置上次的喂养量
if (data.lastRecordType === 2 && data.lastBreastMilkVolume > 0) {
feedingData.value.bottle.amount = data.lastBreastMilkVolume
}
if (data.lastRecordType === 3 && data.lastFormulaVolume > 0) {
feedingData.value.formula.amount = data.lastFormulaVolume
}
// 设置计时器状态
if (data.leftTimerRunning) {
isLeftTimerRunning.value = true
timerData.value.leftDuration = Math.floor(data.leftTimerDuration / 1000) // 转换为秒
startLeftTimer()
}
if (data.rightTimerRunning) {
isRightTimerRunning.value = true
timerData.value.rightDuration = Math.floor(data.rightTimerDuration / 1000) // 转换为秒
startRightTimer()
}
// 如果有计时器在运行,切换到计时器模式
if (data.leftTimerRunning || data.rightTimerRunning) {
recordMethods.value.breastfeeding = 'timer'
}
}
// 方法
function selectType(type) {
// 检查是否有正在进行的计时器
if (isLeftTimerRunning.value || isRightTimerRunning.value) {
uni.showToast({
title: '记录进行中,不可切换',
icon: 'none'
})
return
}
// 如果切换的是不同的类型,清空之前的状态
if (selectedType.value !== type) {
clearPreviousState()
}
selectedType.value = type
}
// 清空之前的状态
function clearPreviousState() {
// 清理轮询定时器
clearPollingInterval()
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 清空语音识别状态
voiceRecognitionState.value = {
isRecording: false,
showResultPage: false,
recognizedText: '',
recordingDuration: 0,
recordingStartTime: null,
recordingTimer: null,
taskId: '',
isPolling: false,
pollingInterval: null
}
// 清空辅食选择状态
foodSelectionState.value = {
isEditMode: false,
showAddPopup: false,
currentCategory: '',
newFoodItem: '',
pendingDeletes: []
}
// 清空录音状态
isRecording.value = false
// 清空计时器状态(如果不在计时中)
if (!isLeftTimerRunning.value && !isRightTimerRunning.value) {
// 重置母乳亲喂的时长
feedingData.value.breastfeeding.leftDuration = 0
feedingData.value.breastfeeding.rightDuration = 0
}
// 清空其他喂养方式的数据
feedingData.value.bottle.amount = 0
feedingData.value.formula.amount = 0
feedingData.value.food.selectedItems = []
// 重置记录方法为手动
recordMethods.value[selectedType.value] = 'manual'
// 清空计时器数据(如果不在计时中)
if (!isLeftTimerRunning.value && !isRightTimerRunning.value) {
timerData.value.leftDuration = 0
timerData.value.rightDuration = 0
}
// 清空滑动状态
isDragging.value = false
startY.value = 0
startAmount.value = 0
}
function adjustDuration(side, value) {
const currentData = feedingData.value.breastfeeding
if (side === 'left') {
......@@ -541,6 +886,15 @@ function onPickerChange(e) {
}
function setRecordMethod(method) {
// 辅食不支持语音模式
if (selectedType.value === 'food' && method === 'voice') {
uni.showToast({
title: '辅食只支持手动选择模式',
icon: 'none'
})
return
}
recordMethods.value[selectedType.value] = method
// 如果切换到语音模式,检查录音权限
......@@ -553,26 +907,82 @@ function toggleRecording() {
isRecording.value = !isRecording.value
}
function toggleTimer(side) {
async function toggleTimer(side) {
const babyId = feedStore.getCurrentBabyId()
if (side === 'left') {
// 左侧计时逻辑
isLeftTimerRunning.value = !isLeftTimerRunning.value
if (isLeftTimerRunning.value) {
if (!isLeftTimerRunning.value) {
// 开始左侧计时
startLeftTimer()
try {
const response = await feedingTimerStart({
babyId: babyId,
side: 'left'
})
console.log('开始左侧计时成功:', response)
isLeftTimerRunning.value = true
startLeftTimer()
} catch (error) {
console.error('开始左侧计时失败:', error)
uni.showToast({
title: '开始计时失败',
icon: 'none'
})
}
} else {
// 停止左侧计时
stopLeftTimer()
try {
const response = await feedingTimerStop({
babyId: babyId,
side: 'left'
})
console.log('停止左侧计时成功:', response)
isLeftTimerRunning.value = false
stopLeftTimer()
} catch (error) {
console.error('停止左侧计时失败:', error)
uni.showToast({
title: '停止计时失败',
icon: 'none'
})
}
}
} else if (side === 'right') {
// 右侧计时逻辑
isRightTimerRunning.value = !isRightTimerRunning.value
if (isRightTimerRunning.value) {
if (!isRightTimerRunning.value) {
// 开始右侧计时
startRightTimer()
try {
const response = await feedingTimerStart({
babyId: babyId,
side: 'right'
})
console.log('开始右侧计时成功:', response)
isRightTimerRunning.value = true
startRightTimer()
} catch (error) {
console.error('开始右侧计时失败:', error)
uni.showToast({
title: '开始计时失败',
icon: 'none'
})
}
} else {
// 停止右侧计时
stopRightTimer()
try {
const response = await feedingTimerStop({
babyId: babyId,
side: 'right'
})
console.log('停止右侧计时成功:', response)
isRightTimerRunning.value = false
stopRightTimer()
} catch (error) {
console.error('停止右侧计时失败:', error)
uni.showToast({
title: '停止计时失败',
icon: 'none'
})
}
}
}
}
......@@ -580,6 +990,10 @@ function toggleTimer(side) {
function startLeftTimer() {
// 开始左侧计时
console.log('开始左侧计时')
// 如果已经有计时器在运行,先停止
if (leftTimerInterval) {
clearInterval(leftTimerInterval)
}
leftTimerInterval = setInterval(() => {
timerData.value.leftDuration++
}, 1000) // 每秒增加1秒
......@@ -597,6 +1011,10 @@ function stopLeftTimer() {
function startRightTimer() {
// 开始右侧计时
console.log('开始右侧计时')
// 如果已经有计时器在运行,先停止
if (rightTimerInterval) {
clearInterval(rightTimerInterval)
}
rightTimerInterval = setInterval(() => {
timerData.value.rightDuration++
}, 1000) // 每秒增加1秒
......@@ -611,9 +1029,242 @@ function stopRightTimer() {
}
}
function completeRecord() {
console.log('完成记录')
uni.navigateBack()
// 完成记录
async function completeRecord() {
// 防止重复提交
if (isSubmitting.value) {
return
}
// 检查当前是否为语音模式(辅食除外)
const currentMethod = recordMethods.value[selectedType.value]
if (currentMethod === 'voice' && selectedType.value !== 'food') {
// 语音模式应该在语音识别弹窗中确认提交,这里不处理
uni.showToast({
title: '请在语音识别结果中确认提交',
icon: 'none'
})
return
}
try {
isSubmitting.value = true
// 验证数据
const validationResult = validateRecordData()
if (!validationResult.valid) {
uni.showToast({
title: validationResult.message,
icon: 'none'
})
return
}
// 显示加载提示
uni.showLoading({
title: '保存中...'
})
// 构建记录数据
const recordData = buildRecordData()
console.log('提交记录数据:', recordData)
// 调用接口保存记录
const response = await feedingRecords(recordData)
console.log('保存记录成功:', response)
// 隐藏加载提示
uni.hideLoading()
// 显示成功提示
uni.showToast({
title: currentRecordId.value ? '修改成功' : '记录成功',
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('保存记录失败:', error)
uni.hideLoading()
uni.showToast({
title: '保存失败',
icon: 'none'
})
} finally {
isSubmitting.value = false
}
}
// 验证记录数据
function validateRecordData() {
const currentBaby = feedStore.getCurrentBaby()
// 验证宝宝信息
if (!currentBaby || !currentBaby.id) {
return { valid: false, message: '请选择宝宝' }
}
// 验证喂养时间
if (!currentTime.value) {
return { valid: false, message: '请选择喂养时间' }
}
// 根据喂养类型和记录方式验证具体数据
switch (selectedType.value) {
case 'breastfeeding':
const currentMethod = recordMethods.value.breastfeeding
if (currentMethod === 'manual') {
// 手动模式:验证左右侧时长
const leftDuration = feedingData.value.breastfeeding.leftDuration
const rightDuration = feedingData.value.breastfeeding.rightDuration
if (leftDuration === 0 && rightDuration === 0) {
return { valid: false, message: '请设置喂养时长' }
}
} else if (currentMethod === 'timer') {
// 计时器模式:验证计时器状态
const leftDuration = timerData.value.leftDuration
const rightDuration = timerData.value.rightDuration
if (leftDuration === 0 && rightDuration === 0) {
return { valid: false, message: '请先启动计时器' }
}
} else if (currentMethod === 'voice') {
// 语音模式:验证语音识别结果
if (!voiceRecognitionState.value.recognizedText.trim()) {
return { valid: false, message: '请先进行语音识别' }
}
}
break
case 'bottle':
case 'formula':
const currentMethod2 = recordMethods.value[selectedType.value]
if (currentMethod2 === 'manual') {
// 手动模式:验证喂养量
const amount = feedingData.value[selectedType.value].amount
if (amount === 0) {
return { valid: false, message: '请设置喂养量' }
}
} else if (currentMethod2 === 'voice') {
// 语音模式:验证语音识别结果
if (!voiceRecognitionState.value.recognizedText.trim()) {
return { valid: false, message: '请先进行语音识别' }
}
}
break
case 'food':
// 辅食只有手动选择模式
const selectedItems = feedingData.value.food.selectedItems
if (selectedItems.length === 0) {
return { valid: false, message: '请选择辅食' }
}
break
}
return { valid: true }
}
// 构建记录数据
function buildRecordData() {
const currentBaby = feedStore.getCurrentBaby()
const recordTime = formatCurrentTime()
// 基础数据
const baseData = {
recordId: currentRecordId.value, // 修改时传入记录ID,新增时为null
babyId: currentBaby.id,
recordTime: recordTime
}
// 根据喂养类型和记录方式构建具体数据
switch (selectedType.value) {
case 'breastfeeding':
const currentMethod = recordMethods.value.breastfeeding
if (currentMethod === 'manual') {
// 手动模式:使用手动设置的时长
return {
...baseData,
feedingType: 1, // 母乳亲喂
durationLeftSeconds: feedingData.value.breastfeeding.leftDuration * 60, // 转换为秒
durationRightSeconds: feedingData.value.breastfeeding.rightDuration * 60,
totalDurationSeconds: (feedingData.value.breastfeeding.leftDuration + feedingData.value.breastfeeding.rightDuration) * 60
}
} else if (currentMethod === 'timer') {
// 计时器模式:使用计时器的实际时长
return {
...baseData,
feedingType: 1, // 母乳亲喂
durationLeftSeconds: timerData.value.leftDuration,
durationRightSeconds: timerData.value.rightDuration,
totalDurationSeconds: timerData.value.leftDuration + timerData.value.rightDuration
}
} else if (currentMethod === 'voice') {
// 语音模式:使用语音识别的结果
return {
...baseData,
feedingType: 1, // 母乳亲喂
foodDetails: voiceRecognitionState.value.recognizedText.trim() // 语音识别的详情作为备注
}
}
break
case 'bottle':
const currentMethod2 = recordMethods.value.bottle
if (currentMethod2 === 'manual') {
// 手动模式:使用手动设置的喂养量
return {
...baseData,
feedingType: 2, // 母乳瓶喂
volume: feedingData.value.bottle.amount
}
} else if (currentMethod2 === 'voice') {
// 语音模式:使用语音识别的结果
return {
...baseData,
feedingType: 2, // 母乳瓶喂
foodDetails: voiceRecognitionState.value.recognizedText.trim()
}
}
break
case 'formula':
const currentMethod3 = recordMethods.value.formula
if (currentMethod3 === 'manual') {
// 手动模式:使用手动设置的喂养量
return {
...baseData,
feedingType: 3, // 奶粉喂养
volume: feedingData.value.formula.amount
}
} else if (currentMethod3 === 'voice') {
// 语音模式:使用语音识别的结果
return {
...baseData,
feedingType: 3, // 奶粉喂养
foodDetails: voiceRecognitionState.value.recognizedText.trim()
}
}
break
case 'food':
// 辅食只有手动选择模式
return {
...baseData,
feedingType: 4, // 辅食
foodDetails: feedingData.value.food.selectedItems.join(',')
}
}
return baseData
}
function goBack() {
......@@ -697,10 +1348,70 @@ function toggleFoodSelection(item) {
}
function enterEditMode() {
foodSelectionState.value.isEditMode = true
if (foodSelectionState.value.isEditMode) {
// 如果已经在编辑模式,则取消编辑
cancelEditMode()
} else {
// 进入编辑模式
foodSelectionState.value.isEditMode = true
// 清空待删除列表
foodSelectionState.value.pendingDeletes = []
}
}
function cancelEditMode() {
// 恢复被删除的辅食到UI中
foodSelectionState.value.pendingDeletes.forEach(item => {
const category = foodCategories.value[item.categoryName]
if (category) {
// 检查是否已存在,避免重复添加
if (!category.items.includes(item.foodName) && !category.customItems.includes(item.foodName)) {
category.items.push(item.foodName)
}
}
})
// 清空待删除列表
foodSelectionState.value.pendingDeletes = []
// 退出编辑模式
foodSelectionState.value.isEditMode = false
}
function exitEditMode() {
async function exitEditMode() {
// 如果有待删除的辅食,调用接口删除
if (foodSelectionState.value.pendingDeletes.length > 0) {
try {
// 获取所有待删除的foodId
const foodIds = foodSelectionState.value.pendingDeletes.map(item => item.foodId).join(',')
// 调用接口删除辅食
const response = await feedingFoodsCustomDelete({
foodIds: foodIds
})
console.log('批量删除辅食成功:', response)
// 清空待删除列表
foodSelectionState.value.pendingDeletes = []
// 重新加载辅食列表
await loadFoodsData()
uni.showToast({
title: '删除成功',
icon: 'success'
})
} catch (error) {
console.error('批量删除辅食失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
// 退出编辑模式
foodSelectionState.value.isEditMode = false
}
......@@ -720,7 +1431,7 @@ function showAddFoodPopup(categoryName) {
// 打开弹窗
uni.showModal({
title: '添加辅食',
content: '请输入辅食名称(最多10个字)',
content: '',
editable: true,
placeholderText: '请输入辅食名称',
success: (res) => {
......@@ -752,7 +1463,7 @@ function confirmAddFood() {
cancelAddFood()
}
function addCustomFoodItem(categoryName, itemName) {
async function addCustomFoodItem(categoryName, itemName) {
// 检查字数限制
if (itemName.length > 10) {
uni.showToast({
......@@ -781,16 +1492,63 @@ function addCustomFoodItem(categoryName, itemName) {
return
}
// 添加自定义辅食
category.customItems.push(itemName)
uni.showToast({
title: '添加成功',
icon: 'success'
})
try {
// 获取辅食类型ID
const foodTypeId = Object.keys(foodTypeMap).find(key => foodTypeMap[key] === categoryName)
if (!foodTypeId) {
uni.showToast({
title: '辅食类型错误',
icon: 'none'
})
return
}
// 调用接口添加自定义辅食
const response = await feedingFoodsCustomAdd({
foodName: itemName,
foodType: parseInt(foodTypeId)
})
console.log('添加自定义辅食成功:', response)
// 重新加载辅食列表
await loadFoodsData()
uni.showToast({
title: '添加成功',
icon: 'success'
})
} catch (error) {
console.error('添加自定义辅食失败:', error)
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
function removeFoodItem(categoryName, itemName) {
// 获取foodId
const foodId = foodIdMap.value.get(itemName)
if (!foodId) {
uni.showToast({
title: '找不到辅食ID',
icon: 'none'
})
return
}
// 添加到待删除列表
foodSelectionState.value.pendingDeletes.push({
foodId: foodId,
foodName: itemName,
categoryName: categoryName
})
// 从UI中移除(但不调用接口)
const category = foodCategories.value[categoryName]
// 从默认辅食中移除
......@@ -811,10 +1569,7 @@ function removeFoodItem(categoryName, itemName) {
feedingData.value.food.selectedItems.splice(selectedIndex, 1)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
console.log('添加到待删除列表:', itemName, '当前待删除:', foodSelectionState.value.pendingDeletes)
}
// 语音识别相关方法
......@@ -822,14 +1577,18 @@ function checkVoicePermission() {
// 检查录音权限
uni.getSetting({
success: (res) => {
console.log('录音权限检查结果:', res.authSetting)
if (!res.authSetting['scope.record']) {
// 没有录音权限,请求授权
console.log('请求录音权限...')
uni.authorize({
scope: 'scope.record',
success: () => {
console.log('录音权限授权成功')
},
fail: () => {
fail: (err) => {
console.error('录音权限授权失败:', err)
// 用户拒绝授权
uni.showModal({
title: '需要录音权限',
......@@ -841,9 +1600,13 @@ function checkVoicePermission() {
// 打开设置页面
uni.openSetting({
success: (settingRes) => {
console.log('设置页面结果:', settingRes)
if (settingRes.authSetting['scope.record']) {
console.log('录音权限已开启')
}
},
fail: (err) => {
console.error('打开设置页面失败:', err)
}
})
}
......@@ -854,6 +1617,13 @@ function checkVoicePermission() {
} else {
console.log('已有录音权限')
}
},
fail: (err) => {
console.error('检查录音权限失败:', err)
uni.showToast({
title: '录音权限检查失败',
icon: 'none'
})
}
})
}
......@@ -870,40 +1640,129 @@ function startRecording() {
return
}
// 清理之前的录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 重置录音状态
voiceRecognitionState.value.isRecording = true
voiceRecognitionState.value.recordingDuration = 0
voiceRecognitionState.value.recordingStartTime = null
console.log('开始录音...')
// 开始录音
uni.startRecord({
success: (res) => {
console.log('开始录音成功', res)
// 记录录音开始时间
voiceRecognitionState.value.recordingStartTime = Date.now()
// 启动录音时长定时器
voiceRecognitionState.value.recordingTimer = setInterval(() => {
if (voiceRecognitionState.value.isRecording && voiceRecognitionState.value.recordingStartTime) {
const duration = Math.floor((Date.now() - voiceRecognitionState.value.recordingStartTime) / 1000)
voiceRecognitionState.value.recordingDuration = duration
}
}, 1000) // 每秒更新一次
},
fail: (err) => {
console.log('开始录音失败', err)
console.error('开始录音失败', err)
voiceRecognitionState.value.isRecording = false
uni.showToast({
title: '录音失败',
icon: 'none'
})
}
})
},
fail: (err) => {
console.error('检查录音权限失败', err)
uni.showToast({
title: '录音权限检查失败',
icon: 'none'
})
}
})
}
function stopRecording() {
async function stopRecording() {
if (!voiceRecognitionState.value.isRecording) return
console.log('停止录音...')
voiceRecognitionState.value.isRecording = false
// 停止录音并识别
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 计算录音时长
if (voiceRecognitionState.value.recordingStartTime) {
const duration = Math.floor((Date.now() - voiceRecognitionState.value.recordingStartTime) / 1000)
voiceRecognitionState.value.recordingDuration = duration
console.log('录音时长:', duration, '秒')
// 检查录音时长是否太短
if (duration < 1) {
uni.showToast({
title: '录音时间太短,请重新录音',
icon: 'none'
})
return
}
// 检查录音时长是否太长
if (duration > 60) {
uni.showToast({
title: '录音时间太长,请重新录音',
icon: 'none'
})
return
}
}
// 停止录音并上传
uni.stopRecord({
success: (res) => {
console.log('录音完成', res)
// 模拟语音识别结果
simulateVoiceRecognition()
success: async (res) => {
console.log('录音完成,返回结果:', res)
// 检查录音文件路径 - 不同平台可能返回不同的字段名
const tempFilePath = res.tempFilePath || res.tempFilePaths?.[0] || res.filePath
if (!tempFilePath) {
console.error('录音文件路径不存在,完整返回结果:', res)
uni.hideLoading()
uni.showToast({
title: '录音文件获取失败',
icon: 'none'
})
return
}
console.log('获取到录音文件路径:', tempFilePath)
try {
// 显示加载提示
uni.showLoading({
title: '正在识别...'
})
// 上传语音文件
const uploadResponse = await uploadVoiceFile(tempFilePath)
console.log('语音上传成功:', uploadResponse)
// 开始轮询识别结果
await startPollingRecognitionResult(uploadResponse.taskId)
} catch (error) {
handleVoiceRecognitionError(error)
}
},
fail: (err) => {
console.log('停止录音失败', err)
console.error('停止录音失败', err)
uni.showToast({
title: '录音失败',
icon: 'none'
......@@ -912,17 +1771,171 @@ function stopRecording() {
})
}
function simulateVoiceRecognition() {
// 模拟语音识别结果
const mockResults = {
breastfeeding: '早上8点,左边5分钟,右边6分钟',
bottle: '早上8点,喂奶量90毫升',
formula: '早上8点,喂奶量120毫升',
food: '早上8点,吃了米粉和苹果'
// 上传语音文件
async function uploadVoiceFile(tempFilePath) {
return new Promise((resolve, reject) => {
// 检查文件路径
if (!tempFilePath || typeof tempFilePath !== 'string') {
reject(new Error('无效的语音文件路径'))
return
}
console.log('开始读取语音文件:', tempFilePath)
// 先获取文件信息
uni.getFileSystemManager().getFileInfo({
filePath: tempFilePath,
success: (fileInfo) => {
console.log('文件信息:', fileInfo)
// 检查文件大小(限制为10MB)
if (fileInfo.size > 10 * 1024 * 1024) {
reject(new Error('语音文件过大,请重新录音'))
return
}
// 读取文件为base64
uni.getFileSystemManager().readFile({
filePath: tempFilePath,
encoding: 'base64',
success: async (res) => {
try {
console.log('语音文件读取成功,开始上传')
// 调用上传接口
const response = await feedingVoiceUpload({
audioData: res.data
})
if (response && response.data && response.data.taskId) {
resolve(response.data)
} else {
reject(new Error('上传失败:未获取到taskId'))
}
} catch (error) {
reject(error)
}
},
fail: (err) => {
console.error('读取语音文件失败:', err)
reject(new Error('读取语音文件失败:' + err.errMsg))
}
})
},
fail: (err) => {
console.error('获取文件信息失败:', err)
reject(new Error('获取文件信息失败:' + err.errMsg))
}
})
})
}
// 开始轮询识别结果
async function startPollingRecognitionResult(taskId) {
voiceRecognitionState.value.taskId = taskId
voiceRecognitionState.value.isPolling = true
// 设置轮询间隔(每2秒查询一次)
voiceRecognitionState.value.pollingInterval = setInterval(async () => {
try {
const response = await feedingVoiceResult({
taskId: taskId
})
console.log('轮询识别结果:', response)
if (response && response.data) {
const result = response.data
// 检查识别状态
if (result.status === 'completed') {
// 识别完成
clearPollingInterval()
uni.hideLoading()
// 显示识别结果
voiceRecognitionState.value.recognizedText = result.text || '未能识别语音内容'
voiceRecognitionState.value.showResultPage = true
} else if (result.status === 'failed') {
// 识别失败
clearPollingInterval()
uni.hideLoading()
uni.showToast({
title: '语音识别失败',
icon: 'none'
})
}
// 如果status是'processing',继续轮询
}
} catch (error) {
console.error('轮询识别结果失败:', error)
// 轮询失败时继续尝试,不中断轮询
// 但如果连续失败次数过多,可以考虑停止轮询
}
}, 2000) // 每2秒轮询一次
// 设置最大轮询时间(30秒后停止)
setTimeout(() => {
if (voiceRecognitionState.value.isPolling) {
clearPollingInterval()
uni.hideLoading()
uni.showToast({
title: '识别超时,请重试',
icon: 'none'
})
}
}, 30000)
}
// 清理轮询定时器
function clearPollingInterval() {
if (voiceRecognitionState.value.pollingInterval) {
clearInterval(voiceRecognitionState.value.pollingInterval)
voiceRecognitionState.value.pollingInterval = null
}
voiceRecognitionState.value.isPolling = false
// 同时清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
}
// 处理语音识别错误
function handleVoiceRecognitionError(error) {
console.error('语音识别错误:', error)
clearPollingInterval()
uni.hideLoading()
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 根据错误类型显示不同的提示
let errorMessage = '语音识别失败,请重试'
if (error.message) {
if (error.message.includes('无效的语音文件路径')) {
errorMessage = '录音文件获取失败,请重试'
} else if (error.message.includes('读取语音文件失败')) {
errorMessage = '录音文件读取失败,请重试'
} else if (error.message.includes('上传失败')) {
errorMessage = '语音上传失败,请重试'
} else if (error.message.includes('语音文件过大')) {
errorMessage = '录音文件过大,请重新录音'
} else if (error.message.includes('获取文件信息失败')) {
errorMessage = '录音文件信息获取失败,请重试'
}
}
voiceRecognitionState.value.recognizedText = mockResults[selectedType.value] || '早上8点,喂奶量90毫升'
voiceRecognitionState.value.showResultPage = true
uni.showToast({
title: errorMessage,
icon: 'none'
})
}
function getFeedingTypeLabel() {
......@@ -935,12 +1948,44 @@ function getFeedingTypeLabel() {
return typeMap[selectedType.value] || '未知'
}
// 格式化录音时长显示
function formatRecordingDuration(seconds) {
if (!seconds || seconds <= 0) return '0秒'
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
if (minutes > 0) {
return `${minutes}分${remainingSeconds}秒`
} else {
return `${remainingSeconds}秒`
}
}
function reRecognize() {
// 清理轮询状态
clearPollingInterval()
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 重置识别状态
voiceRecognitionState.value.showResultPage = false
voiceRecognitionState.value.recognizedText = ''
voiceRecognitionState.value.taskId = ''
voiceRecognitionState.value.recordingStartTime = null
voiceRecognitionState.value.recordingDuration = 0
}
function completeVoiceRecord() {
async function completeVoiceRecord() {
// 防止重复提交
if (isSubmitting.value) {
return
}
const detailText = voiceRecognitionState.value.recognizedText.trim()
if (!detailText) {
......@@ -951,30 +1996,148 @@ function completeVoiceRecord() {
return
}
// 生成喂养记录
const record = {
time: formatCurrentTime(),
type: getFeedingTypeLabel(),
detail: detailText
try {
isSubmitting.value = true
// 显示加载提示
uni.showLoading({
title: '保存中...'
})
// 构建记录数据(使用统一的构建函数)
const recordData = buildRecordData()
console.log('提交语音记录数据:', recordData)
// 调用接口保存记录
const response = await feedingRecords(recordData)
console.log('保存语音记录成功:', response)
// 隐藏加载提示
uni.hideLoading()
// 关闭结果页面
voiceRecognitionState.value.showResultPage = false
voiceRecognitionState.value.recognizedText = ''
voiceRecognitionState.value.taskId = ''
voiceRecognitionState.value.recordingStartTime = null
voiceRecognitionState.value.recordingDuration = 0
// 清理录音定时器
if (voiceRecognitionState.value.recordingTimer) {
clearInterval(voiceRecognitionState.value.recordingTimer)
voiceRecognitionState.value.recordingTimer = null
}
// 显示成功提示
uni.showToast({
title: currentRecordId.value ? '修改成功' : '记录成功',
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('保存语音记录失败:', error)
uni.hideLoading()
uni.showToast({
title: '保存失败',
icon: 'none'
})
} finally {
isSubmitting.value = false
}
}
// 宝宝切换相关方法
function showBabySwitch() {
showBabySwitchPopup.value = true
}
function onBabyChange(baby, index) {
feedStore.switchBaby(index)
console.log('切换到宝宝:', baby.name, '索引:', index)
console.log('生成喂养记录:', record)
// 切换宝宝后重新加载该宝宝的喂养数据
loadHomeData()
// 关闭结果页面
voiceRecognitionState.value.showResultPage = false
voiceRecognitionState.value.recognizedText = ''
// 清空当前状态,避免数据混乱
clearPreviousState()
}
// 获取辅食列表数据
async function loadFoodsData() {
try {
const response = await feedingFoodsCustom()
console.log('辅食列表数据:', response)
if (response && response.data) {
const foodsList = response.data
processFoodsData(foodsList)
} else {
// 如果没有数据,使用默认数据
loadDefaultFoodsData()
}
} catch (error) {
console.error('获取辅食列表失败:', error)
// 使用默认数据作为fallback
loadDefaultFoodsData()
}
}
// 处理辅食数据
function processFoodsData(foodsList) {
// 清空现有数据
Object.keys(foodCategories.value).forEach(category => {
foodCategories.value[category].items = []
foodCategories.value[category].customItems = []
})
// 显示成功提示
uni.showToast({
title: '记录成功',
icon: 'success'
// 清空ID映射
foodIdMap.value.clear()
// 按类型分组辅食
foodsList.forEach(food => {
const categoryName = foodTypeMap[food.foodType]
if (categoryName && foodCategories.value[categoryName]) {
foodCategories.value[categoryName].items.push(food.foodName)
// 保存foodId映射
foodIdMap.value.set(food.foodName, food.foodId)
}
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
console.log('处理后的辅食数据:', foodCategories.value)
console.log('辅食ID映射:', foodIdMap.value)
}
// 加载默认辅食数据(fallback)
function loadDefaultFoodsData() {
foodCategories.value = {
主食: {
items: ['米粉', '面条', '馒头', '包子', '粥'],
customItems: []
},
蔬菜: {
items: ['山药', '萝卜', '土豆', '豆', '青瓜', '白菜', '胡萝卜', '南瓜'],
customItems: []
},
水果: {
items: ['香蕉', '苹果', '牛油果', '橙子', '梨'],
customItems: []
},
其他: {
items: ['鱼油', '维生素'],
customItems: []
}
}
}
</script>
<style lang="scss" scoped>
......@@ -1017,6 +2180,12 @@ function completeVoiceRecord() {
display: flex;
align-items: center;
gap: 15rpx;
cursor: pointer;
transition: opacity 0.3s ease;
&:active {
opacity: 0.7;
}
.avatar-img {
width: 80rpx;
......@@ -1097,6 +2266,8 @@ function completeVoiceRecord() {
}
}
/* ===== 喂养类型选择 ===== */
.feeding-types {
position: absolute;
......@@ -1502,6 +2673,8 @@ function completeVoiceRecord() {
cursor: pointer;
}
}
}
}
}
......@@ -1754,6 +2927,8 @@ function completeVoiceRecord() {
}
}
/* 圆形计时器 */
.circular-timers {
display: flex;
......@@ -1816,7 +2991,7 @@ function completeVoiceRecord() {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 120rpx;
bottom: 100rpx;
width: 686rpx;
height: 94rpx;
display: flex;
......@@ -1831,6 +3006,14 @@ function completeVoiceRecord() {
width: 100%;
height: 100%;
}
.complete-btn-text {
position: relative;
z-index: 1;
font-size: 32rpx;
color: white;
font-weight: bold;
}
}
}
......@@ -1948,4 +3131,19 @@ function completeVoiceRecord() {
}
}
}
// 语音模式提示样式
.voice-mode-tip {
position: absolute;
top: -60rpx;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
white-space: nowrap;
z-index: 10;
}
</style>
\ No newline at end of file
......@@ -8,9 +8,9 @@
<view class="nav-left">
<image :src="feedingRecordRes.icon_return" class="back-btn" @click="goBack" />
<image :src="feedingRecordRes.icon_star" class="baby-icon-star" />
<view class="baby-info">
<text class="baby-name">小糖豆3123</text>
<text class="baby-age">1岁24天</text>
<view class="baby-info" @click="showBabySwitch">
<text class="baby-name">{{ babyList[currentBabyIndex].name }}</text>
<text class="baby-age">{{ calculateBabyAge(babyList[currentBabyIndex].birthday) }}</text>
</view>
<image :src="feedingRecordRes.icon_baby_change" class="baby-icon-change" />
</view>
......@@ -69,7 +69,10 @@
<!-- 喂养记录列表 -->
<scroll-view class="records-container" scroll-y>
<view v-if="todayRecords.length === 0" class="empty-state">
<view v-if="isLoading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="todayRecords.length === 0" class="empty-state">
<text class="empty-text">当日暂无记录</text>
</view>
<view v-else class="record-list">
......@@ -129,7 +132,7 @@
</scroll-view>
<!-- 底部添加按钮 -->
<view class="bottom-add-btn" @click="addFeedingRecord">
<view class="bottom-add-btn" @click="goToFeedingIndex">
<image :src="feedingRecordRes.add_btn" class="add-btn-img" />
</view>
......@@ -159,14 +162,67 @@
</view>
</view>
</view>
<!-- 编辑弹窗 -->
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup">
<view class="popup-content" @click.stop>
<view class="popup-title">修改喂养记录</view>
<view class="form-item">
<text class="label">时间:</text>
<picker mode="time" :value="editForm.time" @change="onEditTimeChange">
<view class="picker">{{ editForm.time || '请选择时间' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">类型:</text>
<picker :range="feedingTypes" @change="onEditTypeChange">
<view class="picker">{{ editForm.type || '请选择类型' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">喂养详情:</text>
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
</view>
<view class="popup-buttons">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
<button class="confirm-btn" @click="saveEditRecord">保存</button>
</view>
</view>
</view>
</view>
<!-- 切换宝宝弹窗 -->
<BabySwitchPopup
v-model:visible="showBabySwitchPopup"
:baby-list="babyList"
:selected-index="currentBabyIndex"
@change="onBabyChange"
/>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import BabySwitchPopup from '@/components/BabySwitchPopup.vue'
import { feedingRecordsByDate, feedingRecordsCalendarStatus, feedingRecords as feedingRecordsAPI } from '@/api/feeding.js'
import { useFeedStore } from '@/stores/feed.js'
// API 集成说明:
// 1. 使用 /c/feeding/records GET 方法查询指定日期的喂养记录列表
// - 参数:babyId (宝宝ID), date (日期,格式:YYYY-MM-DD)
// - 返回:该日期的所有喂养记录数组
// 2. 使用 /c/feeding/records/calendar-status GET 方法获取日历状态
// - 参数:babyId (宝宝ID), month (月份,格式:YYYY-MM)
// - 返回:该月份每天的记录状态数组 [{date: "2025-07-01", hasRecord: false}, ...]
// 3. 实现了缓存机制、错误重试、超时处理、数据验证等功能
// 4. 支持本地数据作为 fallback,确保页面正常显示
// 5. 添加了性能监控和调试开关
// 6. 支持宝宝切换时自动刷新数据
// 7. 支持页面显示时自动刷新数据
const version = 'v1'
const DEBUG_API = false // 调试开关,控制是否输出详细的API调试信息
const feedingRecordRes = {
......@@ -187,7 +243,7 @@ const feedingRecordRes = {
// 响应式数据
const currentSelectedDate = ref('')
const currentSelectedDate = ref(formatDateString(new Date()))
const showPopup = ref(false)
const isCalendarExpanded = ref(false)
const currentDate = ref(new Date())
......@@ -197,6 +253,12 @@ const forceUpdate = ref(0)
// 添加当前显示月份的字符串表示,确保响应性
const currentMonthKey = ref('')
// 全局状态管理
const feedStore = useFeedStore()
// 宝宝切换相关状态
const showBabySwitchPopup = ref(false)
// 新记录表单
const newRecord = ref({
time: '',
......@@ -213,88 +275,26 @@ const feedingTypes = ['母乳亲喂', '母乳瓶喂', '奶粉喂养', '辅食']
// 星期标题
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
// 喂养记录数据
const feedingRecords = ref({
'2025-07-03': [
{
time: '06:20',
type: '母乳亲喂',
leftDuration: '10min',
rightDuration: '10min'
},
{
time: '09:30',
type: '母乳瓶喂',
amount: '约50ml'
},
{
time: '13:50',
type: '奶粉喂养',
amount: '约50ml'
},
{
time: '17:25',
type: '辅食',
content: '小米粥、玉米、馒头、蒸面条、包子、奶粉、啊啊啊啊、啊啊啊啊'
}
],
'2025-07-02': [
{
time: '07:00',
type: '母乳亲喂',
leftDuration: '12min',
rightDuration: '8min'
},
{
time: '11:30',
type: '母乳瓶喂',
amount: '约80ml'
}
],
'2025-06-01': [
{
time: '08:00',
type: '母乳亲喂',
leftDuration: '15min',
rightDuration: '10min'
}
],
'2025-05-31': [
{
time: '09:15',
type: '母乳亲喂',
leftDuration: '8min',
rightDuration: '12min'
}
],
'2025-05-30': [
{
time: '10:00',
type: '母乳瓶喂',
amount: '约60ml'
}
],
'2025-07-01': [
{
time: '08:30',
type: '母乳亲喂',
leftDuration: '15min',
rightDuration: '12min'
}
],
'2025-04-25': [
{
time: '14:00',
type: '辅食',
content: '苹果泥、香蕉'
}
]
})
// API 数据存储
const apiRecords = ref({}) // 存储从API获取的记录数据
const calendarStatus = ref({}) // 存储日历状态数据
const isLoading = ref(false) // 加载状态
const loadingRecords = ref(new Set()) // 正在加载的记录日期集合,避免重复请求
const loadingCalendar = ref(new Set()) // 正在加载的日历月份集合,避免重复请求
const recordsCache = ref(new Map()) // 记录数据缓存,避免重复请求
const calendarCache = ref(new Map()) // 日历状态缓存,避免重复请求
// 计算属性 - 获取宝宝信息
const babyList = computed(() => feedStore.babyList)
const currentBabyIndex = computed(() => feedStore.currentBabyIndex)
const currentBaby = computed(() => feedStore.getCurrentBaby())
const currentBabyId = computed(() => feedStore.getCurrentBabyId())
// 计算属性 - 当前选中日期的记录
const todayRecords = computed(() => {
if (!currentSelectedDate.value) return []
return feedingRecords.value[currentSelectedDate.value] || []
// 优先使用API数据,如果没有则使用本地数据
return apiRecords.value[currentSelectedDate.value] || []
})
// 计算属性 - 今天的日期字符串(用于限制uni-datetime-picker)
......@@ -433,6 +433,9 @@ function selectDate(dateObj) {
// 选择日期
currentSelectedDate.value = dateObj.dateString
// 加载选中日期的记录
loadRecordsByDate(dateObj.dateString)
// 如果点击的是非当月日期,切换到该日期所在的月份
if (!dateObj.isCurrentMonth) {
console.log('切换到:', dateObj.date.getFullYear(), '年', dateObj.date.getMonth() + 1, '月')
......@@ -454,6 +457,10 @@ function selectDate(dateObj) {
isCalendarExpanded.value = true
}
// 加载新月份的日历状态
const newMonth = formatMonthString(newCurrentDate)
loadCalendarStatus(newMonth)
// 使用 nextTick 确保 DOM 更新后再强制更新一次
nextTick(() => {
console.log('selectDate: nextTick后再次强制更新')
......@@ -496,6 +503,9 @@ function onDateChange(event) {
currentSelectedDate.value = event
// 加载选中日期的记录
loadRecordsByDate(event)
// 计算新的月份(使用已声明的selectedDate变量)
const newCurrentDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1)
const newMonthKey = `${selectedDate.getFullYear()}-${selectedDate.getMonth()}`
......@@ -507,6 +517,10 @@ function onDateChange(event) {
currentMonthKey.value = newMonthKey
forceUpdate.value++
// 加载新月份的日历状态
const newMonth = formatMonthString(newCurrentDate)
loadCalendarStatus(newMonth)
// 使用 nextTick 确保 DOM 更新后再强制更新一次
nextTick(() => {
console.log('onDateChange: nextTick后再次强制更新')
......@@ -535,7 +549,12 @@ function formatCurrentDate() {
}
function hasRecordOnDate(date) {
return feedingRecords.value[date] && feedingRecords.value[date].length > 0
// 优先使用API返回的日历状态数据
if (calendarStatus.value[date] !== undefined) {
return calendarStatus.value[date]
}
// 如果没有API数据,则使用本地数据判断
return apiRecords.value[date] && apiRecords.value[date].length > 0
}
function getRecordIcon(type) {
......@@ -573,11 +592,55 @@ function getRecordBgColor(type) {
}
function editRecord(index) {
// 编辑记录功能
uni.showToast({
title: '编辑功能',
icon: 'none'
})
const record = todayRecords.value[index]
if (!record) return
editForm.value = {
time: record.time || '',
type: record.type || '',
content: record.content || ''
}
editingRecord.value = { index, record }
showEditPopup.value = true
}
function closeEditPopup() {
showEditPopup.value = false
editingRecord.value = null
editForm.value = { time: '', type: '', content: '' }
}
function onEditTimeChange(event) {
editForm.value.time = event.detail.value
}
function onEditTypeChange(event) {
editForm.value.type = feedingTypes[event.detail.value]
}
async function saveEditRecord() {
if (!editForm.value.time || !editForm.value.type) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (editForm.value.content && editForm.value.content.length > 20) {
uni.showToast({ title: '喂养详情最多20字', icon: 'none' })
return
}
if (!editingRecord.value) {
uni.showToast({ title: '编辑记录不存在', icon: 'none' })
return
}
const { index, record } = editingRecord.value
const apiData = {
recordId: record.recordId,
babyId: feedStore.getCurrentBabyId(),
recordTime: formatDateTimeString(currentSelectedDate.value, editForm.value.time),
feedingType: getFeedingTypeId(editForm.value.type),
foodDetails: editForm.value.content
}
await feedingRecordsAPI(apiData)
closeEditPopup()
if (currentSelectedDate.value) loadRecordsByDate(currentSelectedDate.value)
}
function goToFeedingAnalysis() {
......@@ -591,73 +654,410 @@ function goBack() {
uni.navigateBack()
}
function addFeedingRecord() {
if (!currentSelectedDate.value) {
uni.showToast({
title: '请先选择日期',
icon: 'none'
})
return
// 宝宝切换相关方法
function showBabySwitch() {
showBabySwitchPopup.value = true
}
function onBabyChange(baby, index) {
feedStore.switchBaby(index)
console.log('切换到宝宝:', baby.name, '索引:', index)
// 切换宝宝后清空缓存,重新加载数据
clearCache()
// 重新加载当前日期的记录
if (currentSelectedDate.value) {
loadRecordsByDate(currentSelectedDate.value)
}
// 重新加载当前月份的日历状态
if (currentMonthKey.value) {
loadCalendarStatus(currentMonthKey.value)
}
}
// 调试:输出图片路径
console.log('add_btn 图片路径:', feedingRecordRes.add_btn)
function calculateBabyAge(birthday) {
const birthDate = new Date(birthday)
const today = new Date()
const diffTime = Math.abs(today - birthDate)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const years = Math.floor(diffDays / 365)
const days = diffDays % 365
if (years > 0) {
return `${years}${days}天`
} else {
return `${days}天`
}
}
// 重置表单
newRecord.value = {
time: getCurrentTime(),
type: '',
content: ''
// API 调用函数
async function loadRecordsByDate(date) {
// 避免重复请求
if (loadingRecords.value.has(date)) {
console.log('该日期正在加载中,跳过重复请求:', date)
return
}
// 检查缓存
const cacheKey = `${date}_${feedStore.getCurrentBabyId()}`
const cachedData = recordsCache.value.get(cacheKey)
if (cachedData && Date.now() - cachedData.timestamp < 5 * 60 * 1000) { // 5分钟缓存
console.log('使用缓存的记录数据:', date)
apiRecords.value[date] = cachedData.data
return
}
try {
const startTime = Date.now()
loadingRecords.value.add(date)
// 只有在没有其他加载中的记录时才显示全局加载状态
if (loadingRecords.value.size === 1) {
isLoading.value = true
}
const babyId = feedStore.getCurrentBabyId()
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsByDate({
babyId: babyId,
date: date
})
const response = await Promise.race([apiPromise, timeoutPromise])
console.log('获取指定日期记录:', date, response)
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (DEBUG_API) {
console.log('API返回数据结构:', JSON.stringify(response, null, 2))
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
console.error('API返回的数据格式不正确,期望数组:', response.data)
apiRecords.value[date] = []
return
}
// 转换API数据格式为页面需要的格式
const records = response.data.map(record => {
// 验证记录格式
if (!record || typeof record !== 'object') {
console.warn('跳过无效的记录:', record)
return null
}
return {
id: record.id, // 保留原始记录ID,用于修改时传递
time: formatTimeFromTimestamp(record.recordTime),
type: getFeedingTypeLabel(record.feedingType),
leftDuration: record.durationLeftSeconds ? formatDuration(record.durationLeftSeconds) : '',
rightDuration: record.durationRightSeconds ? formatDuration(record.durationRightSeconds) : '',
amount: record.volume ? `${record.volume}ml` : '',
content: record.foodDetails || ''
}
}).filter(record => record !== null) // 过滤掉无效记录
// 按时间排序
records.sort((a, b) => a.time.localeCompare(b.time))
apiRecords.value[date] = records
console.log('转换后的记录数据:', records)
// 添加到缓存
recordsCache.value.set(cacheKey, {
data: records,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取记录数据耗时: ${endTime - startTime}ms`)
} else {
apiRecords.value[date] = []
console.log('API返回空数据')
// 缓存空数据
recordsCache.value.set(cacheKey, {
data: [],
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取记录数据耗时: ${endTime - startTime}ms`)
}
} catch (error) {
handleApiError(error, '获取指定日期记录')
// 简单的重试机制(最多重试1次)
if (!loadingRecords.value.has(`${date}_retry`)) {
console.log('尝试重试获取记录数据...')
loadingRecords.value.add(`${date}_retry`)
setTimeout(() => {
loadRecordsByDate(date)
}, 2000) // 2秒后重试
return
}
// 在API失败时不再使用本地mock数据作为fallback
// apiRecords.value[date] = feedingRecords.value[date] || []
// 显示错误提示(可选)
console.warn('API获取记录失败,不使用本地数据作为fallback')
// 注释掉错误提示,避免用户体验不佳
// uni.showToast({
// title: '获取记录失败,显示本地数据',
// icon: 'none',
// duration: 2000
// })
} finally {
loadingRecords.value.delete(date)
// 只有在没有其他加载中的记录时才隐藏全局加载状态
if (loadingRecords.value.size === 0) {
isLoading.value = false
}
}
}
showPopup.value = true
async function loadCalendarStatus(month) {
// 避免重复请求
if (loadingCalendar.value.has(month)) {
console.log('该月份正在加载中,跳过重复请求:', month)
return
}
// 检查缓存
const cacheKey = `${month}_${babyList.value[currentBabyIndex.value]?.id || 1}`
const cachedData = calendarCache.value.get(cacheKey)
if (cachedData && Date.now() - cachedData.timestamp < 10 * 60 * 1000) { // 10分钟缓存
console.log('使用缓存的日历状态数据:', month)
calendarStatus.value = { ...calendarStatus.value, ...cachedData.data }
return
}
try {
const startTime = Date.now()
loadingCalendar.value.add(month)
const babyId = feedStore.getCurrentBabyId()
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsCalendarStatus({
babyId: babyId,
month: month // 格式:YYYY-MM
})
const response = await Promise.race([apiPromise, timeoutPromise])
console.log('获取日历状态:', month, response)
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (DEBUG_API) {
console.log('日历状态API返回数据结构:', JSON.stringify(response, null, 2))
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
console.error('日历状态API返回的数据格式不正确,期望数组:', response.data)
return
}
// 将API返回的日历状态数据转换为对象格式
const statusMap = {}
response.data.forEach(item => {
// 验证项目格式
if (item && typeof item === 'object' && item.date && typeof item.hasRecord === 'boolean') {
statusMap[item.date] = item.hasRecord
} else {
console.warn('跳过无效的日历状态项目:', item)
}
})
calendarStatus.value = { ...calendarStatus.value, ...statusMap }
console.log('日历状态数据:', statusMap)
// 添加到缓存
calendarCache.value.set(cacheKey, {
data: statusMap,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取日历状态耗时: ${endTime - startTime}ms`)
} else {
console.log('日历状态API返回空数据')
// 缓存空数据
calendarCache.value.set(cacheKey, {
data: {},
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取日历状态耗时: ${endTime - startTime}ms`)
}
} catch (error) {
handleApiError(error, '获取日历状态')
// 简单的重试机制(最多重试1次)
if (!loadingCalendar.value.has(`${month}_retry`)) {
console.log('尝试重试获取日历状态...')
loadingCalendar.value.add(`${month}_retry`)
setTimeout(() => {
loadCalendarStatus(month)
}, 2000) // 2秒后重试
return
}
// 在API失败时不再使用本地mock数据作为fallback
// 这里可以根据本地数据生成日历状态
const localStatusMap = {}
Object.keys(apiRecords.value).forEach(date => {
if (date.startsWith(month)) {
localStatusMap[date] = apiRecords.value[date] && apiRecords.value[date].length > 0
}
})
calendarStatus.value = { ...calendarStatus.value, ...localStatusMap }
} finally {
loadingCalendar.value.delete(month)
}
}
function onTimeChange(event) {
newRecord.value.time = event.detail.value
// 统一错误处理函数
function handleApiError(error, context) {
console.error(`${context} 失败:`, error)
// 检查网络状态
uni.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
console.warn('网络连接不可用')
}
}
})
// 根据错误类型提供不同的处理建议
if (error.message && error.message.includes('超时')) {
console.warn('请求超时,可能是网络问题')
} else if (error.message && error.message.includes('API返回错误')) {
console.warn('服务器返回错误,请检查参数')
} else {
console.warn('未知错误,请检查网络连接')
}
}
function onTypeChange(event) {
newRecord.value.type = feedingTypes[event.detail.value]
// 工具函数
function formatTimeFromTimestamp(timestamp) {
if (!timestamp) return ''
let date
// 处理不同的时间戳格式
if (typeof timestamp === 'string') {
// 如果是字符串格式,直接解析
date = new Date(timestamp)
} else if (typeof timestamp === 'number') {
// 如果是数字格式,判断是否为毫秒级时间戳
if (timestamp > 1000000000000) {
// 毫秒级时间戳
date = new Date(timestamp)
} else {
// 秒级时间戳
date = new Date(timestamp * 1000)
}
} else {
date = new Date(timestamp)
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
console.error('无效的时间戳:', timestamp)
return ''
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
function closePopup() {
showPopup.value = false
function formatDuration(seconds) {
if (!seconds || seconds <= 0) return ''
const minutes = Math.floor(seconds / 60)
return `${minutes}min`
}
function saveRecord() {
// 验证表单
if (!newRecord.value.time || !newRecord.value.type || !newRecord.value.content) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
function getFeedingTypeLabel(type) {
const typeMap = {
1: '母乳亲喂',
2: '母乳瓶喂',
3: '奶粉喂养',
4: '辅食'
}
return typeMap[type] || '未知'
}
// 添加记录
const dateKey = currentSelectedDate.value
if (!feedingRecords.value[dateKey]) {
feedingRecords.value[dateKey] = []
function getFeedingTypeId(label) {
const typeMap = {
'母乳亲喂': 1,
'母乳瓶喂': 2,
'奶粉喂养': 3,
'辅食': 4
}
return typeMap[label] || 1 // 默认值
}
feedingRecords.value[dateKey].push({
time: newRecord.value.time,
type: newRecord.value.type,
content: newRecord.value.content
})
// 按时间排序
feedingRecords.value[dateKey].sort((a, b) => a.time.localeCompare(b.time))
function formatDateTimeString(dateString, timeString) {
const date = new Date(dateString);
const [hours, minutes] = timeString.split(':');
date.setHours(parseInt(hours, 10));
date.setMinutes(parseInt(minutes, 10));
return date.toISOString(); // 使用ISO格式的时间戳
}
uni.showToast({
title: '保存成功',
icon: 'success'
})
function loadBabyFeedingRecords(baby) {
// 根据宝宝信息加载对应的喂养记录
console.log('加载宝宝喂养记录:', baby.name)
// 清空之前的API数据和缓存
apiRecords.value = {}
calendarStatus.value = {}
recordsCache.value.clear()
calendarCache.value.clear()
// 重新加载当前选中日期的记录和日历状态
if (currentSelectedDate.value) {
loadRecordsByDate(currentSelectedDate.value)
}
// 加载当前月份的日历状态
const currentMonth = formatMonthString(currentDate.value)
loadCalendarStatus(currentMonth)
}
closePopup()
function formatMonthString(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${year}-${month}`
}
// 移除addFeedingRecord、saveRecord、closePopup等相关方法
function getCurrentTime() {
......@@ -689,8 +1089,29 @@ onLoad(() => {
currentDate.value = new Date(today.getFullYear(), today.getMonth(), 1) // 当前月份的第一天
currentMonthKey.value = `${today.getFullYear()}-${today.getMonth()}` // 设置月份键
console.log('初始化今日日期:', todayString, '当前月份:', currentDate.value, '月份键:', currentMonthKey.value)
// 初始化API数据
initializeApiData()
})
// 初始化API数据
async function initializeApiData() {
console.log('初始化API数据...')
// 加载当前选中日期的记录
if (currentSelectedDate.value) {
console.log('加载当前选中日期记录:', currentSelectedDate.value)
await loadRecordsByDate(currentSelectedDate.value)
}
// 加载当前月份的日历状态
const currentMonth = formatMonthString(currentDate.value)
console.log('加载当前月份日历状态:', currentMonth)
await loadCalendarStatus(currentMonth)
console.log('API数据初始化完成')
}
// 通用的月份切换处理函数(已简化,避免重复调用)
function handleMonthChange(newDate, source = 'unknown') {
console.log(`====== 处理月份切换 (${source}) ======`)
......@@ -704,6 +1125,10 @@ function handleMonthChange(newDate, source = 'unknown') {
// 强制更新
forceUpdate.value++
// 加载新月份的日历状态
const newMonth = formatMonthString(newDate)
loadCalendarStatus(newMonth)
// 使用nextTick确保DOM更新
nextTick(() => {
console.log(`${source}: nextTick后再次强制更新`)
......@@ -726,6 +1151,10 @@ watch(currentDate, (newDate, oldDate) => {
currentMonthKey.value = newMonthKey
forceUpdate.value++
// 加载新月份的日历状态
const newMonth = formatMonthString(newDate)
loadCalendarStatus(newMonth)
nextTick(() => {
console.log('watch: nextTick后再次强制更新')
forceUpdate.value++
......@@ -736,6 +1165,51 @@ watch(currentDate, (newDate, oldDate) => {
onMounted(() => {
// 组件挂载后的操作
})
// 页面卸载时清理资源
onUnmounted(() => {
// 清理缓存
recordsCache.value.clear()
calendarCache.value.clear()
loadingRecords.value.clear()
loadingCalendar.value.clear()
})
// 页面显示时刷新数据
onShow(() => {
// 如果已经有选中日期,刷新当前日期的记录
if (currentSelectedDate.value) {
loadRecordsByDate(currentSelectedDate.value)
}
// 刷新当前月份的日历状态
const currentMonth = formatMonthString(currentDate.value)
loadCalendarStatus(currentMonth)
})
// 测试函数(开发时使用)
function testApiIntegration() {
console.log('=== 测试API集成 ===')
console.log('当前选中日期:', currentSelectedDate.value)
console.log('当前月份:', formatMonthString(currentDate.value))
console.log('当前宝宝ID:', babyList.value[currentBabyIndex.value]?.id)
console.log('API记录数据:', apiRecords.value)
console.log('日历状态数据:', calendarStatus.value)
console.log('缓存状态:', {
recordsCache: recordsCache.value.size,
calendarCache: calendarCache.value.size
})
}
// 功能总结:
// 1. 自动加载:页面加载、显示、日期切换时自动调用API
// 2. 智能缓存:避免重复请求,提升性能
// 3. 错误处理:网络错误、超时、服务器错误等
// 4. 重试机制:失败时自动重试,提高成功率
// 5. 数据验证:确保API返回数据的正确性
// 6. 性能监控:记录API调用耗时
// 7. 调试支持:可控制的调试信息输出
// 8. 资源清理:页面卸载时清理缓存和定时器
</script>
<style lang="scss" scoped>
......@@ -779,6 +1253,12 @@ onMounted(() => {
.baby-info {
display: flex;
flex-direction: column;
cursor: pointer;
transition: opacity 0.3s ease;
&:active {
opacity: 0.7;
}
.baby-name {
font-size: 32rpx;
......@@ -1056,6 +1536,18 @@ onMounted(() => {
// padding: 20rpx;
padding-bottom: 180rpx; // 为浮动按钮留出空间
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.empty-state {
display: flex;
justify-content: center;
......
<template>
<view class="shengzhang-test-result-container">
<view class="result-bg">
<image class="result-bg-img0" src="/static/shengzhangTestResult/resultBg0.jpg" mode="aspectFit"></image>
<image class="result-bg-img1" src="/static/shengzhangTestResult/resultBg1.jpg" mode="aspectFit"></image>
</view>
<!-- 返回按钮 -->
<!-- <view class="back-btn" @click="backHandler">
<text class="back-text"></text>
</view> -->
<image @tap="backHandler" class="back-btn" :src="`/static/shengzhangTool/backBtn.png`"></image>
<text class="title">生长测评</text>
<view class="content-wrapper">
<!-- 顶部导航标签 -->
<view class="nav-tabs">
<view class="tab-item" :class="{ 'active': activeTab === 'latest' }" @click="switchTab('latest')">
<text class="tab-text">最新</text>
</view>
<view class="tab-item" :class="{ 'active': activeTab === 'history' }" @click="switchTab('history')">
<text class="tab-text">历史</text>
</view>
<!-- <view class="tab-decoration">
<text class="star"></text>
<text class="star"></text>
<text class="star"></text>
</view> -->
</view>
<!-- 最新内容容器 -->
<view class="latest-content" :class="{ 'active': activeTab === 'latest' }">
<!-- 宝宝信息卡片 -->
<view class="baby-info-card">
<view class="card-header">
<image class="name-icon" src="/static/shengzhangTestResult/nameIcon.png" mode="aspectFit"></image>
<text class="card-title">宝宝名称</text>
</view>
<view class="baby-basic-info">
<text class="gender"></text>
<text class="age">2月21天</text>
<text class="test-date">测评于2025年06月06日</text>
</view>
<view class="measurement-summary">
<view class="values-row">
<text class="measurement-value">60.6cm</text>
<text class="measurement-value">5.8kg</text>
<text class="measurement-value">39.0cm</text>
<text class="measurement-value">16.0</text>
</view>
<view class="labels-row">
<view class="measurement-item">
<text class="measurement-label">身高</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">体重</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">头围</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">BMI</text>
<text class="measurement-status normal">正常</text>
</view>
</view>
</view>
<view class="growth-evaluation">
<text class="evaluation-text">宝宝发育的非常棒,身高、体重和头围都处于正常的发育水平。TA与大多数宝宝一样,正在健康苗壮地成长。建议定期观察和记录宝宝的体格发育数据,这样有利于评估宝宝的生长发育情况,能够及时发现宝宝生长发育过程中的存在的问题和异常。</text>
</view>
</view>
<!-- 生长情况卡片 -->
<view class="growth-status-card">
<view class="card-header">
<image class="status-icon" src="/static/shengzhangTestResult/shengzhangqingkuangIcon.png" mode="aspectFit"></image>
<text class="card-title">生长情况</text>
</view>
<view class="legend">
<view class="legend-item">
<view class="legend-color too-low"></view>
<text class="legend-text">偏低</text>
</view>
<view class="legend-item">
<view class="legend-color slightly-low"></view>
<text class="legend-text">略低</text>
</view>
<view class="legend-item">
<view class="legend-color normal"></view>
<text class="legend-text">正常</text>
</view>
<view class="legend-item">
<view class="legend-color slightly-high"></view>
<text class="legend-text">略高</text>
</view>
<view class="legend-item">
<view class="legend-color too-high"></view>
<text class="legend-text">偏高</text>
</view>
</view>
<view class="measurement-bars">
<view class="bar-item">
<view class="value-triangle-container">
<text class="bar-value">60.6cm 正常</text>
<image class="triangle" src="/static/shengzhangTestResult/triangle.png" mode="aspectFit"></image>
</view>
<view class="bar-row">
<text class="measurement-label">身高</text>
<image class="value-bar" src="/static/shengzhangTestResult/valueBar.png" mode="aspectFit"></image>
<text class="bar-percentage">超过25%同龄宝宝</text>
</view>
</view>
<view class="bar-item">
<view class="value-triangle-container">
<text class="bar-value">5.75kg 正常</text>
<image class="triangle" src="/static/shengzhangTestResult/triangle.png" mode="aspectFit"></image>
</view>
<view class="bar-row">
<text class="measurement-label">体重</text>
<image class="value-bar" src="/static/shengzhangTestResult/valueBar.png" mode="aspectFit"></image>
<text class="bar-percentage">超过25%同龄宝宝</text>
</view>
</view>
<view class="bar-item">
<view class="value-triangle-container">
<text class="bar-value">39.0cm 正常</text>
<image class="triangle" src="/static/shengzhangTestResult/triangle.png" mode="aspectFit"></image>
</view>
<view class="bar-row">
<text class="measurement-label">头围</text>
<image class="value-bar" src="/static/shengzhangTestResult/valueBar.png" mode="aspectFit"></image>
<text class="bar-percentage">超过25%同龄宝宝</text>
</view>
</view>
<view class="bar-item">
<view class="value-triangle-container">
<text class="bar-value">15.0 正常</text>
<image class="triangle" src="/static/shengzhangTestResult/triangle.png" mode="aspectFit"></image>
</view>
<view class="bar-row">
<text class="measurement-label">BMI</text>
<image class="value-bar" src="/static/shengzhangTestResult/valueBar.png" mode="aspectFit"></image>
<text class="bar-percentage">超过25%同龄宝宝</text>
</view>
</view>
</view>
</view>
<!-- 生长曲线卡片 -->
<view class="growth-curve-card">
<view class="card-header">
<text class="card-title">生长曲线</text>
</view>
<view class="curve-tabs">
<view class="curve-tab" :class="{ 'active': activeCurveTab === 'height' }" @click="switchCurveTab('height')">
<image v-if="activeCurveTab === 'height'"class="tab-icon" src="/static/shengzhangTestResult/shengaoTab0.png" mode="aspectFit"></image>
<image v-else class="tab-icon" src="/static/shengzhangTestResult/shengaoTab1.png" mode="aspectFit"></image>
</view>
<view class="curve-tab" :class="{ 'active': activeCurveTab === 'weight' }" @click="switchCurveTab('weight')">
<image v-if="activeCurveTab === 'weight'" class="tab-icon" src="/static/shengzhangTestResult/tizhongTab0.png" mode="aspectFit"></image>
<image v-else class="tab-icon" src="/static/shengzhangTestResult/tizhongTab1.png" mode="aspectFit"></image>
</view>
<view class="curve-tab" :class="{ 'active': activeCurveTab === 'head' }" @click="switchCurveTab('head')">
<image v-if="activeCurveTab === 'head'" class="tab-icon" src="/static/shengzhangTestResult/touweiTab0.png" mode="aspectFit"></image>
<image v-else class="tab-icon" src="/static/shengzhangTestResult/touweiTab1.png" mode="aspectFit"></image>
</view>
</view>
<view class="graph-legend">
<view class="legend-item">
<view class="legend-color slightly-low"></view>
<text class="legend-text">略低</text>
</view>
<view class="legend-item">
<view class="legend-color normal"></view>
<text class="legend-text">正常</text>
</view>
<view class="legend-item">
<view class="legend-color slightly-high"></view>
<text class="legend-text">略高</text>
</view>
<view class="legend-item">
<view class="legend-color baby-record"></view>
<text class="legend-text">宝宝记录</text>
</view>
</view>
<view class="graph-title-y">
<text class="graph-title-text">{{getYAxisLabel()}}</text>
</view>
<view class="graph-container">
<scroll-view class="graph-scroll" scroll-x="true" :scroll-left="scrollLeft" @scroll="onScroll">
<view class="graph-content" :style="{ width: totalWidth + 'px' }">
<canvas class="curve-canvas" canvas-id="growthCurve" :style="{ width: totalWidth + 'px', height: '100%' }"></canvas>
</view>
</scroll-view>
</view>
</view>
<view class="curve-tips" @click="showCurveTips">
<image class="tips-icon" src="/static/shengzhangTestResult/shengzhangTips.png" mode="aspectFit"></image>
</view>
<!-- 专家咨询按钮 -->
<view class="expert-consult-btn" @click="consultExpert">
<image class="consult-bg" src="/static/shengzhangTestResult/zhuanjiazixunBtn.png" mode="aspectFit"></image>
</view>
</view>
</view>
<!-- 历史内容容器 -->
<view class="history-content" :class="{ 'active': activeTab === 'history' }">
<view class="history-list">
<view class="history-item" v-for="(item, index) in historyList" :key="index" @click="selectHistoryItem(item)">
<view class="history-card">
<view class="card-header">
<image class="name-icon" src="/static/shengzhangTestResult/nameIcon.png" mode="aspectFit"></image>
<text class="card-title">宝宝名称</text>
</view>
<view class="baby-basic-info">
<text class="gender">{{item.gender}}</text>
<text class="age">{{item.age}}</text>
<text class="test-date">测评于{{item.testDate}}</text>
</view>
<view class="measurement-summary">
<view class="values-row">
<text class="measurement-value">{{item.height}}cm</text>
<text class="measurement-value">{{item.weight}}kg</text>
<text class="measurement-value">{{item.head}}cm</text>
<text class="measurement-value">{{item.bmi}}</text>
</view>
<view class="labels-row">
<view class="measurement-item">
<text class="measurement-label">身高</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">体重</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">头围</text>
<text class="measurement-status normal">正常</text>
</view>
<view class="measurement-item">
<text class="measurement-label">BMI</text>
<text class="measurement-status normal">正常</text>
</view>
</view>
</view>
<view class="growth-evaluation">
<text class="evaluation-text">{{item.evaluation}}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 导航标签状态
const activeTab = ref('latest') // 默认显示最新内容
// 生长曲线标签状态
const activeCurveTab = ref('height')
// 滑动相关状态
const scrollLeft = ref(0)
const totalWidth = ref(1080) // 36个月 * 10px
// 历史数据列表
const historyList = ref([
{
gender: '男',
age: '2月21天',
testDate: '2025年06月06日',
height: '60.6',
weight: '5.8',
head: '39.0',
bmi: '16.0',
evaluation: '宝宝发育的非常棒,身高、体重和头围都处于正常的发育水平。TA与大多数宝宝一样,正在健康苗壮地成长。建议定期观察和记录宝宝的体格发育数据,这样有利于评估宝宝的生长发育情况,能够及时发现宝宝生长发育过程中的存在的问题和异常。'
},
{
gender: '男',
age: '1月15天',
testDate: '2025年05月01日',
height: '55.2',
weight: '4.5',
head: '36.8',
bmi: '14.8',
evaluation: '宝宝发育良好,各项指标都在正常范围内。建议继续保持良好的喂养习惯,定期进行生长发育监测。'
},
{
gender: '男',
age: '3月05天',
testDate: '2025年07月20日',
height: '62.1',
weight: '6.2',
head: '40.5',
bmi: '16.1',
evaluation: '宝宝生长发育稳定,身高、体重增长符合预期。头围发育正常,整体健康状况良好。'
}
])
// 生长曲线数据
const curveData = ref([
{ month: 1.5, height: 56 },
{ month: 3.5, height: 67 },
{ month: 7, height: 72 }
])
// 生成0-36个月的数据点
/**
* @param {number} startValue - 起始值(0个月时的数值)
* @param {number} endValue - 结束值(36个月时的数值)
* @param {string} type - 数据类型('height'|'weight'|'head')
* @returns {Array} 包含36个月数据点的数组
*/
const generateCurveData = (startValue, endValue, type) => {
const data = []
for (let i = 0; i <= 36; i++) {
const value = startValue + (endValue - startValue) * (i / 36)
const point = { month: i }
point[type] = Math.round(value * 10) / 10
data.push(point)
}
return data
}
// 标准生长曲线数据(多条线,延长到36个月)
const standardCurves = ref({
height: {
slightlyLow: generateCurveData(45, 105, 'height'),
normal: generateCurveData(50, 110, 'height'),
slightlyHigh: generateCurveData(55, 115, 'height')
},
weight: {
slightlyLow: generateCurveData(2.5, 15.0, 'weight'),
normal: generateCurveData(3.0, 16.5, 'weight'),
slightlyHigh: generateCurveData(3.5, 18.0, 'weight')
},
head: {
slightlyLow: generateCurveData(32, 52, 'head'),
normal: generateCurveData(34, 54, 'head'),
slightlyHigh: generateCurveData(36, 56, 'head')
}
})
// 切换导航标签
/**
* @param {string} tab - 要切换的标签名称('latest'|'history')
*/
const switchTab = (tab) => {
activeTab.value = tab
console.log('切换到标签:', tab)
}
// 选择历史记录项
/**
* @param {Object} item - 选中的历史记录项
*/
const selectHistoryItem = (item) => {
console.log('选择历史记录:', item)
// 这里可以添加跳转到详情页或更新当前显示数据的逻辑
uni.showToast({
title: '已选择历史记录',
icon: 'success'
})
}
// 切换生长曲线标签
/**
* @param {string} tab - 要切换的曲线类型('height'|'weight'|'head')
*/
const switchCurveTab = (tab) => {
activeCurveTab.value = tab
console.log('切换到曲线标签:', tab)
// 根据不同的标签更新曲线数据
if (tab === 'height') {
curveData.value = [
{ month: 1.5, height: 56 },
{ month: 3.5, height: 67 },
{ month: 7, height: 72 }
]
} else if (tab === 'weight') {
curveData.value = [
{ month: 1.5, weight: 4.2 },
{ month: 3.5, weight: 6.8 },
{ month: 7, weight: 8.5 }
]
} else if (tab === 'head') {
curveData.value = [
{ month: 1.5, head: 36 },
{ month: 3.5, head: 41 },
{ month: 7, head: 44 }
]
}
// 重新绘制曲线
setTimeout(() => {
drawGrowthCurve()
}, 100)
}
// 获取Y轴标签
/**
* @returns {string} 根据当前曲线类型返回对应的Y轴标签
*/
const getYAxisLabel = () => {
if (activeCurveTab.value === 'height') {
return '身高 (cm)'
} else if (activeCurveTab.value === 'weight') {
return '体重 (kg)'
} else if (activeCurveTab.value === 'head') {
return '头围 (cm)'
}
return '身高 (cm)'
}
// 获取Y轴刻度
/**
* @returns {Array<number>} 根据当前曲线类型返回对应的Y轴刻度数组
*/
const getYTicks = () => {
if (activeCurveTab.value === 'height') {
return [40, 50, 60, 70, 80, 90, 100]
} else if (activeCurveTab.value === 'weight') {
return [0, 3, 6, 9, 12, 15, 18]
} else if (activeCurveTab.value === 'head') {
return [0, 3, 6, 9, 12, 15, 18]
}
return [40, 50, 60, 70, 80, 90, 100]
}
// 显示生长曲线提示
/**
* 显示生长曲线说明弹窗
*/
const showCurveTips = () => {
console.log('显示生长曲线提示')
uni.showModal({
title: '生长曲线说明',
content: '生长曲线是根据大量同龄宝宝的生长发育数据绘制的标准曲线,用于评估宝宝的生长发育是否正常。',
showCancel: false
})
}
// 专家咨询
/**
* 处理专家咨询按钮点击事件
*/
const consultExpert = () => {
console.log('专家在线咨询')
uni.showToast({
title: '正在连接专家...',
icon: 'loading'
})
}
// 首页组件逻辑
/**
* 处理返回按钮点击事件,尝试返回上一页或跳转到首页
*/
const backHandler = () => {
try {
uni.navigateBack({
success: () => {
console.log('返回成功')
},
fail: backFailHandler
})
} catch (error) {
console.log('error=', error)
jump({
type: JumpType.INNER,
url: "/pages/index/index"
})
}
}
onMounted(() => {
// 初始化绘制曲线
setTimeout(() => {
drawGrowthCurve()
}, 100)
})
// 绘制生长曲线
/**
* 绘制完整的生长曲线图表,包括坐标轴、标准曲线和宝宝记录曲线
*/
const drawGrowthCurve = () => {
const query = uni.createSelectorQuery()
query.select('.curve-canvas').boundingClientRect((rect) => {
if (rect) {
const ctx = uni.createCanvasContext('growthCurve')
const width = rect.width
const height = rect.height
// 清空画布
ctx.clearRect(0, 0, width, height)
// 设置画布边距
const margin = { top: 20, right: 20, bottom: 25, left: 30 }
const chartWidth = width - margin.left - margin.right
const chartHeight = height - margin.top - margin.bottom
// 获取当前数据类型
const currentType = activeCurveTab.value
const currentCurves = standardCurves.value[currentType]
// 绘制坐标轴
drawAxes(ctx, width, height, margin, chartWidth, chartHeight, currentType)
// 绘制标准曲线
drawStandardCurves(ctx, currentCurves, margin, chartWidth, chartHeight, currentType)
// 绘制宝宝记录曲线
drawBabyCurve(ctx, curveData.value, margin, chartWidth, chartHeight, currentType)
ctx.draw()
}
}).exec()
}
// 绘制坐标轴
/**
* @param {Object} ctx - Canvas上下文对象
* @param {number} width - Canvas总宽度
* @param {number} height - Canvas总高度
* @param {Object} margin - 边距对象 {top, right, bottom, left}
* @param {number} chartWidth - 图表区域宽度
* @param {number} chartHeight - 图表区域高度
* @param {string} type - 数据类型('height'|'weight'|'head')
*/
const drawAxes = (ctx, width, height, margin, chartWidth, chartHeight, type) => {
console.log('drawAxes', width, height, margin, chartWidth, chartHeight, type);
// 绘制X轴
ctx.beginPath()
ctx.setStrokeStyle('#000')
ctx.setLineWidth(2)
ctx.moveTo(margin.left, height - margin.bottom)
ctx.lineTo(width - margin.right, height - margin.bottom)
ctx.stroke()
// 先绘制水平网格线
const yTicks = getYTicks()
yTicks.forEach((tick, index) => {
const y = margin.top + (1 - index / (yTicks.length - 1)) * chartHeight
// 绘制水平网格线
ctx.beginPath()
ctx.setStrokeStyle('#faf2e7')
ctx.setLineWidth(1)
ctx.moveTo(margin.left, y)
ctx.lineTo(width - margin.right, y)
ctx.stroke()
})
// 绘制Y轴
ctx.beginPath()
ctx.setStrokeStyle('#000')
ctx.setLineWidth(1)
ctx.moveTo(margin.left, margin.top - 8)
ctx.lineTo(margin.left, height - margin.bottom)
ctx.stroke()
// 绘制X轴箭头
ctx.beginPath()
ctx.moveTo(width - margin.right, height - margin.bottom)
ctx.lineTo(width - margin.right - 8, height - margin.bottom - 4)
ctx.lineTo(width - margin.right - 8, height - margin.bottom + 4)
ctx.closePath()
ctx.setFillStyle('#000')
ctx.fill()
// 绘制Y轴箭头
ctx.beginPath()
ctx.moveTo(margin.left, margin.top - 16)
ctx.lineTo(margin.left - 4, margin.top + 8 - 16)
ctx.lineTo(margin.left + 4, margin.top + 8 - 16)
ctx.closePath()
ctx.fill()
// 绘制X轴刻度(每个月都显示)
const xTicks = []
for (let i = 0; i <= 36; i++) {
xTicks.push(i)
}
xTicks.forEach((tick) => {
const x = margin.left + (tick / 36) * chartWidth
const y = height - margin.bottom
// 刻度线
// ctx.beginPath()
// ctx.setStrokeStyle('#000')
// ctx.setLineWidth(1)
// ctx.moveTo(x, y)
// ctx.lineTo(x, y) // 缩短为原来的30%
// ctx.stroke()
// 刻度标签
ctx.setFillStyle('#000')
ctx.setFontSize(12)
ctx.setTextAlign('center')
ctx.fillText(tick.toString(), x, y + 20)
})
// 绘制Y轴刻度和标签
yTicks.forEach((tick, index) => {
const x = margin.left
const y = margin.top + (1 - index / (yTicks.length - 1)) * chartHeight
// 刻度线
// ctx.beginPath()
// ctx.setStrokeStyle('#999')
// ctx.setLineWidth(1)
// ctx.moveTo(x, y)
// ctx.lineTo(x - 5, y)
// ctx.stroke()
// 刻度标签
ctx.setFillStyle('#000')
ctx.setFontSize(12)
ctx.setTextAlign('right')
ctx.fillText(tick.toString(), x - 10, y + 4)
})
// 绘制坐标轴标签
ctx.setFillStyle('#000')
ctx.setFontSize(14)
ctx.setTextAlign('center')
ctx.fillText('月龄', width / 2, height - 10)
// ctx.setTextAlign('center')
// ctx.setFontSize(14)
// const yLabel = getYAxisLabel()
// ctx.save()
// ctx.translate(20, height / 2)
// // ctx.rotate(-Math.PI / 2)
// ctx.fillText(yLabel, 0, -100)
// ctx.restore()
}
// 绘制标准曲线
/**
* @param {Object} ctx - Canvas上下文对象
* @param {Object} curves - 标准曲线数据对象 {slightlyLow, normal, slightlyHigh}
* @param {Object} margin - 边距对象 {top, right, bottom, left}
* @param {number} chartWidth - 图表区域宽度
* @param {number} chartHeight - 图表区域高度
* @param {string} type - 数据类型('height'|'weight'|'head')
*/
const drawStandardCurves = (ctx, curves, margin, chartWidth, chartHeight, type) => {
// 绘制略低曲线 - 黄色
drawCurve(ctx, curves.slightlyLow, margin, chartWidth, chartHeight, type, '#ffeaa7', 2)
// 绘制正常曲线 - 浅绿色
drawCurve(ctx, curves.normal, margin, chartWidth, chartHeight, type, '#a8e6cf', 2)
// 绘制略高曲线 - 紫色
drawCurve(ctx, curves.slightlyHigh, margin, chartWidth, chartHeight, type, '#d4a5f5', 2)
}
// 绘制单条曲线
/**
* @param {Object} ctx - Canvas上下文对象
* @param {Array} data - 曲线数据点数组,每个元素包含 {month, height/weight/head}
* @param {Object} margin - 边距对象 {top, right, bottom, left}
* @param {number} chartWidth - 图表区域宽度
* @param {number} chartHeight - 图表区域高度
* @param {string} type - 数据类型('height'|'weight'|'head')
* @param {string} color - 曲线颜色(十六进制颜色值)
* @param {number} lineWidth - 曲线线宽
*/
const drawCurve = (ctx, data, margin, chartWidth, chartHeight, type, color, lineWidth) => {
ctx.beginPath()
ctx.setStrokeStyle(color)
ctx.setLineWidth(lineWidth)
data.forEach((point, index) => {
const x = margin.left + (point.month / 36) * chartWidth
let y = 0
// 根据不同的数据类型计算y坐标
if (type === 'height') {
y = margin.top + (1 - (point.height - 40) / 80) * chartHeight
} else if (type === 'weight') {
y = margin.top + (1 - (point.weight - 2) / 16) * chartHeight
} else if (type === 'head') {
y = margin.top + (1 - (point.head - 30) / 30) * chartHeight
}
if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
})
ctx.stroke()
}
// 绘制宝宝记录曲线
/**
* @param {Object} ctx - Canvas上下文对象
* @param {Array} data - 宝宝记录数据点数组,每个元素包含 {month, height/weight/head}
* @param {Object} margin - 边距对象 {top, right, bottom, left}
* @param {number} chartWidth - 图表区域宽度
* @param {number} chartHeight - 图表区域高度
* @param {string} type - 数据类型('height'|'weight'|'head')
*/
const drawBabyCurve = (ctx, data, margin, chartWidth, chartHeight, type) => {
// 绘制折线
ctx.beginPath()
ctx.setStrokeStyle('#8b4513')
ctx.setLineWidth(1)
data.forEach((point, index) => {
const x = margin.left + (point.month / 36) * chartWidth
let y = 0
// 根据不同的数据类型计算y坐标
if (type === 'height') {
y = margin.top + (1 - (point.height - 40) / 80) * chartHeight
} else if (type === 'weight') {
y = margin.top + (1 - (point.weight - 2) / 16) * chartHeight
} else if (type === 'head') {
y = margin.top + (1 - (point.head - 30) / 30) * chartHeight
}
if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
})
ctx.stroke()
// 绘制数据点
data.forEach((point) => {
const x = margin.left + (point.month / 36) * chartWidth
let y = 0
// 根据不同的数据类型计算y坐标
if (type === 'height') {
y = margin.top + (1 - (point.height - 40) / 80) * chartHeight
} else if (type === 'weight') {
y = margin.top + (1 - (point.weight - 2) / 16) * chartHeight
} else if (type === 'head') {
y = margin.top + (1 - (point.head - 30) / 30) * chartHeight
}
ctx.beginPath()
ctx.setFillStyle('#8b4513')
ctx.arc(x, y, 6, 0, 2 * Math.PI)
ctx.fill()
})
}
// 获取X轴刻度
/**
* @returns {Array<number>} 返回0-36个月的X轴刻度数组
*/
// const getXTicks = () => {
// const ticks = []
// for (let i = 0; i <= 36; i++) {
// ticks.push(i)
// }
// return ticks
// }
// 处理滑动事件
/**
* @param {Object} e - 滑动事件对象
* @param {number} e.detail.scrollLeft - 当前滑动位置
*/
const onScroll = (e) => {
scrollLeft.value = e.detail.scrollLeft
}
</script>
<style lang="less" scoped>
.shengzhang-test-result-container {
width: 100%;
height: 2700rpx;
// box-sizing: border-box;
position: relative;
overflow: hidden;
.result-bg{
top: 0rpx;
width: 100%;
height: 2700rpx;
position: absolute;
.result-bg-img0{
position: absolute;
top: 0rpx;
width: 100%;
height: 1300rpx;
}
.result-bg-img1{
position: absolute;
top: 1300rpx;
width: 100%;
height: 1400rpx;
}
}
// 内容容器
.content-wrapper {
padding-left: 30rpx;
padding-right: 30rpx;
}
// 最新内容容器
.latest-content {
display: none;
&.active {
display: block;
}
}
// 历史内容容器
.history-content {
padding-left: 30rpx;
padding-right: 30rpx;
display: none;
&.active {
display: block;
}
.history-list {
margin-top: 46rpx;
}
.history-item {
margin-bottom: 30rpx;
}
.history-card {
background-color: #fff;
border-radius: 24rpx;
padding: 50rpx 35rpx 35rpx 35rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.8;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.name-icon,
.status-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.card-title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
}
.baby-basic-info {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.gender {
font-size: 24rpx;
color: #000;
margin-right: 20rpx;
}
.age {
font-size: 24rpx;
color: #000;
margin-right: 20rpx;
}
.test-date {
font-size: 24rpx;
color: #000;
}
}
.measurement-summary {
display: flex;
flex-direction: column;
margin-bottom: 30rpx;
.values-row {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
.measurement-value {
font-size: 32rpx;
color: #333;
font-weight: bold;
flex: 1;
text-align: center;
}
}
.labels-row {
display: flex;
justify-content: space-between;
.measurement-item {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
justify-content: center;
.measurement-label {
font-size: 24rpx;
color: #666;
margin-right: 8rpx;
}
.measurement-status {
font-size: 22rpx;
color: #52c41a;
display: flex;
align-items: center;
&.normal {
&::before {
content: "";
width: 16rpx;
height: 16rpx;
background-image: url('/static/shengzhangTestResult/duihaoIcon.png');
background-size: contain;
background-repeat: no-repeat;
margin-right: 6rpx;
}
}
}
}
}
}
.growth-evaluation {
.evaluation-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
}
}
}
// 返回按钮
.back-btn {
position: absolute;
top: 119rpx;
left: 30rpx;
width: 29rpx;
height: 29rpx;
}
.title{
position: absolute;
top: 112rpx;
font-size: 34rpx;
font-weight: 500;
width: 100%;
text-align: center;
}
// 顶部导航标签
.nav-tabs {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-top: 202rpx;
width: 260rpx;
margin-left: 5rpx;
.tab-item {
width: 116rpx;
height: 51rpx;
// padding: 20rpx 40rpx;
margin-right: 30rpx;
border-radius: 25rpx;
background-color: #fffbed;
transition: all 0.3s ease;
&.active {
background-color: #b27c1e;
.tab-text {
color: #ffffff;
}
}
.tab-text {
font-size: 28rpx;
color: #b27c1e;
font-weight: 500;
align-items: center;
justify-content: center;
display: flex;
width: 100%;
height: 100%;
}
}
// .tab-decoration {
// position: absolute;
// right: 20rpx;
// top: 50%;
// transform: translateY(-50%);
// .star {
// font-size: 20rpx;
// color: #b27c1e;
// margin-left: 10rpx;
// opacity: 0.6;
// }
// }
}
// 宝宝信息卡片
.baby-info-card {
background-color: #fff;
border-radius: 24rpx;
padding: 50rpx 35rpx 35rpx 35rpx;
margin-top: 46rpx;
// height: 100%;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
.card-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.name-icon,
.status-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.card-title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
}
.baby-basic-info {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.gender {
font-size: 24rpx;
color: #000;
margin-right: 20rpx;
}
.age {
font-size: 24rpx;
color: #000;
margin-right: 20rpx;
}
.test-date {
font-size: 24rpx;
color: #000;
}
}
.measurement-summary {
display: flex;
flex-direction: column;
margin-bottom: 30rpx;
.values-row {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
.measurement-value {
font-size: 32rpx;
color: #333;
font-weight: bold;
flex: 1;
text-align: center;
}
}
.labels-row {
display: flex;
justify-content: space-between;
.measurement-item {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
justify-content: center;
.measurement-label {
font-size: 24rpx;
color: #666;
margin-right: 8rpx;
}
.measurement-status {
font-size: 22rpx;
color: #52c41a;
display: flex;
align-items: center;
&.normal {
&::before {
content: "";
width: 16rpx;
height: 16rpx;
background-image: url('/static/shengzhangTestResult/duihaoIcon.png');
background-size: contain;
background-repeat: no-repeat;
margin-right: 6rpx;
}
}
}
}
}
}
.growth-evaluation {
.evaluation-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
}
}
// 生长情况卡片
.growth-status-card {
background-color: #fff;
border-radius: 24rpx;
padding: 50rpx 35rpx 15rpx 35rpx;
margin-top: 40rpx;
// box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
.card-header {
display: flex;
align-items: center;
.name-icon,
.status-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.card-title {
font-size: 32rpx;
color: #000;
font-weight: bold;
}
}
.legend {
display: flex;
justify-content: space-between;
margin-top: 46rpx;
.legend-item {
display: flex;
align-items: center;
.legend-color {
width: 20rpx;
height: 20rpx;
border-radius: 4rpx;
margin-right: 8rpx;
&.too-low {
background-color: #ffede0;
}
&.slightly-low {
background-color: #fde0a5;
}
&.normal {
background-color: #89caa2;
}
&.slightly-high {
background-color: #f3d1e9;
}
&.too-high {
background-color: #a78dbc;
}
}
.legend-text {
font-size: 22rpx;
color: #000;
}
}
}
.measurement-bars {
margin-top: 26rpx;
.bar-item {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
.value-triangle-container {
display: flex;
flex-direction: column;
align-items: center;
.bar-value {
font-size: 24rpx;
color: #b27c1e;
}
.triangle {
top:8rpx;
width: 20rpx;
height: 20rpx;
}
}
.bar-row {
display: flex;
align-items: center;
justify-content: space-between;
.measurement-label {
font-size: 28rpx;
color: #000;
width: 80rpx;
// margin-top: 3rpx;
}
.value-bar {
flex: 1;
height: 30rpx;
}
.bar-percentage {
font-size: 24rpx;
color: #000;
width: 200rpx;
text-align: right;
}
}
}
}
}
// 生长曲线卡片
.growth-curve-card {
background-color: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-top: 40rpx;
// box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
.card-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.name-icon,
.status-icon {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.card-title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
}
.curve-tabs {
display: flex;
justify-content: space-around;
// justify-items: center;
width: 466rpx;
height: 141rpx;
margin-bottom: 37rpx;
margin-top: 37rpx;
margin-left: 78rpx;
background-color: #f6f8fa;
border-radius: 70rpx;
.curve-tab {
margin-top: 8rpx;
display: flex;
align-items: center;
justify-content: center;
width: 132rpx;
height: 131rpx;
border-radius: 50%;
// transition: all 0.3s ease;
// background-color: #ffffff;
// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
position: relative;
&.active {
// background-color: #e0e0e0;
// box-shadow: none;
}
.tab-icon {
width: 100%;
height: 100%;
}
}
}
.graph-legend {
display: flex;
justify-content: space-around;
.legend-item {
display: flex;
align-items: center;
.legend-color {
width: 20rpx;
height: 20rpx;
border-radius: 4rpx;
margin-right: 6rpx;
&.slightly-low {
background-color: #fde0a5; // 黄色
}
&.normal {
background-color: #89caa2; // 浅绿色
}
&.slightly-high {
background-color: #f3d1e9; // 紫色
}
&.baby-record {
background-color: #b27c1e; // 棕色
}
}
.legend-text {
font-size: 20rpx;
color: #000;
}
}
}
.graph-title-y{
position: relative;
margin-top: 30rpx;
margin-left: 20rpx;
.graph-title-text{
font-size: 20rpx;
color: #000;
}
}
.graph-container {
position: relative;
height: 400rpx;
background-color: #fff;
border-radius: 10rpx;
overflow: hidden;
.graph-scroll {
width: 100%;
height: 100%;
white-space: nowrap;
}
.graph-content {
height: 100%;
display: inline-block;
}
.curve-canvas {
height: 100%;
display: block;
}
}
}
.curve-tips {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-top: 60rpx;
margin-bottom: 60rpx;
width: 100%;
height: 24rpx;
.tips-icon{
width: 198rpx;
height: 100%;
}
}
// 专家咨询按钮
.expert-consult-btn {
position: relative;
width: 100%;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
.consult-bg {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
}
}
</style>
\ No newline at end of file
<template>
<view class="shengzhang-tools-container">
<swiper
class="banner-swiper"
:indicator-dots="swiperData.length > 1"
:autoplay="swiperData.length > 1"
:circular="swiperData.length > 1"
indicator-color="#dfddd9"
indicator-active-color="#b27c1e"
:indicator-top="596"
>
<swiper-item v-for="(item, index) in swiperData" :key="index">
<image
class="banner-img"
:src="`${item?.bannerImg}`"
mode="aspectFill"
@click="bannerHandler(item)"
/>
</swiper-item>
</swiper>
<image @tap="backHandler" class="btnback" :src="`/static/shengzhangTool/backBtn.png`"></image>
<text class="title">生长测评</text>
<view class="info-container">
<!-- 顶部宝宝信息区域 -->
<view class="baby-info-section">
<view class="baby-avatar">
<image class="avatar-img" :src="babyAvatar" mode="aspectFill"></image>
</view>
<view class="baby-details">
<view class="baby-name-row">
<text class="baby-name">{{ babyName }}</text>
<image class="change-baby-btn" @click="changeBaby" src="/static/shengzhangTool/changeBaby.png" mode="aspectFit"></image>
</view>
<view class="baby-info-row">
<view class="gender-age">
<image class="gender-icon" :src="babyGender == 'M' ? '/static/shengzhangTool/sex1.png' : '/static/shengzhangTool/sex0.png'" mode="aspectFit"></image>
<text class="age-text">{{ babyAge }}</text>
</view>
<text class="birth-date">{{ formatDate(babyBirthday) }}</text>
</view>
</view>
<view class="record-btn" @click="viewRecords">
<text class="record-text">测评记录</text>
<image class="arrow-icon" src="/static/shengzhangTool/close.png" mode="aspectFit"></image>
</view>
</view>
<!-- 分割线 -->
<image class="divider-line" src="/static/shengzhangTool/line.png" mode="aspectFit"></image>
<!-- 测评信息区域 -->
<view class="test-info-section">
<view class="test-date-row">
<text class="label">本次测评日期</text>
<view class="date-container" @click="showDatePicker">
<text class="date-value">{{ selectedDate }}</text>
<image class="edit-icon" src="/static/shengzhangTool/editIcon.png" mode="aspectFit"></image>
</view>
</view>
<!-- 分割线 -->
<image class="divider-line" src="/static/shengzhangTool/line.png" mode="aspectFit"></image>
<view class="feeding-row">
<text class="label">宝宝喂养方式</text>
<view class="feeding-select" @click="showFeedingPopup">
<text class="feeding-value">{{ selectedFeedText }}</text>
<image class="dropdown-icon" src="/static/shengzhangTool/close.png" mode="aspectFit"></image>
</view>
</view>
</view>
<!-- 测量数据区域 -->
<view class="measurement-section">
<view class="measurement-header">
<view class="measurement-item">
<text class="measurement-title">宝宝身高</text>
</view>
<view class="measurement-item">
<text class="measurement-title">宝宝体重</text>
</view>
<view class="measurement-item">
<text class="measurement-title">宝宝头围</text>
</view>
</view>
<!-- 替换输入框区域为picker-view -->
<view class="input-section">
<view class="input-item">
<view class="input-container">
<image class="input-bg" src="/static/shengzhangTool/numBg.png" mode="aspectFit"></image>
<picker-view
class="measurement-picker"
:class="{ 'measurement-picker-disabled': isHeightTipActive }"
:value="heightPickerValue"
@change="isHeightTipActive ? null : onHeightChange"
:indicator-style="indicatorStyle"
indicator-class="date-picker"
>
<picker-view-column>
<view v-for="(item, index) in heightRange" :key="index" class="picker-item">
{{ item }}
</view>
</picker-view-column>
</picker-view>
<text class="unit">cm</text>
</view>
</view>
<view class="input-item">
<view class="input-container">
<image class="input-bg" src="/static/shengzhangTool/numBg.png" mode="aspectFit"></image>
<picker-view
class="measurement-picker"
:class="{ 'measurement-picker-disabled': isWeightTipActive }"
:value="weightPickerValue"
@change="isWeightTipActive ? null : onWeightChange"
:indicator-style="indicatorStyle"
indicator-class="date-picker"
>
<picker-view-column>
<view v-for="(item, index) in weightRange" :key="index" class="picker-item">
{{ item }}
</view>
</picker-view-column>
</picker-view>
<text class="unit">kg</text>
</view>
</view>
<view class="input-item">
<view class="input-container">
<image class="input-bg" src="/static/shengzhangTool/numBg.png" mode="aspectFit"></image>
<picker-view
class="measurement-picker"
:class="{ 'measurement-picker-disabled': isHeadTipActive }"
:value="headPickerValue"
@change="isHeadTipActive ? null : onHeadChange"
:indicator-style="indicatorStyle"
indicator-class="date-picker"
>
<picker-view-column>
<view v-for="(item, index) in headRange" :key="index" class="picker-item">
{{ item }}
</view>
</picker-view-column>
</picker-view>
<text class="unit">cm</text>
</view>
</view>
</view>
<!-- 底部提示 -->
<view class="tips-section">
<view class="tip-item0" @click="toggleHeightTip">
<text class="tip-text" :class="{ 'tip-text-active': isHeightTipActive }">暂无数据</text>
</view>
<view class="tip-item1" @click="toggleWeightTip">
<text class="tip-text" :class="{ 'tip-text-active': isWeightTipActive }">暂无数据</text>
</view>
<view class="tip-item2" @click="toggleHeadTip">
<text class="tip-text" :class="{ 'tip-text-active': isHeadTipActive }">暂无数据</text>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<view class="submit-btn" @click="submitData">
<image class="submit-bg" src="/static/shengzhangTool/submitBtn.png" mode="aspectFit"></image>
<!-- <text class="submit-text">确认提交</text> -->
</view>
<view class="bottom-tip" @click="onClickTips">
<image class="tip-icon" src="/static/shengzhangTool/tips.png" mode="aspectFit"></image>
<!-- <text class="tip-desc">如何选择你的宝宝身高、体重、头围</text> -->
</view>
</view>
</view>
<view class="guide-container" v-if="guideIndex != -1" @click="guideHandler">
<image v-if="guideIndex == 0" class="guide-img0" src="/static/shengzhangTool/guide0.png" mode="aspectFit"></image>
<image v-if="guideIndex == 1" class="guide-img1" src="/static/shengzhangTool/guide1.png" mode="aspectFit"></image>
<image v-if="guideIndex == 2" class="guide-img2" src="/static/shengzhangTool/guide2.png" mode="aspectFit"></image>
</view>
</view>
<!-- 在页面底部添加弹窗组件 -->
<BabySwitchPopup
v-model:visible="showBabySwitchPopup"
:babyList="babyList"
v-model:selectedIndex="currentBabyIndex"
@change="onBabyChange"
/>
<!-- 喂养方式弹窗 -->
<BabyFeedSwitchPopup
v-model:visible="showFeedSwitchPopup"
v-model:selectedIndex="currentFeedIndex"
@change="onFeedChange"
/>
<!-- 日期选择弹窗 -->
<DatePickerPopup
v-model:visible="showDatePickerPopup"
v-model:selectedDate="selectedDate"
v-model:babyBirthday="babyBirthday"
@change="onDateChange"
/>
<!-- 宝宝测评提示弹窗 -->
<BabyTestTipsPopup
v-model:visible="showBabyTestTipsPopup"
/>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import BabySwitchPopup from '@/components/BabySwitchPopup.vue'
import BabyFeedSwitchPopup from '@/components/BabyFeedSwitchPopup.vue'
import DatePickerPopup from '@/components/DatePickerPopup.vue'
import BabyTestTipsPopup from '@/components/BabyTestTipsPopup.vue'
import { growthHome, guideCompleted, assessmentSave } from '../../api/shengzhangTools'
import { onLoad } from "@dcloudio/uni-app";
import { throttleTap } from '../../utils/index.js';
const swiperData = ref([
{ bannerImg: '/static/shengzhangTool/banner1.png' },
{ bannerImg: '/static/shengzhangTool/banner2.png' },
{ bannerImg: '/static/shengzhangTool/banner3.png' }
])
const babyName = ref('宝宝名称')
const babyAge = ref('8月龄')
const babyBirthday = ref('2024-10-20')
const babyGender = ref('M')
const babyAvatar = ref('/static/shengzhangTool/avatar.png')
const guideFlag = ref(false);
const bannerHandler = (item) => {
console.log(item)
}
// 首页组件逻辑
const backHandler = () => {
try {
uni.navigateBack({
success: () => {
console.log('返回成功')
},
fail: backFailHandler
})
} catch (error) {
console.log('error=', error)
jump({
type: JumpType.INNER,
url: "/pages/index/index"
})
}
}
const backFailHandler = (err) => {
console.log('backFailHandler=', err)
jump({
type: JumpType.INNER,
url: "/pages/index/index"
})
}
// 替换原有的变量
const height = ref('50.3')
const weight = ref('3.32')
const headCircumference = ref('34.5')
const babyId = ref(0);
const assessmentDate = ref('');
const feedingType = ref('');
// 添加picker-view相关数据
const indicatorStyle = `height: 40px; border: none;`
// 生成数值范围
const generateRange = (min, max, step = 0.1) => {
const range = []
for (let i = min; i <= max; i += step) {
if (step < 0.1) {
range.push(parseFloat(i.toFixed(2)))
} else {
range.push(parseFloat(i.toFixed(1)))
}
}
return range
}
// 日期选择器相关状态
const showDatePickerPopup = ref(false)
const selectedDate = ref('2025-06-06')
const showDatePicker = () => {
console.log('显示日期选择器')
showDatePickerPopup.value = true
}
// 处理日期选择变化
const onDateChange = (date) => {
console.log('选择了日期:', date)
selectedDate.value = date
}
const showFeedingPopup = () => {
console.log('显示喂养方式弹窗')
showFeedSwitchPopup.value = true
currentFeedIndex.value = 1 // 默认选中"母乳+奶粉混合喂养"
}
// 身高范围 (40-80cm)
const heightRange = generateRange(40, 80, 0.1)
const heightPickerValue = ref([heightRange.indexOf(parseFloat(height.value))])
// 体重范围 (2-10kg)
const weightRange = generateRange(2, 10, 0.01)
const weightPickerValue = ref([weightRange.indexOf(parseFloat(weight.value))])
// 头围范围 (30-50cm)
const headRange = generateRange(30, 50, 0.1)
const headPickerValue = ref([headRange.indexOf(parseFloat(headCircumference.value))])
// picker-view change事件处理
const onHeightChange = (e) => {
const index = e.detail.value[0]
height.value = heightRange[index].toString()
heightPickerValue.value = [index]
}
const onWeightChange = (e) => {
const index = e.detail.value[0]
weight.value = weightRange[index].toString()
weightPickerValue.value = [index]
}
const onHeadChange = (e) => {
const index = e.detail.value[0]
headCircumference.value = headRange[index].toString()
headPickerValue.value = [index]
}
// 其他方法保持不变
const changeBaby = () => {
console.log('切换宝宝')
showBabySwitchPopup.value = true
currentBabyIndex.value = 0 // 默认选中第一个宝宝
}
const viewRecords = () => {
console.log('查看测评记录')
}
const convertFeedingType = (type) => {
if(type == '纯母乳'){
return 'BREAST_MILK'
}else if(type == '母乳+奶粉混合喂养'){
return 'MIXED'
}else if(type == '奶粉'){
return 'FORMULA'
}else if(type == '母乳+辅食'){
return 'BREAST_MILK_CF'
}else if(type == '奶粉+辅食'){
return 'FORMULA_CF'
}
}
const submitData = throttleTap(async () => {
const submitData = {
babyId: babyId.value,
height: height.value,
weight: weight.value,
headCircumference: headCircumference.value,
assessmentDate: assessmentDate.value,
feedingType: convertFeedingType(selectedFeedText.value)
};
console.log('提交数据', submitData);
const data = await assessmentSave(submitData);
if(data.success){
uni.showToast({
title: '提交成功',
icon: 'success'
})
}
}, 1000)
// 添加以下数据
const showBabySwitchPopup = ref(false)
const currentBabyIndex = ref(0)
// 喂养方式弹窗相关状态
const showFeedSwitchPopup = ref(false)
const currentFeedIndex = ref(1) // 默认选中"母乳+奶粉混合喂养"
const selectedFeedText = ref('母乳+奶粉混合喂养')
// 示例宝宝列表数据
const babyList = ref([
{
name: '宝宝名称',
gender: 1, // 1: 男孩, 0: 女孩
birthday: '2024-10-20',
avatar: '/static/shengzhangTool/avatar.png'
},
{
name: '宝宝名称',
gender: 0,
birthday: '2022-04-20',
avatar: '/static/shengzhangTool/avatar.png'
}
])
// 处理宝宝选择变化
const onBabyChange = (baby, index) => {
console.log('选择了宝宝:', baby, index)
// 这里可以更新页面上的宝宝信息
// 比如更新宝宝名称、性别、生日等
}
// 处理喂养方式选择变化
const onFeedChange = (feedOption, index) => {
console.log('选择了喂养方式:', feedOption, index)
selectedFeedText.value = feedOption.name
currentFeedIndex.value = index
// 这里可以更新页面上显示的喂养方式
// 比如更新 feeding-select 中的文本
}
// 身高提示状态管理
const isHeightTipActive = ref(false)
// 处理身高提示点击
const toggleHeightTip = () => {
if (!canClick(0)) return
pushCount(0)
isHeightTipActive.value = !isHeightTipActive.value
console.log('身高提示状态:', isHeightTipActive.value)
}
// 体重提示状态管理
const isWeightTipActive = ref(false)
// 处理体重提示点击
const toggleWeightTip = () => {
if (!canClick(1)) return
pushCount(1)
isWeightTipActive.value = !isWeightTipActive.value
console.log('体重提示状态:', isWeightTipActive.value)
}
// 头围提示状态管理
const isHeadTipActive = ref(false)
// 处理头围提示点击
const toggleHeadTip = () => {
if (!canClick(2)) return
pushCount(2)
isHeadTipActive.value = !isHeadTipActive.value
console.log('头围提示状态:', isHeadTipActive.value)
}
const selectCountArr = ref([])
// 数组内存在当前索引可以点击,如果不存在,在判断是不是已经选择两个了,如果已经选择两个,则不能点击
const canClick = (val) => {
if (selectCountArr.value.indexOf(val) == -1) {
if (selectCountArr.value.length >= 2) {
return false
}
return true
} else {
return true
}
}
const pushCount = (val) => {
if (selectCountArr.value.indexOf(val) == -1) {
selectCountArr.value.push(val)
} else {
selectCountArr.value.splice(selectCountArr.value.indexOf(val), 1)
}
}
// 宝宝测评提示弹窗状态
const showBabyTestTipsPopup = ref(false)
const onClickTips = () => {
console.log('显示宝宝测评提示弹窗')
showBabyTestTipsPopup.value = true
}
const guideIndex = ref(-1);
const guideHandler = async () => {
guideIndex.value++
if (guideIndex.value > 2) {
const data = await guideCompleted();
if(data.success){
guideFlag.value = true;
guideIndex.value = -1;
}else{
//引导页完成失败,提示用户
}
}
}
// 获取页面参数
onLoad((options) => {
if (options.babyId) {
babyId.value = parseInt(options.babyId)
console.log('获取到的babyId:', babyId.value)
}
})
onMounted(async () => {
const {data} = await growthHome(babyId.value);
// const data = {"babyId":1234,"babyName":"小强","gender":"M","monthAge":3,"avatar":"https://momclub.feihe.com/pmall/momclub-picture/integral/1009/yuerBtn.png","birthDate":"2018-10-28 14:06:45","guideFlag":false};
babyName.value = data.babyName
babyAge.value = data.monthAge + '月龄'
babyBirthday.value = data.birthDate
babyGender.value = data.gender;
babyAvatar.value = data.avatar
assessmentDate.value = selectedDate.value;
feedingType.value = selectedFeedText.value;
guideFlag.value = data.guideFlag;
if (guideFlag.value) {
guideIndex.value = -1;
}else{
guideIndex.value = 0;
}
})
const formatDate = (timestamp) => {
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
console.error('无效的时间戳:', timestamp);
return '';
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
</script>
<style lang="less" scoped>
.shengzhang-tools-container {
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #fdf6eb;
.banner-swiper {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 191rpx;
width: 687rpx;
height: 176rpx;
.banner-img {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
.btnback {
position: absolute;
top: 119rpx;
left: 30rpx;
width: 29rpx;
height: 29rpx;
}
.title {
position: absolute;
top: 111rpx;
left: 50%;
transform: translateX(-50%);
font-size: 34rpx;
color: #000;
font-weight: 600;
}
.info-container {
width: 750rpx;
height: 1210rpx;
margin: 414rpx auto 0;
background-color: #ffffff;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
padding: 40rpx 30rpx;
box-sizing: border-box;
overflow: hidden;
.baby-info-section {
display: flex;
align-items: center;
padding: 0 10rpx;
.baby-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
.avatar-img {
width: 100%;
height: 100%;
}
}
.baby-details {
flex: 1;
.baby-name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.baby-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 20rpx;
}
.change-baby-btn {
width: 139rpx;
height: 37rpx;
}
}
.baby-info-row {
display: flex;
align-items: center;
.gender-age {
display: flex;
align-items: center;
margin-right: 30rpx;
.gender-icon {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.age-text {
font-size: 26rpx;
color: #666;
}
}
.birth-date {
font-size: 26rpx;
color: #666;
}
}
}
.record-btn {
display: flex;
align-items: center;
.record-text {
font-size: 26rpx;
color: #666;
margin-right: 8rpx;
}
.arrow-icon {
width: 20rpx;
height: 20rpx;
}
}
}
.divider-line {
width: 100%;
height: 2rpx;
margin: 20rpx 0;
}
.test-info-section {
.test-date-row {
display: flex;
align-items: center;
justify-content: space-between;
.label {
font-size: 28rpx;
color: #333;
}
.date-container {
display: flex;
align-items: center;
.date-value {
font-size: 28rpx;
color: #666;
margin-right: 10rpx;
}
.edit-icon {
width: 24rpx;
height: 24rpx;
}
}
}
.feeding-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 25rpx;
.label {
font-size: 28rpx;
color: #333;
}
.feeding-select {
display: flex;
align-items: center;
.feeding-value {
font-size: 28rpx;
color: #666;
margin-right: 8rpx;
}
.dropdown-icon {
width: 20rpx;
height: 20rpx;
}
}
}
}
.measurement-section {
margin-bottom: 40rpx;
margin-top: 80rpx;
.measurement-header {
display: flex;
margin-bottom: 25rpx;
.measurement-item {
flex: 1;
text-align: center;
.measurement-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
}
}
.input-section {
display: flex;
.input-item {
flex: 1;
display: flex;
justify-content: center;
.input-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.input-bg {
position: absolute;
width: 105rpx;
height: 44rpx;
z-index: 1;
}
.measurement-picker {
position: relative;
z-index: 2;
width: 70rpx;
height: 370rpx;
background: transparent;
:deep(.date-picker::before) {
content: none;
}
:deep(.date-picker::after) {
content: none;
}
&.measurement-picker-disabled {
opacity: 0.4;
pointer-events: none;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #333;
font-weight: bold;
height: 40rpx;
line-height: 40rpx;
}
}
.unit {
position: absolute;
z-index: 2;
font-size: 24rpx;
color: #666;
margin-left: 162rpx;
font-weight: 600;
}
}
}
}
.tips-section {
display: flex;
.tip-item0 {
flex: 1;
text-align: center;
&:hover {
opacity: 0.8;
}
.tip-text {
font-size: 24rpx;
color: #1d1e26;
text-decoration: underline;
&.tip-text-active {
color: red !important;
}
}
}
.tip-item1 {
flex: 1;
text-align: center;
&:hover {
opacity: 0.8;
}
.tip-text {
font-size: 24rpx;
color: #1d1e26;
text-decoration: underline;
&.tip-text-active {
color: red !important;
}
}
}
.tip-item2 {
flex: 1;
text-align: center;
&:hover {
opacity: 0.8;
}
.tip-text {
font-size: 24rpx;
color: #1d1e26;
text-decoration: underline;
&.tip-text-active {
color: red !important;
}
}
}
}
}
.submit-section {
margin-top: 80rpx;
.submit-btn {
position: relative;
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
.submit-bg {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
.submit-text {
position: relative;
z-index: 2;
font-size: 32rpx;
color: #fff;
font-weight: bold;
}
}
.bottom-tip {
display: flex;
align-items: center;
justify-content: center;
.tip-icon {
width: 424rpx;
height: 23rpx;
}
}
}
}
.guide-container{
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1000;
.guide-img0{
width: 100%;
height: 1624rpx;
position: absolute;
top: 0;
left: 0;
}
.guide-img1{
width: 100%;
height: 1624rpx;
position: absolute;
top: 0;
left: 0;
}
.guide-img2{
width: 100%;
height: 1624rpx;
position: absolute;
top: 0;
left: 0;
}
}
}
</style>
\ No newline at end of file
import { defineStore } from "pinia";
import { fetchBabyInfo } from "../api/user.js";
export const useFeedStore = defineStore("feed", {
state: () => {
return {
// 宝宝列表和当前选中宝宝的管理
babyList: [],
currentBabyIndex: 0,
};
},
actions: {
/**
* 获取宝宝信息
*/
async loadBabyInfo() {
const { data } = await fetchBabyInfo();
console.log("babyInfo", data);
if (data?.memberId !== "not_login") {
// 处理宝宝列表数据
this.processBabyList(data);
}
},
/**
* 处理宝宝列表数据
*/
processBabyList(data) {
if (data.allBabyBaseInfo && Array.isArray(data.allBabyBaseInfo)) {
// 转换接口数据格式为组件需要的格式
this.babyList = data.allBabyBaseInfo.map(baby => ({
id: baby.id,
name: baby.babyName || baby.content?.babyName || '未命名',
avatar: baby.content?.babyAvatar || '/static/feedingIndex/v1/avatar.png',
gender: baby.content?.babyGender,
birthday: baby.content?.babyBirthday,
babyStage: baby.babyStage,
babyType: baby.babyType,
typeName: baby.typeName,
selected: baby.selected
}))
// 找到选中的宝宝,如果没有选中的则选择第一个
const selectedIndex = this.babyList.findIndex(baby => baby.selected)
this.currentBabyIndex = selectedIndex >= 0 ? selectedIndex : 0
console.log('处理后的宝宝列表:', this.babyList)
console.log('当前选中的宝宝索引:', this.currentBabyIndex)
} else {
// 如果没有宝宝数据,使用默认数据
this.babyList = [{
id: 1,
name: '默认宝宝',
avatar: '/static/feedingIndex/v1/avatar.png',
gender: null,
birthday: null,
babyStage: 1,
babyType: null,
typeName: '默认',
selected: true
}]
this.currentBabyIndex = 0
}
},
/**
* 切换宝宝
*/
switchBaby(index) {
if (index >= 0 && index < this.babyList.length) {
this.currentBabyIndex = index
console.log('切换到宝宝:', this.babyList[index].name, '索引:', index)
}
},
/**
* 获取当前选中的宝宝
*/
getCurrentBaby() {
return this.babyList[this.currentBabyIndex] || null
},
/**
* 获取当前选中的宝宝ID
*/
getCurrentBabyId() {
const currentBaby = this.getCurrentBaby()
return currentBaby ? currentBaby.id : 1
},
},
});
\ No newline at end of file
......@@ -21,6 +21,7 @@ export const useUserStore = defineStore("userInfo", {
babyInfo: null,
memberInfo: null,
babyNickCache: [],
cepingjieguoInfo:null,
};
},
actions: {
......
......@@ -1472,6 +1472,7 @@
//banner点击事件
const bannerHandler = (item) => {
md.sensorLogTake({
xcxClick: "积分服务页-首屏页面点击",
pageName: "积分服务页-首屏",
......
......@@ -232,6 +232,8 @@ const babyInfo = computed(() => userStore?.babyInfo || {});
const showRegisterLayer = ref(false);
const showBabySwitcher = ref(false);
const babyId = ref(0);
const handleHot = (e) => {
const type = e.currentTarget.dataset.type;
md.sensorLog(e);
......@@ -245,6 +247,7 @@ const handleHot = (e) => {
}
};
// 页面跳转
const navigateTo = (url) => {
uni.navigateTo({
......@@ -299,7 +302,13 @@ const handleToolClick = async (item) => {
},
});
} else {
jump({ type: item.link.type, url: item.link.url });
const extra = item.link.extra;
if(extra && extra.babyId){
jump({ type: item.link.type, url: item.link.url+'?babyId='+extra.babyId});
}else{
jump({ type: item.link.type, url: item.link.url});
}
}
};
......@@ -323,9 +332,8 @@ const handleEditProfile = (e) => {
md.sensorLog(e);
const type =
userStore.babyInfo?.allBabyBaseInfo?.length == 0 ? "add" : "edit";
const babyId = userStore.babyInfo?.allBabyBaseInfo.find(
const type = userStore.babyInfo?.allBabyBaseInfo?.length == 0 ? "add" : "edit";
babyId.value = userStore.babyInfo?.allBabyBaseInfo.find(
(item) => item.selected
)?.id;
......@@ -436,11 +444,32 @@ onMounted(async () => {
await pageCfgStore.fetchCfg();
initData();
hideLoading();
babyId.value = userStore.babyInfo?.allBabyBaseInfo.find(
(item) => item.selected
)?.id;
console.log('babyIdsdfsdfsdfsdfsdfsdfdsfsdf=', babyId.value);
const a = {
"bgUrl": "my/babytest.png",
"desc": "生长测评",
"link": {
"extra": {babyId: babyId.value},
"type": 1,
"url": "/pages/shengzhangTools/shengzhangTools"
},
"title": "生长测评"
}
toolList.value.push(a);
});
watch([() => userStore.userInfo, () => userStore.babyInfo], () => {
console.log("userInfo/babyInfo变化", userStore.userInfo, userStore.babyInfo);
initData();
babyId.value = userStore.babyInfo?.allBabyBaseInfo.find(
(item) => item.selected
)?.id;
});
// 定义页面配置
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment