recipe-recommend.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <template>
  2. <view class="recipe-container">
  3. <view class="nav-bar">
  4. <text class="nav-back" @click="goBack">‹ 返回</text>
  5. <text class="nav-title">食谱推荐</text>
  6. <view class="nav-placeholder"></view>
  7. </view>
  8. <view class="meal-tabs">
  9. <text class="tab-item" v-for="mt in mealTypes" :key="mt.value"
  10. :class="{ 'tab-active': selectedMeal === mt.value }"
  11. @click="selectMeal(mt.value)">
  12. <text class="tab-icon">{{ mt.icon }}</text>
  13. <text class="tab-label">{{ mt.label }}</text>
  14. </text>
  15. </view>
  16. <view class="section-card">
  17. <view class="card-header">
  18. <text class="card-title">🥗 AI 智能食谱</text>
  19. <text class="card-subtitle">基于健康数据个性化推荐</text>
  20. </view>
  21. <view class="recommend-btn" :class="{ 'recommend-btn-loading': loading }" @click="doRecommend">
  22. <text class="recommend-btn-text">{{ loading ? 'AI思考中...' : '生成今日食谱' }}</text>
  23. <text class="recommend-btn-icon">✨</text>
  24. </view>
  25. </view>
  26. <view class="section-card" v-if="recipeResult">
  27. <view class="card-header">
  28. <text class="card-title">📋 推荐食谱</text>
  29. </view>
  30. <view class="daily-meals">
  31. <view class="meal-block" v-for="(meal, idx) in parsedMeals" :key="idx">
  32. <view class="meal-block-header">
  33. <text class="meal-block-icon">{{ mealTypeIcon(meal.mealType) }}</text>
  34. <text class="meal-block-title">{{ meal.mealType || meal.recipeName }}</text>
  35. </view>
  36. <text class="meal-reason" v-if="meal.reason">{{ meal.reason }}</text>
  37. <view class="meal-ingredients" v-if="meal.ingredients">
  38. <text class="ingredient-label">食材:</text>
  39. <text class="ingredient-text">{{ meal.ingredients }}</text>
  40. </view>
  41. <text class="meal-tips" v-if="meal.tips">💡 {{ meal.tips }}</text>
  42. </view>
  43. </view>
  44. <view class="nutrition-tip" v-if="parsedNutritionTip">
  45. <text class="tip-label">营养建议</text>
  46. <text class="tip-text">{{ parsedNutritionTip }}</text>
  47. </view>
  48. </view>
  49. <view class="section-card" v-if="recipeResult">
  50. <view class="card-header">
  51. <text class="card-title">🔄 食材替换建议</text>
  52. <text class="card-subtitle">一键替换推荐食材</text>
  53. </view>
  54. <view class="replace-options">
  55. <view class="replace-item" v-for="(food, idx) in replaceOptions" :key="idx" @click="replaceFood(food.id)">
  56. <text class="replace-name">{{ food.name }}</text>
  57. <text class="replace-score" v-if="food.score">{{ food.score }}分</text>
  58. <text class="replace-icon">↻</text>
  59. </view>
  60. <view class="replace-empty" v-if="!replaceOptions || replaceOptions.length === 0">
  61. <text>暂无替换建议</text>
  62. </view>
  63. </view>
  64. </view>
  65. <view class="section-card" v-if="recipeResult">
  66. <view class="card-header">
  67. <text class="card-title">📊 营养摄取概要</text>
  68. </view>
  69. <view class="nutrition-summary">
  70. <view class="nutri-stat" v-for="(val, key) in nutritionSummary" :key="key">
  71. <text class="nutri-value">{{ val }}</text>
  72. <text class="nutri-label">{{ key }}</text>
  73. </view>
  74. <view class="nutri-empty" v-if="!nutritionSummary || Object.keys(nutritionSummary).length === 0">
  75. <text>暂无数据</text>
  76. </view>
  77. </view>
  78. </view>
  79. <view class="bottom-spacer"></view>
  80. </view>
  81. </template>
  82. <script>
  83. import config from '@/config.js'
  84. export default {
  85. data() {
  86. return {
  87. childId: null,
  88. selectedMeal: 'daily',
  89. loading: false,
  90. recipeResult: null,
  91. replaceOptions: [],
  92. nutritionSummary: null,
  93. mealTypes: [
  94. { label: '每日', value: 'daily', icon: '📅' },
  95. { label: '早餐', value: 'breakfast', icon: '🌅' },
  96. { label: '午餐', value: 'lunch', icon: '☀️' },
  97. { label: '晚餐', value: 'dinner', icon: '🌙' },
  98. { label: '加餐', value: 'snack', icon: '🍪' }
  99. ]
  100. }
  101. },
  102. computed: {
  103. parsedMeals: function() {
  104. if (!this.recipeResult || !this.recipeResult.answer) return []
  105. try {
  106. var parsed = JSON.parse(this.recipeResult.answer)
  107. if (parsed.dailyMeals) return parsed.dailyMeals
  108. if (parsed.meals) return parsed.meals
  109. return [{ recipeName: 'AI推荐', reason: this.recipeResult.answer.substring(0, 100) + '...' }]
  110. } catch (e) {
  111. return [{ recipeName: 'AI推荐方案', reason: this.recipeResult.answer }]
  112. }
  113. },
  114. parsedNutritionTip: function() {
  115. if (!this.recipeResult || !this.recipeResult.answer) return ''
  116. try {
  117. var parsed = JSON.parse(this.recipeResult.answer)
  118. return parsed.nutritionTip || parsed.tips || ''
  119. } catch (e) {
  120. return ''
  121. }
  122. }
  123. },
  124. onLoad: function(options) {
  125. this.childId = options.childId ? parseInt(options.childId) : null
  126. },
  127. methods: {
  128. selectMeal: function(value) {
  129. this.selectedMeal = value
  130. },
  131. doRecommend: function() {
  132. this.loading = true
  133. var self = this
  134. healthRequest('/api/meal/recommend', {
  135. mealType: this.selectedMeal !== 'daily' ? this.selectedMeal : null,
  136. childId: this.childId
  137. }).then(function(res) {
  138. self.loading = false
  139. if (res.data) {
  140. self.recipeResult = res.data
  141. self.replaceOptions = res.data.replaceOptions || []
  142. self.nutritionSummary = res.data.nutritionSummary || null
  143. }
  144. }).catch(function(e) {
  145. self.loading = false
  146. var mockData = {
  147. answer: JSON.stringify({
  148. dailyMeals: [
  149. { mealType: '早餐', recipeName: '全麦三明治+牛奶', reason: '搭配优质碳水+蛋白质', ingredients: '全麦面包2片、鸡蛋1个、生菜2片、牛奶200ml', tips: '全麦面包提供膳食纤维,牛奶补充钙质' },
  150. { mealType: '午餐', recipeName: '清蒸鱼+杂粮饭', reason: '优质蛋白+复合碳水', ingredients: '鲈鱼150g、西兰花100g、杂粮米50g', tips: '鱼类富含DHA,有助于大脑发育' },
  151. { mealType: '晚餐', recipeName: '番茄鸡肉意面', reason: '均衡营养易消化', ingredients: '鸡胸肉100g、番茄1个、意面80g', tips: '晚餐适量减少碳水量' }
  152. ],
  153. nutritionTip: '建议每日摄入:蛋白质50-60g,碳水150-200g,蔬果500g以上'
  154. }),
  155. conversationId: '',
  156. replaceOptions: [{ id: 1, name: '鸡胸肉', score: 85 }, { id: 2, name: '三文鱼', score: 90 }, { id: 3, name: '豆腐', score: 75 }],
  157. nutritionSummary: { 蛋白质: '55g', 碳水: '180g', 脂肪: '40g', 膳食纤维: '15g', 热量: '1450kcal' }
  158. }
  159. self.recipeResult = mockData
  160. self.replaceOptions = mockData.replaceOptions
  161. self.nutritionSummary = mockData.nutritionSummary
  162. })
  163. },
  164. replaceFood: function(foodId) {
  165. var self = this
  166. healthRequest('/api/meal/replace', { foodId: foodId, childId: this.childId }).then(function(res) {
  167. if (res.data) {
  168. uni.showToast({ title: '已替换:' + (res.data.name || ''), icon: 'success' })
  169. }
  170. }).catch(function() {
  171. uni.showToast({ title: '替换失败', icon: 'none' })
  172. })
  173. },
  174. mealTypeIcon: function(type) {
  175. var map = { '早餐': '🌅', '午餐': '☀️', '晚餐': '🌙', '加餐': '🍪', 'breakfast': '🌅', 'lunch': '☀️', 'dinner': '🌙', 'snack': '🍪' }
  176. return map[type] || '🍽️'
  177. },
  178. goBack: function() { uni.navigateBack() }
  179. }
  180. }
  181. function healthRequest(url, data) {
  182. var token = uni.getStorageSync('token')
  183. return new Promise(function(resolve, reject) {
  184. uni.request({
  185. url: config.API_BASE_URL + url,
  186. method: 'POST',
  187. data: data,
  188. header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' },
  189. success: function(res) { if (res.data.code === 200) resolve(res.data); else reject(res.data) },
  190. fail: function(err) { reject(err) }
  191. })
  192. })
  193. }
  194. </script>
  195. <style scoped>
  196. .recipe-container { min-height: 100vh; background: #f5f7fa; padding-bottom: 60rpx; }
  197. .nav-bar { position: sticky; top: 0; background: #fff; display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 20rpx 30rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); z-index: 100; }
  198. .nav-back { font-size: 32rpx; color: #5B9BD5; }
  199. .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
  200. .nav-placeholder { width: 80rpx; }
  201. .meal-tabs { display: flex; flex-direction: row; margin: 16rpx 30rpx; }
  202. .tab-item { display: flex; flex-direction: column; align-items: center; padding: 12rpx 20rpx; border-radius: 16rpx; background: #fff; margin-right: 12rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); }
  203. .tab-active { background: #FFF3E0; border: 2rpx solid #FF8C42; }
  204. .tab-icon { font-size: 28rpx; }
  205. .tab-label { font-size: 22rpx; color: #666; margin-top: 4rpx; }
  206. .section-card { margin: 16rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
  207. .card-header { margin-bottom: 16rpx; }
  208. .card-title { font-size: 30rpx; font-weight: bold; color: #333; }
  209. .card-subtitle { font-size: 24rpx; color: #999; margin-top: 4rpx; }
  210. .recommend-btn { display: flex; flex-direction: row; align-items: center; justify-content: center; background: linear-gradient(135deg, #FF8C42, #FFA726); color: #fff; padding: 24rpx; border-radius: 40rpx; }
  211. .recommend-btn-loading { opacity: 0.7; }
  212. .recommend-btn-text { font-size: 32rpx; font-weight: bold; margin-right: 12rpx; }
  213. .recommend-btn-icon { font-size: 36rpx; }
  214. .daily-meals { display: flex; flex-direction: column; }
  215. .meal-block { background: #FFFBEB; border-radius: 16rpx; padding: 16rpx; margin-bottom: 12rpx; }
  216. .meal-block-header { display: flex; flex-direction: row; align-items: center; margin-bottom: 8rpx; }
  217. .meal-block-icon { font-size: 32rpx; margin-right: 8rpx; }
  218. .meal-block-title { font-size: 28rpx; font-weight: 600; color: #333; }
  219. .meal-reason { font-size: 24rpx; color: #666; margin-bottom: 8rpx; }
  220. .meal-ingredients { display: flex; flex-direction: row; flex-wrap: wrap; margin-bottom: 6rpx; }
  221. .ingredient-label { font-size: 24rpx; color: #999; }
  222. .ingredient-text { font-size: 24rpx; color: #333; flex: 1; }
  223. .meal-tips { font-size: 24rpx; color: #E65100; }
  224. .nutrition-tip { background: #E8F5E9; border-radius: 12rpx; padding: 16rpx; margin-top: 12rpx; }
  225. .tip-label { font-size: 24rpx; font-weight: bold; color: #2E7D32; display: block; margin-bottom: 4rpx; }
  226. .tip-text { font-size: 24rpx; color: #333; }
  227. .replace-options { display: flex; flex-direction: column; }
  228. .replace-item { display: flex; flex-direction: row; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
  229. .replace-name { flex: 1; font-size: 28rpx; color: #333; }
  230. .replace-score { font-size: 24rpx; color: #4CAF50; margin-right: 16rpx; }
  231. .replace-icon { font-size: 28rpx; color: #5B9BD5; }
  232. .replace-empty { text-align: center; padding: 20rpx; color: #999; }
  233. .nutrition-summary { display: flex; flex-direction: row; flex-wrap: wrap; }
  234. .nutri-stat { display: flex; flex-direction: column; align-items: center; min-width: 25%; margin-bottom: 12rpx; }
  235. .nutri-value { font-size: 32rpx; font-weight: bold; color: #FF8C42; }
  236. .nutri-label { font-size: 22rpx; color: #666; }
  237. .nutri-empty { text-align: center; padding: 20rpx; color: #999; }
  238. .bottom-spacer { height: 60rpx; }
  239. </style>