|
|
@@ -0,0 +1,536 @@
|
|
|
+<template>
|
|
|
+ <view class="diet-page">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-back" @tap="goBack">
|
|
|
+ <text class="back-text">‹ 返回</text>
|
|
|
+ </view>
|
|
|
+ <text class="nav-title">饮食</text>
|
|
|
+ <view class="nav-placeholder"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <scroll-view class="content" scroll-y>
|
|
|
+ <!-- 今日共餐摘要 -->
|
|
|
+ <view class="card meal-summary-card">
|
|
|
+ <view class="card-accent"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <text class="card-title">今日共餐</text>
|
|
|
+ <view class="meal-row" v-if="mealSummary.breakfast">
|
|
|
+ <text class="meal-label">早餐</text>
|
|
|
+ <text class="meal-members">{{ mealSummary.breakfast }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="meal-row" v-if="mealSummary.lunch">
|
|
|
+ <text class="meal-label">午餐</text>
|
|
|
+ <text class="meal-members">{{ mealSummary.lunch }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="meal-row" v-if="mealSummary.dinner">
|
|
|
+ <text class="meal-label">晚餐</text>
|
|
|
+ <text class="meal-members">{{ mealSummary.dinner }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="empty-hint" v-if="!mealSummary.breakfast && !mealSummary.lunch && !mealSummary.dinner">
|
|
|
+ 尚未配置共餐,请先前往配置
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 食材推荐器 -->
|
|
|
+ <view class="card ingredient-card">
|
|
|
+ <view class="card-accent"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">今日食材推荐</text>
|
|
|
+ <view class="action-btns">
|
|
|
+ <view class="btn-secondary" @tap="refreshIngredients">换一批</view>
|
|
|
+ <view class="btn-primary" @tap="showFoodPicker">添加食材</view>
|
|
|
+ <view class="btn-primary" @tap="generateRecipe" :class="{'disabled': selectedIngredients.length === 0}">
|
|
|
+ 生成食谱
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="ingredient-list" v-if="ingredients.length > 0">
|
|
|
+ <view class="ingredient-item" v-for="(item, index) in ingredients" :key="'ing-' + index">
|
|
|
+ <view class="ingredient-info">
|
|
|
+ <text class="ingredient-name">{{ item.name }}</text>
|
|
|
+ <text class="ingredient-reason" v-if="item.reason">{{ item.reason }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="ingredient-remove" @tap="removeIngredient(index)">✕</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="empty-state" v-else>
|
|
|
+ <text>{{ ingredientsLoading ? '加载中...' : '暂无推荐食材' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 快捷入口 -->
|
|
|
+ <view class="nav-grid">
|
|
|
+ <view class="nav-item" @tap="navTo('pages/diet/food-query')">
|
|
|
+ <text class="nav-icon">🥗</text>
|
|
|
+ <text class="nav-label">食材查询</text>
|
|
|
+ </view>
|
|
|
+ <view class="nav-item" @tap="navTo('pages/diet/records')">
|
|
|
+ <text class="nav-icon">📝</text>
|
|
|
+ <text class="nav-label">饮食记录</text>
|
|
|
+ </view>
|
|
|
+ <view class="nav-item" @tap="navTo('pages/diet/preferences')">
|
|
|
+ <text class="nav-icon">📋</text>
|
|
|
+ <text class="nav-label">调研表</text>
|
|
|
+ </view>
|
|
|
+ <view class="nav-item" @tap="navTo('pages/diet/meal-config')">
|
|
|
+ <text class="nav-icon">👨👩👧</text>
|
|
|
+ <text class="nav-label">共餐配置</text>
|
|
|
+ </view>
|
|
|
+ <view class="nav-item" @tap="navTo('pages/diet/recommendation')">
|
|
|
+ <text class="nav-icon">🍽️</text>
|
|
|
+ <text class="nav-label">食谱推荐</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 本周趋势 -->
|
|
|
+ <view class="card trend-card" v-if="trendData.days > 0">
|
|
|
+ <view class="card-accent"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <text class="card-title">本周饮食</text>
|
|
|
+ <view class="trend-stats">
|
|
|
+ <text class="trend-num">{{ trendData.totalCalories || 0 }}</text>
|
|
|
+ <text class="trend-unit">kcal</text>
|
|
|
+ <text class="trend-label">总摄入</text>
|
|
|
+ </view>
|
|
|
+ <view class="trend-stats">
|
|
|
+ <text class="trend-num">{{ trendData.days }}</text>
|
|
|
+ <text class="trend-unit">天</text>
|
|
|
+ <text class="trend-label">记录天数</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <!-- 食材选择弹窗 -->
|
|
|
+ <view class="modal" v-if="showPicker" @tap="closePicker">
|
|
|
+ <view class="modal-content" @tap.stop>
|
|
|
+ <view class="modal-header">
|
|
|
+ <text class="modal-title">选择食材</text>
|
|
|
+ <text class="modal-close" @tap="closePicker">✕</text>
|
|
|
+ </view>
|
|
|
+ <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @input="searchFood" />
|
|
|
+ <scroll-view class="food-list" scroll-y>
|
|
|
+ <view class="food-item" v-for="food in searchResults" :key="food.id" @tap="addFood(food)">
|
|
|
+ <text class="food-name">{{ food.name }}</text>
|
|
|
+ <text class="food-category">{{ food.category }}</text>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ <view class="empty-state" v-if="searchResults.length === 0 && searchQuery">
|
|
|
+ <text>未找到匹配食材</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getDietIngredients, refreshDietIngredients, addDietIngredient, removeDietIngredient, generateDietRecommendation, getMealConfig } from '@/utils/api.js'
|
|
|
+import { parseDate } from '@/utils/format.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ familyId: null,
|
|
|
+ familyMemberId: null,
|
|
|
+ ingredients: [],
|
|
|
+ ingredientsLoading: false,
|
|
|
+ mealSummary: {},
|
|
|
+ trendData: {},
|
|
|
+ showPicker: false,
|
|
|
+ searchQuery: '',
|
|
|
+ searchResults: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.familyId = uni.getStorageSync('familyId')
|
|
|
+ this.familyMemberId = uni.getStorageSync('currentMemberId')
|
|
|
+ this.loadIngredients()
|
|
|
+ this.loadMealSummary()
|
|
|
+ },
|
|
|
+ onShow() {
|
|
|
+ this.loadIngredients()
|
|
|
+ this.loadMealSummary()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ goBack() {
|
|
|
+ uni.navigateBack()
|
|
|
+ },
|
|
|
+ navTo(url) {
|
|
|
+ uni.navigateTo({ url: url })
|
|
|
+ },
|
|
|
+ loadIngredients() {
|
|
|
+ this.ingredientsLoading = true
|
|
|
+ var self = this
|
|
|
+ getDietIngredients().then(function(res) {
|
|
|
+ self.ingredientsLoading = false
|
|
|
+ if (res && res.ingredients) {
|
|
|
+ self.ingredients = res.ingredients
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.ingredientsLoading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ refreshIngredients() {
|
|
|
+ var self = this
|
|
|
+ refreshDietIngredients().then(function(res) {
|
|
|
+ if (res && res.ingredients) {
|
|
|
+ self.ingredients = res.ingredients
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ addIngredient(index) {
|
|
|
+ var item = this.ingredients[index]
|
|
|
+ addDietIngredient({ food_id: item.foodId, name: item.name }).then(function(res) {
|
|
|
+ self.loadIngredients()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ removeIngredient(index) {
|
|
|
+ var item = this.ingredients[index]
|
|
|
+ removeDietIngredient({ food_id: item.foodId }).then(function(res) {
|
|
|
+ self.loadIngredients()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ generateRecipe() {
|
|
|
+ var self = this
|
|
|
+ var foodIds = this.ingredients.map(function(item) { return { food_id: item.foodId, name: item.name } })
|
|
|
+ generateDietRecommendation({
|
|
|
+ date: self.formatDate(new Date()),
|
|
|
+ meal_type: 'all',
|
|
|
+ selected_foods: JSON.stringify(foodIds)
|
|
|
+ }).then(function(res) {
|
|
|
+ if (res && res.id) {
|
|
|
+ uni.showToast({ title: '食谱生成成功', icon: 'success' })
|
|
|
+ setTimeout(function() {
|
|
|
+ self.navTo('pages/diet/recommendation')
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ formatDate(date) {
|
|
|
+ var d = parseDate(date)
|
|
|
+ if (!d) return ''
|
|
|
+ var m = (d.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ var day = d.getDate().toString().padStart(2, '0')
|
|
|
+ return d.getFullYear() + '-' + m + '-' + day
|
|
|
+ },
|
|
|
+ loadMealSummary() {
|
|
|
+ var self = this
|
|
|
+ var dateType = self.isWeekend(new Date()) ? 'weekend' : 'weekday'
|
|
|
+ Promise.all([
|
|
|
+ getMealConfig({ date_type: dateType, meal_type: 'breakfast' }),
|
|
|
+ getMealConfig({ date_type: dateType, meal_type: 'lunch' }),
|
|
|
+ getMealConfig({ date_type: dateType, meal_type: 'dinner' })
|
|
|
+ ]).then(function(results) {
|
|
|
+ self.mealSummary.breakfast = self.parseMembers(results[0])
|
|
|
+ self.mealSummary.lunch = self.parseMembers(results[1])
|
|
|
+ self.mealSummary.dinner = self.parseMembers(results[2])
|
|
|
+ })
|
|
|
+ },
|
|
|
+ parseMembers(config) {
|
|
|
+ if (!config || !config.participantMemberIds) return ''
|
|
|
+ try {
|
|
|
+ var ids = JSON.parse(config.participantMemberIds)
|
|
|
+ return '共 ' + ids.length + ' 人'
|
|
|
+ } catch (e) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ isWeekend(date) {
|
|
|
+ var day = date.getDay()
|
|
|
+ return day === 0 || day === 6
|
|
|
+ },
|
|
|
+ showFoodPicker() {
|
|
|
+ this.showPicker = true
|
|
|
+ },
|
|
|
+ closePicker() {
|
|
|
+ this.showPicker = false
|
|
|
+ this.searchQuery = ''
|
|
|
+ this.searchResults = []
|
|
|
+ },
|
|
|
+ searchFood() {
|
|
|
+ if (!this.searchQuery) {
|
|
|
+ this.searchResults = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 简化:实际应调用搜索API
|
|
|
+ this.searchResults = []
|
|
|
+ },
|
|
|
+ addFood(food) {
|
|
|
+ addDietIngredient({ food_id: food.id, name: food.name }).then(function(res) {
|
|
|
+ self.closePicker()
|
|
|
+ self.loadIngredients()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.diet-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #F5F7FA;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.nav-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 88rpx;
|
|
|
+ padding-top: env(safe-area-inset-top);
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ position: relative;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.nav-back {
|
|
|
+ position: absolute;
|
|
|
+ left: 30rpx;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ padding: 10rpx;
|
|
|
+}
|
|
|
+.back-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.nav-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.nav-placeholder {
|
|
|
+ width: 80rpx;
|
|
|
+}
|
|
|
+.content {
|
|
|
+ flex: 1;
|
|
|
+ padding: 30rpx;
|
|
|
+}
|
|
|
+.card {
|
|
|
+ display: flex;
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.card-accent {
|
|
|
+ width: 8rpx;
|
|
|
+ background-color: #FF8C42;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.card-body {
|
|
|
+ flex: 1;
|
|
|
+ padding: 30rpx;
|
|
|
+}
|
|
|
+.card-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.section-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.action-btns {
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+.btn-secondary {
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.btn-primary {
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ background-color: #FF8C42;
|
|
|
+ color: #FFFFFF;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+.btn-primary.disabled {
|
|
|
+ background-color: #ccc;
|
|
|
+}
|
|
|
+.ingredient-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+.ingredient-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.ingredient-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4rpx;
|
|
|
+}
|
|
|
+.ingredient-name {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.ingredient-reason {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.ingredient-remove {
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.meal-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.meal-label {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.meal-members {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #FF8C42;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.empty-hint {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+ text-align: center;
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+.empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40rpx 0;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.nav-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+.nav-item {
|
|
|
+ width: 30%;
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx 0;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.nav-icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.nav-label {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.trend-stats {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.trend-num {
|
|
|
+ font-size: 48rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #FF8C42;
|
|
|
+}
|
|
|
+.trend-unit {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.trend-label {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #666;
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+.modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+.modal-content {
|
|
|
+ width: 90%;
|
|
|
+ max-height: 80%;
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx;
|
|
|
+}
|
|
|
+.modal-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.modal-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.modal-close {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.search-input {
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ background-color: #FAFAFA;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.food-list {
|
|
|
+ max-height: 500rpx;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+.food-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 20rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.food-name {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.food-category {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+</style>
|