|
|
@@ -0,0 +1,482 @@
|
|
|
+<template>
|
|
|
+ <view class="recommendation-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="empty-state" v-if="!recommendation">
|
|
|
+ <text class="empty-icon">🍽️</text>
|
|
|
+ <text class="empty-text">暂无今日食谱推荐</text>
|
|
|
+ <view class="generate-btn" @tap="generateRecipe">
|
|
|
+ <text>生成食谱</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 有推荐状态 -->
|
|
|
+ <view v-else>
|
|
|
+ <!-- 日期和状态 -->
|
|
|
+ <view class="card header-card">
|
|
|
+ <view class="card-accent"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <text class="date-text">{{ recommendation.date }}</text>
|
|
|
+ <view class="status-badge" :class="'status-' + recommendation.status">
|
|
|
+ <text>{{ statusText }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 菜品列表 -->
|
|
|
+ <view class="meal-section" v-for="(meal, index) in meals" :key="index">
|
|
|
+ <view class="meal-header">
|
|
|
+ <text class="meal-icon">{{ meal.icon }}</text>
|
|
|
+ <text class="meal-name">{{ meal.name }}</text>
|
|
|
+ <text class="meal-count">{{ meal.participants }} 人份</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="dish-list">
|
|
|
+ <view class="dish-item" v-for="(dish, dIndex) in meal.dishes" :key="dIndex">
|
|
|
+ <view class="dish-header">
|
|
|
+ <text class="dish-name">{{ dish.name }}</text>
|
|
|
+ <text class="dish-cal">{{ dish.calories }} kcal</text>
|
|
|
+ </view>
|
|
|
+ <view class="dish-ingredients">
|
|
|
+ <text class="ingredient-tag" v-for="(ing, iIndex) in dish.ingredients" :key="iIndex">
|
|
|
+ {{ ing.name }} {{ ing.amount }}g
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view class="dish-method">
|
|
|
+ <text class="method-text">🍳 {{ dish.method }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="dish-actions">
|
|
|
+ <view class="action-btn like-btn" @tap="likeDish(meal.name, dIndex)">👍</view>
|
|
|
+ <view class="action-btn swap-btn" @tap="swapDish(meal.name, dIndex)">🔄</view>
|
|
|
+ <view class="action-btn skip-btn" @tap="skipDish(meal.name, dIndex)">✕</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 营养汇总 -->
|
|
|
+ <view class="card nutrition-card">
|
|
|
+ <view class="card-accent"></view>
|
|
|
+ <view class="card-body">
|
|
|
+ <text class="card-title">今日营养汇总</text>
|
|
|
+ <view class="nutrition-grid">
|
|
|
+ <view class="nutrition-item">
|
|
|
+ <text class="nutrition-value">{{ summary.calories }}</text>
|
|
|
+ <text class="nutrition-unit">kcal</text>
|
|
|
+ <text class="nutrition-label">热量</text>
|
|
|
+ </view>
|
|
|
+ <view class="nutrition-item">
|
|
|
+ <text class="nutrition-value">{{ summary.protein }}</text>
|
|
|
+ <text class="nutrition-unit">g</text>
|
|
|
+ <text class="nutrition-label">蛋白质</text>
|
|
|
+ </view>
|
|
|
+ <view class="nutrition-item">
|
|
|
+ <text class="nutrition-value">{{ summary.carbs }}</text>
|
|
|
+ <text class="nutrition-unit">g</text>
|
|
|
+ <text class="nutrition-label">碳水</text>
|
|
|
+ </view>
|
|
|
+ <view class="nutrition-item">
|
|
|
+ <text class="nutrition-value">{{ summary.fat }}</text>
|
|
|
+ <text class="nutrition-unit">g</text>
|
|
|
+ <text class="nutrition-label">脂肪</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <view class="action-area">
|
|
|
+ <view class="btn-row">
|
|
|
+ <view class="btn-secondary" @tap="regenerate">重新生成</view>
|
|
|
+ <view class="btn-primary" @tap="completeRecipe" v-if="recommendation.status !== 'completed'">
|
|
|
+ 标记完成
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getDietRecommendation, generateDietRecommendation, completeDietRecommendation } from '@/utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ recommendation: null,
|
|
|
+ meals: [],
|
|
|
+ summary: {},
|
|
|
+ loading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ statusText: function() {
|
|
|
+ var map = {
|
|
|
+ 'pending': '待确认',
|
|
|
+ 'accepted': '已接受',
|
|
|
+ 'completed': '已完成'
|
|
|
+ }
|
|
|
+ return map[this.recommendation && this.recommendation.status] || '待确认'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad: function() {
|
|
|
+ this.loadRecommendation()
|
|
|
+ },
|
|
|
+ onShow: function() {
|
|
|
+ this.loadRecommendation()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ goBack: function() {
|
|
|
+ uni.navigateBack()
|
|
|
+ },
|
|
|
+ loadRecommendation: function() {
|
|
|
+ var self = this
|
|
|
+ var today = self.formatDate(new Date())
|
|
|
+ getDietRecommendation({ date: today }).then(function(res) {
|
|
|
+ if (res && res.id) {
|
|
|
+ self.recommendation = res
|
|
|
+ self.parseMenu(res.menu)
|
|
|
+ self.summary = res.nutritionSummary || {}
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ parseMenu: function(menuJson) {
|
|
|
+ try {
|
|
|
+ var menu = JSON.parse(menuJson || '{}')
|
|
|
+ this.meals = []
|
|
|
+
|
|
|
+ var mealMap = {
|
|
|
+ 'breakfast': { name: '早餐', icon: '🌅' },
|
|
|
+ 'lunch': { name: '午餐', icon: '☀️' },
|
|
|
+ 'dinner': { name: '晚餐', icon: '🌙' }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (menu.meals) {
|
|
|
+ menu.meals.forEach(function(meal) {
|
|
|
+ var config = mealMap[meal.type] || { name: meal.type, icon: '🍽️' }
|
|
|
+ self.meals.push({
|
|
|
+ name: config.name,
|
|
|
+ icon: config.icon,
|
|
|
+ participants: meal.participants || 1,
|
|
|
+ dishes: meal.dishes || []
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析菜单失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ generateRecipe: function() {
|
|
|
+ var self = this
|
|
|
+ self.loading = true
|
|
|
+ generateDietRecommendation({
|
|
|
+ date: self.formatDate(new Date()),
|
|
|
+ meal_type: 'all'
|
|
|
+ }).then(function(res) {
|
|
|
+ self.loading = false
|
|
|
+ if (res && res.id) {
|
|
|
+ uni.showToast({ title: '食谱生成成功', icon: 'success' })
|
|
|
+ self.loadRecommendation()
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.loading = false
|
|
|
+ uni.showToast({ title: '生成失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ likeDish: function(mealName, dishIndex) {
|
|
|
+ uni.showToast({ title: '已点赞', icon: 'success' })
|
|
|
+ },
|
|
|
+ swapDish: function(mealName, dishIndex) {
|
|
|
+ uni.showToast({ title: '已替换菜品', icon: 'success' })
|
|
|
+ },
|
|
|
+ skipDish: function(mealName, dishIndex) {
|
|
|
+ uni.showToast({ title: '已跳过', icon: 'none' })
|
|
|
+ },
|
|
|
+ regenerate: function() {
|
|
|
+ this.generateRecipe()
|
|
|
+ },
|
|
|
+ completeRecipe: function() {
|
|
|
+ var self = this
|
|
|
+ if (!self.recommendation) return
|
|
|
+ completeDietRecommendation({ id: self.recommendation.id }).then(function() {
|
|
|
+ uni.showToast({ title: '已标记完成', icon: 'success' })
|
|
|
+ self.loadRecommendation()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ formatDate: function(date) {
|
|
|
+ var d = date
|
|
|
+ var m = (d.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ var day = d.getDate().toString().padStart(2, '0')
|
|
|
+ return d.getFullYear() + '-' + m + '-' + day
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.recommendation-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;
|
|
|
+}
|
|
|
+.empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 100rpx 40rpx;
|
|
|
+}
|
|
|
+.empty-icon {
|
|
|
+ font-size: 120rpx;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+.empty-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+}
|
|
|
+.generate-btn {
|
|
|
+ background-color: #FF8C42;
|
|
|
+ color: #FFFFFF;
|
|
|
+ padding: 28rpx 60rpx;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+.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;
|
|
|
+}
|
|
|
+.header-card .card-body {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.date-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.status-badge {
|
|
|
+ padding: 8rpx 20rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+.status-pending {
|
|
|
+ background-color: #FFF3E0;
|
|
|
+ color: #FF9800;
|
|
|
+}
|
|
|
+.status-accepted {
|
|
|
+ background-color: #E3F2FD;
|
|
|
+ color: #2196F3;
|
|
|
+}
|
|
|
+.status-completed {
|
|
|
+ background-color: #E8F5E9;
|
|
|
+ color: #4CAF50;
|
|
|
+}
|
|
|
+.meal-section {
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.meal-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 24rpx 30rpx;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.meal-icon {
|
|
|
+ font-size: 36rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+}
|
|
|
+.meal-name {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.meal-count {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.dish-list {
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+}
|
|
|
+.dish-item {
|
|
|
+ padding: 24rpx 0;
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
+}
|
|
|
+.dish-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+.dish-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.dish-name {
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.dish-cal {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #FF8C42;
|
|
|
+}
|
|
|
+.dish-ingredients {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.ingredient-tag {
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
+ background-color: #F5F5F5;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.dish-method {
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.method-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.dish-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+.action-btn {
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+.like-btn {
|
|
|
+ background-color: #E8F5E9;
|
|
|
+ color: #4CAF50;
|
|
|
+}
|
|
|
+.swap-btn {
|
|
|
+ background-color: #E3F2FD;
|
|
|
+ color: #2196F3;
|
|
|
+}
|
|
|
+.skip-btn {
|
|
|
+ background-color: #FFEBEE;
|
|
|
+ color: #F44336;
|
|
|
+}
|
|
|
+.nutrition-grid {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+}
|
|
|
+.nutrition-item {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.nutrition-value {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #FF8C42;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.nutrition-unit {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.nutrition-label {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+ display: block;
|
|
|
+ margin-top: 4rpx;
|
|
|
+}
|
|
|
+.action-area {
|
|
|
+ padding: 40rpx 30rpx;
|
|
|
+}
|
|
|
+.btn-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+}
|
|
|
+.btn-secondary {
|
|
|
+ flex: 1;
|
|
|
+ padding: 28rpx 0;
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.btn-primary {
|
|
|
+ flex: 1;
|
|
|
+ padding: 28rpx 0;
|
|
|
+ background-color: #FF8C42;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #FFFFFF;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+</style>
|