Commit 33f625f8 authored by spc's avatar spc

feeding

parent cb5235b5
......@@ -22,13 +22,17 @@
</view>
<view class="time-section">
<text class="time-label">喂养时间</text>
<text class="time-value">2025-06-01 22:22</text>
<image class="edit-icon" src="/static/feedingIndex/v1/icon_modify.png" />
<uni-datetime-picker ref="timePickerRef" v-model="currentTime" type="datetime" :clear-icon="false" :border="false" @change="onTimeChange" :start="earliestDateString" :end="latestDateString">
<view class="time-display">
<text class="time-value">{{ formatCurrentTime() }}</text>
<image class="edit-icon" src="/static/feedingIndex/v1/icon_modify.png" />
</view>
</uni-datetime-picker>
</view>
</view>
<!-- 喂养记录 -->
<view class="feeding-records">
<view class="feeding-records" @click="goToFeedingRecord">
<text class="records-link">喂养记录</text>
<image class="arrow-right" src="/static/feedingIndex/v1/icon_arrow_yellow.png"></image>
</view>
......@@ -137,49 +141,54 @@
<!-- 辅食表单 -->
<view v-if="selectedType === 'food'" class="food-form">
<!-- 已选择的辅食展示区域 -->
<view class="selected-items">
<text>{{ feedingData.food.selectedItems.join(',') }}</text>
<text>{{ feedingData.food.selectedItems.join(',') || '请选择辅食' }}</text>
</view>
<!-- 辅食分类标题和操作按钮 -->
<view class="food-categories">
<text class="category-title">辅食分类</text>
<text class="complete-btn">完成</text>
<view class="category-actions">
<text v-if="foodSelectionState.isEditMode" class="complete-btn" @click="exitEditMode">完成</text>
<text v-else class="delete-btn" @click="enterEditMode">删除</text>
</view>
</view>
<!-- 辅食分类列表 -->
<view class="category-list">
<view class="category-item">
<text class="category-name">主食</text>
<text class="add-btn">+</text>
<view class="selected-tags">
<text class="tag">米粉 ✕</text>
<text class="tag">面条 ✕</text>
</view>
</view>
<view class="category-item">
<text class="category-name">蔬菜</text>
<text class="add-btn">+</text>
<view class="selected-tags">
<text class="tag">山药 ✕</text>
<text class="tag">萝卜 ✕</text>
<text class="tag active">土豆 ✕</text>
<text class="tag">豆 ✕</text>
<text class="tag">青瓜 ✕</text>
<text class="tag">白菜 ✕</text>
</view>
<text class="expand-arrow"></text>
</view>
<view class="category-item">
<text class="category-name">水果</text>
<text class="add-btn">+</text>
<view class="selected-tags">
<text class="tag">香蕉 ✕</text>
<text class="tag active">苹果 ✕</text>
<text class="tag">牛油果 ✕</text>
</view>
</view>
<view class="category-item">
<text class="category-name">其他</text>
<text class="add-btn">+</text>
<view v-for="(category, categoryName) in foodCategories" :key="categoryName" class="category-item">
<text class="category-name">{{ categoryName }}</text>
<text class="add-btn" :class="{ disabled: foodSelectionState.isEditMode }" @click="showAddFoodPopup(categoryName)">+</text>
<view class="selected-tags">
<text class="tag">鱼油 ✕</text>
<!-- 默认辅食项 -->
<text
v-for="item in category.items"
:key="item"
class="tag"
:class="{
active: feedingData.food.selectedItems.includes(item),
editing: foodSelectionState.isEditMode
}"
@click="toggleFoodSelection(item)"
>
{{ item }}
<text v-if="foodSelectionState.isEditMode" class="delete-x" @click.stop="removeFoodItem(categoryName, item)"></text>
</text>
<!-- 自定义辅食项 -->
<text
v-for="item in category.customItems"
:key="`custom-${item}`"
class="tag"
:class="{
active: feedingData.food.selectedItems.includes(item),
editing: foodSelectionState.isEditMode
}"
@click="toggleFoodSelection(item)"
>
{{ item }}
<text v-if="foodSelectionState.isEditMode" class="delete-x" @click.stop="removeFoodItem(categoryName, item)"></text>
</text>
</view>
</view>
</view>
......@@ -199,15 +208,19 @@
</view>
<!-- 麦克风图标 -->
<view class="voice-microphone" @click="toggleRecording">
<view class="voice-microphone"
@touchstart="startRecording"
@touchend="stopRecording"
@touchcancel="stopRecording">
<image class="microphone-icon"
:src="isRecording ? '/static/feedingIndex/v1/icon_luyining.png' : '/static/feedingIndex/v1/icon_luyin.png'"
:src="voiceRecognitionState.isRecording ? '/static/feedingIndex/v1/icon_luyining.gif' : '/static/feedingIndex/v1/icon_luyin.png'"
mode="aspectFit" />
</view>
<!-- 点击提示 -->
<!-- 录音提示 -->
<view class="voice-click-tip">
<text class="click-text">点击识别语音</text>
<text class="click-text" v-if="voiceRecognitionState.isRecording">正在录音...</text>
<text class="click-text" v-else>长按录音</text>
</view>
</view>
......@@ -264,13 +277,82 @@
</view>
</view>
</view>
<!-- 添加辅食弹窗 -->
<uni-popup ref="addFoodPopup" type="center" :mask-click="false">
<view class="add-food-popup">
<view class="popup-header">
<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="请输入辅食名称"
maxlength="10"
/>
</view>
<view class="popup-buttons">
<text class="cancel-btn" @click="cancelAddFood">取消</text>
<text class="confirm-btn" @click="confirmAddFood">添加</text>
</view>
</view>
</uni-popup>
<!-- 语音识别结果页面 -->
<view v-if="voiceRecognitionState.showResultPage" class="voice-result-page">
<view class="result-container">
<!-- 识别结果标题 -->
<view class="result-header">
<text class="result-title">识别结果</text>
</view>
<!-- 喂养信息展示 -->
<view class="feeding-info">
<view class="info-item">
<text class="info-label">喂养时间</text>
<text class="info-value">{{ formatCurrentTime() }}</text>
</view>
<view class="info-item">
<text class="info-label">喂养方式</text>
<text class="info-value">{{ getFeedingTypeLabel() }}</text>
</view>
<view class="info-item">
<text class="info-label">喂养详情</text>
<input
class="detail-input"
v-model="voiceRecognitionState.recognizedText"
placeholder="请输入喂养详情"
maxlength="20"
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="result-actions">
<text class="re-recognize-btn" @click="reRecognize">不对,重新识别</text>
<text class="complete-record-btn" @click="completeVoiceRecord">完成记录</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
// 弹窗引用
const addFoodPopup = ref(null)
const swiperData = ref([{ bannerImg: '/static/feedingIndex/v1/banner.png' }, { bannerImg: '/static/feedingIndex/v1/banner.png' }, { bannerImg: '/static/feedingIndex/v1/banner.png' }]);
const indicatorStyle = `height: 40px; border: none;`
// 当前时间,默认为当前时间
const currentTime = ref(new Date().getTime())
// 日期范围限制
const earliestDateString = ref('2020-01-01')
const latestDateString = ref('2030-12-31')
const bannerHandler = (item) => {
console.log(item);
}
......@@ -294,6 +376,42 @@ const isRecording = ref(false)
const isLeftTimerRunning = ref(false)
const isRightTimerRunning = ref(false)
// 辅食数据结构
const foodCategories = ref({
主食: {
items: ['米粉', '面条', '馒头', '包子', '粥'],
customItems: []
},
蔬菜: {
items: ['山药', '萝卜', '土豆', '豆', '青瓜', '白菜', '胡萝卜', '南瓜'],
customItems: []
},
水果: {
items: ['香蕉', '苹果', '牛油果', '橙子', '梨'],
customItems: []
},
其他: {
items: ['鱼油', '维生素'],
customItems: []
}
})
// 辅食选择状态
const foodSelectionState = ref({
isEditMode: false, // 是否处于编辑模式
showAddPopup: false, // 是否显示添加弹窗
currentCategory: '', // 当前添加的分类
newFoodItem: '' // 新添加的辅食项
})
// 语音识别状态
const voiceRecognitionState = ref({
isRecording: false, // 是否正在录音
showResultPage: false, // 是否显示识别结果页面
recognizedText: '', // 识别结果文本
recordingDuration: 0 // 录音时长
})
// 为每种喂养方式设置独立的数据
const feedingData = ref({
breastfeeding: {
......@@ -424,6 +542,11 @@ function onPickerChange(e) {
function setRecordMethod(method) {
recordMethods.value[selectedType.value] = method
// 如果切换到语音模式,检查录音权限
if (method === 'voice') {
checkVoicePermission()
}
}
function toggleRecording() {
......@@ -497,6 +620,27 @@ function goBack() {
uni.navigateBack()
}
function goToFeedingRecord() {
uni.navigateTo({
url: '/pages/feedingRecord/feedingRecord'
})
}
// 时间选择器相关方法
function onTimeChange(value) {
currentTime.value = value
}
function formatCurrentTime() {
const date = new Date(currentTime.value)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
function getTabImage() {
const currentMethod = recordMethods.value[selectedType.value]
if (currentMethod === 'manual') {
......@@ -532,6 +676,305 @@ function formatTotalDuration() {
const seconds = totalSeconds % 60
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// 辅食选择相关方法
function toggleFoodSelection(item) {
// 在编辑模式时不允许选择
if (foodSelectionState.value.isEditMode) {
return
}
const selectedItems = feedingData.value.food.selectedItems
const index = selectedItems.indexOf(item)
if (index > -1) {
// 如果已选中,则取消选中
selectedItems.splice(index, 1)
} else {
// 如果未选中,则添加选中
selectedItems.push(item)
}
}
function enterEditMode() {
foodSelectionState.value.isEditMode = true
}
function exitEditMode() {
foodSelectionState.value.isEditMode = false
}
function showAddFoodPopup(categoryName) {
// 在编辑模式下不允许添加
if (foodSelectionState.value.isEditMode) {
uni.showToast({
title: '编辑模式下不能添加辅食',
icon: 'none'
})
return
}
foodSelectionState.value.currentCategory = categoryName
foodSelectionState.value.newFoodItem = ''
foodSelectionState.value.showAddPopup = true
// 打开弹窗
uni.showModal({
title: '添加辅食',
content: '请输入辅食名称(最多10个字)',
editable: true,
placeholderText: '请输入辅食名称',
success: (res) => {
if (res.confirm && res.content) {
addCustomFoodItem(categoryName, res.content)
}
}
})
}
function cancelAddFood() {
foodSelectionState.value.showAddPopup = false
foodSelectionState.value.newFoodItem = ''
}
function confirmAddFood() {
const itemName = foodSelectionState.value.newFoodItem.trim()
const categoryName = foodSelectionState.value.currentCategory
if (!itemName) {
uni.showToast({
title: '请输入辅食名称',
icon: 'none'
})
return
}
addCustomFoodItem(categoryName, itemName)
cancelAddFood()
}
function addCustomFoodItem(categoryName, itemName) {
// 检查字数限制
if (itemName.length > 10) {
uni.showToast({
title: '辅食名称不能超过10个字',
icon: 'none'
})
return
}
// 检查分类下的自定义辅食数量限制
const category = foodCategories.value[categoryName]
if (category.customItems.length >= 20) {
uni.showToast({
title: '该分类下最多可添加20个自定义辅食',
icon: 'none'
})
return
}
// 检查是否已存在
if (category.items.includes(itemName) || category.customItems.includes(itemName)) {
uni.showToast({
title: '该辅食已存在',
icon: 'none'
})
return
}
// 添加自定义辅食
category.customItems.push(itemName)
uni.showToast({
title: '添加成功',
icon: 'success'
})
}
function removeFoodItem(categoryName, itemName) {
const category = foodCategories.value[categoryName]
// 从默认辅食中移除
const defaultIndex = category.items.indexOf(itemName)
if (defaultIndex > -1) {
category.items.splice(defaultIndex, 1)
}
// 从自定义辅食中移除
const customIndex = category.customItems.indexOf(itemName)
if (customIndex > -1) {
category.customItems.splice(customIndex, 1)
}
// 从已选择的辅食中移除
const selectedIndex = feedingData.value.food.selectedItems.indexOf(itemName)
if (selectedIndex > -1) {
feedingData.value.food.selectedItems.splice(selectedIndex, 1)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
// 语音识别相关方法
function checkVoicePermission() {
// 检查录音权限
uni.getSetting({
success: (res) => {
if (!res.authSetting['scope.record']) {
// 没有录音权限,请求授权
uni.authorize({
scope: 'scope.record',
success: () => {
console.log('录音权限授权成功')
},
fail: () => {
// 用户拒绝授权
uni.showModal({
title: '需要录音权限',
content: '为了使用语音识别功能,需要获取录音权限。请在设置中开启录音权限。',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
// 打开设置页面
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.record']) {
console.log('录音权限已开启')
}
}
})
}
}
})
}
})
} else {
console.log('已有录音权限')
}
}
})
}
function startRecording() {
// 再次检查录音权限
uni.getSetting({
success: (res) => {
if (!res.authSetting['scope.record']) {
uni.showToast({
title: '请先授权录音权限',
icon: 'none'
})
return
}
voiceRecognitionState.value.isRecording = true
voiceRecognitionState.value.recordingDuration = 0
// 开始录音
uni.startRecord({
success: (res) => {
console.log('开始录音成功', res)
},
fail: (err) => {
console.log('开始录音失败', err)
uni.showToast({
title: '录音失败',
icon: 'none'
})
}
})
}
})
}
function stopRecording() {
if (!voiceRecognitionState.value.isRecording) return
voiceRecognitionState.value.isRecording = false
// 停止录音并识别
uni.stopRecord({
success: (res) => {
console.log('录音完成', res)
// 模拟语音识别结果
simulateVoiceRecognition()
},
fail: (err) => {
console.log('停止录音失败', err)
uni.showToast({
title: '录音失败',
icon: 'none'
})
}
})
}
function simulateVoiceRecognition() {
// 模拟语音识别结果
const mockResults = {
breastfeeding: '早上8点,左边5分钟,右边6分钟',
bottle: '早上8点,喂奶量90毫升',
formula: '早上8点,喂奶量120毫升',
food: '早上8点,吃了米粉和苹果'
}
voiceRecognitionState.value.recognizedText = mockResults[selectedType.value] || '早上8点,喂奶量90毫升'
voiceRecognitionState.value.showResultPage = true
}
function getFeedingTypeLabel() {
const typeMap = {
breastfeeding: '母乳亲喂',
bottle: '母乳瓶喂',
formula: '奶粉喂养',
food: '辅食'
}
return typeMap[selectedType.value] || '未知'
}
function reRecognize() {
voiceRecognitionState.value.showResultPage = false
voiceRecognitionState.value.recognizedText = ''
}
function completeVoiceRecord() {
const detailText = voiceRecognitionState.value.recognizedText.trim()
if (!detailText) {
uni.showToast({
title: '还没有输入喂养信息哦~',
icon: 'none'
})
return
}
// 生成喂养记录
const record = {
time: formatCurrentTime(),
type: getFeedingTypeLabel(),
detail: detailText
}
console.log('生成喂养记录:', record)
// 关闭结果页面
voiceRecognitionState.value.showResultPage = false
voiceRecognitionState.value.recognizedText = ''
// 显示成功提示
uni.showToast({
title: '记录成功',
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
</script>
<style lang="scss" scoped>
......@@ -609,6 +1052,12 @@ function formatTotalDuration() {
margin-right: 15rpx;
}
.time-display {
display: flex;
align-items: center;
cursor: pointer;
}
.time-value {
font-size: 28rpx;
color: #1d1e25;
......@@ -963,16 +1412,28 @@ function formatTotalDuration() {
font-weight: bold;
}
.complete-btn {
font-size: 26rpx;
color: #C89F6B;
.category-actions {
display: flex;
gap: 20rpx;
.complete-btn {
font-size: 26rpx;
color: #C89F6B;
cursor: pointer;
}
.delete-btn {
font-size: 26rpx;
color: #FF6B6B;
cursor: pointer;
}
}
}
.category-list {
.category-item {
display: flex;
align-items: center;
align-items: flex-start;
gap: 20rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
......@@ -981,6 +1442,7 @@ function formatTotalDuration() {
font-size: 26rpx;
color: #333;
min-width: 80rpx;
margin-top: 8rpx;
}
.add-btn {
......@@ -994,12 +1456,22 @@ function formatTotalDuration() {
justify-content: center;
font-size: 24rpx;
font-weight: bold;
cursor: pointer;
margin-top: 8rpx;
transition: all 0.3s ease;
&.disabled {
background: #E0E0E0;
color: #999;
cursor: not-allowed;
}
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
flex: 1;
.tag {
background: #FFE4B5;
......@@ -1007,18 +1479,95 @@ function formatTotalDuration() {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
&.active {
background: #C89F6B;
color: white;
}
&.editing {
padding-right: 30rpx;
}
.delete-x {
position: absolute;
right: 8rpx;
top: 50%;
transform: translateY(-50%);
font-size: 18rpx;
color: #FF6B6B;
cursor: pointer;
}
}
}
}
}
}
.expand-arrow {
font-size: 24rpx;
color: #666;
}
/* ===== 添加辅食弹窗 ===== */
.add-food-popup {
background: white;
border-radius: 20rpx;
padding: 40rpx;
width: 600rpx;
.popup-header {
text-align: center;
margin-bottom: 30rpx;
.popup-title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
}
.popup-content {
margin-bottom: 30rpx;
.input-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 15rpx;
}
.food-input {
width: 100%;
padding: 20rpx;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
}
.popup-buttons {
display: flex;
gap: 20rpx;
.cancel-btn,
.confirm-btn {
flex: 1;
text-align: center;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
cursor: pointer;
}
.cancel-btn {
background: #F5F5F5;
color: #666;
}
.confirm-btn {
background: #C89F6B;
color: white;
}
}
}
......@@ -1156,6 +1705,11 @@ function formatTotalDuration() {
.microphone-icon {
width: 278rpx;
height: 278rpx;
&[src*="icon_luyining.gif"] {
width: 351rpx;
height: 339rpx;
}
}
}
......@@ -1279,4 +1833,119 @@ function formatTotalDuration() {
}
}
}
// uni-datetime-picker样式覆盖(与feedingRecord页面保持一致)
::v-deep .uni-datetime-picker--btn {
background-color: #D4A574 !important;
}
::v-deep .uni-calendar-item--checked {
background-color: #D4A574 !important;
}
::v-deep .uni-datetime-picker-btn-text {
color: #D4A574 !important;
}
/* ===== 语音识别结果页面 ===== */
.voice-result-page {
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;
justify-content: center;
.result-container {
background: white;
border-radius: 20rpx 20rpx 0 0;
padding: 40rpx;
width: 100%;
max-height: 80vh;
overflow-y: auto;
.result-header {
text-align: center;
margin-bottom: 40rpx;
.result-title {
font-size: 36rpx;
color: #333;
font-weight: bold;
}
}
.feeding-info {
margin-bottom: 40rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.info-label {
font-size: 28rpx;
color: #666;
width: 120rpx;
flex-shrink: 0;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.detail-input {
flex: 1;
padding: 15rpx;
border: 2rpx solid #E0E0E0;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background: #F8F9FA;
}
}
}
.result-actions {
display: flex;
gap: 20rpx;
.re-recognize-btn,
.complete-record-btn {
flex: 1;
text-align: center;
padding: 25rpx;
border-radius: 12rpx;
font-size: 28rpx;
cursor: pointer;
transition: all 0.3s ease;
}
.re-recognize-btn {
background: #F5F5F5;
color: #666;
border: 2rpx solid #E0E0E0;
&:active {
background: #E0E0E0;
}
}
.complete-record-btn {
background: #C89F6B;
color: white;
&:active {
background: #B27C1E;
}
}
}
}
}
</style>
\ No newline at end of file
......@@ -6,7 +6,7 @@
<!-- 头部导航 -->
<view class="header">
<view class="nav-left">
<image :src="feedingRecordRes.icon_return" class="back-btn" />
<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>
......@@ -586,6 +586,11 @@ function goToFeedingAnalysis() {
})
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
function addFeedingRecord() {
if (!currentSelectedDate.value) {
uni.showToast({
......
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