|
@@ -0,0 +1,337 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="meal-log">
|
|
|
|
|
+ <!-- 日期导航 -->
|
|
|
|
|
+ <view class="date-nav">
|
|
|
|
|
+ <text class="nav-arrow" @click="changeDate(-1)">‹</text>
|
|
|
|
|
+ <text class="date-text">{{ currentDate }}</text>
|
|
|
|
|
+ <text class="nav-arrow" @click="changeDate(1)">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 记录新饮食 -->
|
|
|
|
|
+ <view class="log-form">
|
|
|
|
|
+ <view class="form-title">记录饮食</view>
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <text class="label">餐次</text>
|
|
|
|
|
+ <picker :value="mealTypeIndex" :range="mealTypes" @change="onMealTypeChange">
|
|
|
|
|
+ <view class="picker-value">{{ mealTypes[mealTypeIndex] || '请选择' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <text class="label">食物</text>
|
|
|
|
|
+ <input class="input" v-model="foodsInput" placeholder="输入食物名称,逗号分隔" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <text class="label">备注</text>
|
|
|
|
|
+ <input class="input" v-model="note" placeholder="选填" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <button class="submit-btn" type="primary" :loading="saving" @click="saveLog" :disabled="saving">
|
|
|
|
|
+ {{ saving ? '保存中...' : '记录' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 最近饮食 -->
|
|
|
|
|
+ <view class="logs-section">
|
|
|
|
|
+ <view class="section-title">近期饮食记录</view>
|
|
|
|
|
+ <view v-if="logs.length === 0 && !loadingLogs" class="empty-row">
|
|
|
|
|
+ <text class="empty-text">暂无记录</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-for="log in logs" :key="log.id" class="log-card">
|
|
|
|
|
+ <view class="log-header">
|
|
|
|
|
+ <text class="log-meal-type">{{ log.mealType }}</text>
|
|
|
|
|
+ <text class="log-time">{{ formatTime(log.createdAt) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="log-foods">{{ log.foods }}</text>
|
|
|
|
|
+ <text v-if="log.note" class="log-note">{{ log.note }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="loadingLogs" class="loading-row">
|
|
|
|
|
+ <text>加载中...</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 营养摘要 -->
|
|
|
|
|
+ <view class="nutrition-summary">
|
|
|
|
|
+ <view class="section-title">营养摄入统计 (近7天)</view>
|
|
|
|
|
+ <view v-if="Object.keys(nutritionData).length === 0 && !loadingNutrition" class="empty-row">
|
|
|
|
|
+ <text class="empty-text">暂无数据</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="loadingNutrition" class="loading-row">
|
|
|
|
|
+ <text>加载中...</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="Object.keys(nutritionData).length > 0" class="nutrition-grid">
|
|
|
|
|
+ <view v-for="(val, key) in nutritionData" :key="key" class="nutrition-item">
|
|
|
|
|
+ <text class="nutrition-key">{{ key }}</text>
|
|
|
|
|
+ <text class="nutrition-val">{{ val }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { logMeal, getMealLogs, getNutritionSummary } from '@/utils/api'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'MealLog',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ mealTypes: ['早餐', '午餐', '晚餐', '加餐'],
|
|
|
|
|
+ mealTypeIndex: -1,
|
|
|
|
|
+ foodsInput: '',
|
|
|
|
|
+ note: '',
|
|
|
|
|
+ saving: false,
|
|
|
|
|
+ currentDate: this.getToday(),
|
|
|
|
|
+ logs: [],
|
|
|
|
|
+ loadingLogs: false,
|
|
|
|
|
+ nutritionData: {},
|
|
|
|
|
+ loadingNutrition: false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onLoad() {
|
|
|
|
|
+ this.loadLogs()
|
|
|
|
|
+ this.loadNutrition()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ getToday() {
|
|
|
|
|
+ const d = new Date()
|
|
|
|
|
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
|
|
|
|
+ },
|
|
|
|
|
+ formatDate(date) {
|
|
|
|
|
+ const d = new Date(date)
|
|
|
|
|
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
|
|
|
|
+ },
|
|
|
|
|
+ formatTime(dateStr) {
|
|
|
|
|
+ if (!dateStr) return ''
|
|
|
|
|
+ const d = new Date(dateStr)
|
|
|
|
|
+ return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
|
|
|
|
|
+ },
|
|
|
|
|
+ changeDate(offset) {
|
|
|
|
|
+ const d = new Date(this.currentDate)
|
|
|
|
|
+ d.setDate(d.getDate() + offset)
|
|
|
|
|
+ this.currentDate = this.formatDate(d)
|
|
|
|
|
+ },
|
|
|
|
|
+ onMealTypeChange(e) {
|
|
|
|
|
+ this.mealTypeIndex = e.detail.value
|
|
|
|
|
+ },
|
|
|
|
|
+ async saveLog() {
|
|
|
|
|
+ if (this.mealTypeIndex < 0) {
|
|
|
|
|
+ uni.showToast({ title: '请选择餐次', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.foodsInput.trim()) {
|
|
|
|
|
+ uni.showToast({ title: '请输入食物名称', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.saving = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = {
|
|
|
|
|
+ mealType: this.mealTypes[this.mealTypeIndex],
|
|
|
|
|
+ foods: this.foodsInput.trim(),
|
|
|
|
|
+ note: this.note,
|
|
|
|
|
+ mealDate: this.currentDate
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await logMeal(data)
|
|
|
|
|
+ if (res.code === 0) {
|
|
|
|
|
+ uni.showToast({ title: '记录成功', icon: 'success' })
|
|
|
|
|
+ this.foodsInput = ''
|
|
|
|
|
+ this.note = ''
|
|
|
|
|
+ this.mealTypeIndex = -1
|
|
|
|
|
+ this.loadLogs()
|
|
|
|
|
+ this.loadNutrition()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: res.message || '记录失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.showToast({ title: '网络错误', icon: 'none' })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.saving = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadLogs() {
|
|
|
|
|
+ this.loadingLogs = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getMealLogs({ days: 7 })
|
|
|
|
|
+ if (res.code === 0) {
|
|
|
|
|
+ this.logs = res.data || []
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.log('load logs error', e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loadingLogs = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadNutrition() {
|
|
|
|
|
+ this.loadingNutrition = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getNutritionSummary({ days: 7 })
|
|
|
|
|
+ if (res.code === 0) {
|
|
|
|
|
+ this.nutritionData = res.data || {}
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.log('load nutrition error', e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loadingNutrition = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.meal-log {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: var(--bg, #FFF7ED);
|
|
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
|
|
+ padding-bottom: 60rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.date-nav {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.nav-arrow {
|
|
|
|
|
+ font-size: 48rpx;
|
|
|
|
|
+ color: var(--color-primary, #F97316);
|
|
|
|
|
+ padding: 0 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.date-text {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ min-width: 200rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.log-form {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 30rpx;
|
|
|
|
|
+ margin-top: 10rpx;
|
|
|
|
|
+ box-shadow: var(--shadow-md, 0 2rpx 12rpx rgba(0,0,0,0.06));
|
|
|
|
|
+}
|
|
|
|
|
+.form-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.form-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.form-item .label {
|
|
|
|
|
+ width: 100rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.form-item .input {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
|
|
+ border: 1rpx solid #eee;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.form-item .picker-value {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: var(--color-primary, #F97316);
|
|
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
|
|
+ background: #FFF0E0;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.submit-btn {
|
|
|
|
|
+ margin-top: 20rpx;
|
|
|
|
|
+ background: var(--color-primary, #F97316);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 10rpx;
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.logs-section {
|
|
|
|
|
+ margin-top: 30rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ padding-left: 10rpx;
|
|
|
|
|
+ border-left: 6rpx solid var(--color-primary, #F97316);
|
|
|
|
|
+}
|
|
|
|
|
+.log-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ box-shadow: var(--shadow-md, 0 2rpx 12rpx rgba(0,0,0,0.06));
|
|
|
|
|
+}
|
|
|
|
|
+.log-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.log-meal-type {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: var(--color-primary, #F97316);
|
|
|
|
|
+}
|
|
|
|
|
+.log-time {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+.log-foods {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+.log-note {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+.empty-row {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 40rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #ccc;
|
|
|
|
|
+}
|
|
|
|
|
+.loading-row {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+.nutrition-summary {
|
|
|
|
|
+ margin-top: 30rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.nutrition-grid {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.nutrition-item {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ min-width: 140rpx;
|
|
|
|
|
+ box-shadow: var(--shadow-md, 0 2rpx 12rpx rgba(0,0,0,0.06));
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.nutrition-key {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+.nutrition-val {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ color: var(--color-primary, #F97316);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-top: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|