Home.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <template>
  2. <div class="min-h-screen bg-yellow-400">
  3. <!-- 头部 - 粉色区域 -->
  4. <header class="bg-pink-400 border-4 border-black mx-4 mt-4 rounded-lg relative">
  5. <!-- <div class="absolute top-2 right-2">
  6. <button class="bg-white/20 hover:bg-white/30 rounded-full px-3 py-1 text-sm text-white transition-colors">中文</button>
  7. </div> -->
  8. <div class="text-center py-8">
  9. <h1 class="text-5xl font-black text-yellow-300 mb-2 tracking-wider">一饭封神</h1>
  10. <p class="text-white text-lg font-medium">UPLOAD YOUR INGREDIENTS | SPIT OUT RECIPES!</p>
  11. </div>
  12. </header>
  13. <!-- 使用量显示 -->
  14. <!-- <div class="mx-4 mt-4">
  15. <div class="bg-white border-2 border-black rounded-lg p-4 max-w-sm mx-auto">
  16. <div class="flex items-center justify-between">
  17. <div class="flex items-center gap-2">
  18. <span class="text-lg">⚡</span>
  19. <span class="font-bold text-dark-800">DAILY USAGE</span>
  20. </div>
  21. <span class="font-bold text-xl">0/5</span>
  22. </div>
  23. <div class="mt-2 bg-gray-200 rounded-full h-2">
  24. <div class="bg-dark-800 h-2 rounded-full" style="width: 0%"></div>
  25. </div>
  26. </div>
  27. </div> -->
  28. <div class="max-w-5xl mx-auto px-4 py-6">
  29. <!-- 步骤1: 输入食材 -->
  30. <div class="mb-6">
  31. <div class="bg-pink-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  32. <span class="font-bold">1. 输入食材</span>
  33. </div>
  34. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 md:p-8">
  35. <div class="text-center mb-6">
  36. <div class="w-16 h-16 bg-black rounded-lg flex items-center justify-center mx-auto mb-4">
  37. <span class="text-white text-2xl">🥬</span>
  38. </div>
  39. <h2 class="text-2xl font-bold text-dark-800 mb-2">添加食材</h2>
  40. <p class="text-gray-600">输入你现有的食材,按回车添加</p>
  41. <p class="text-xs text-gray-500 mt-1">支持蔬菜、肉类、调料等 (最多10种)</p>
  42. </div>
  43. <!-- 食材输入区域 -->
  44. <div class="space-y-4">
  45. <!-- 已添加的食材 -->
  46. <div v-if="ingredients.length > 0" class="flex flex-wrap gap-2">
  47. <div
  48. v-for="ingredient in ingredients"
  49. :key="ingredient"
  50. class="inline-flex items-center gap-2 bg-yellow-400 text-dark-800 px-3 py-2 rounded-full text-sm font-medium border-2 border-black"
  51. >
  52. {{ ingredient }}
  53. <button @click="removeIngredient(ingredient)" class="hover:bg-yellow-500 rounded-full p-1 transition-colors">
  54. <span class="text-xs">✕</span>
  55. </button>
  56. </div>
  57. </div>
  58. <!-- 输入框 -->
  59. <div class="relative">
  60. <input
  61. v-model="currentIngredient"
  62. @keyup.enter="addIngredient"
  63. placeholder="输入食材名称,按回车添加..."
  64. class="w-full p-4 border-2 border-black rounded-lg text-lg font-medium focus:outline-none focus:ring-2 focus:ring-pink-400"
  65. />
  66. </div>
  67. <!-- 快速选择食材 -->
  68. <div class="mt-4">
  69. <button @click="toggleIngredientPicker" class="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-800 transition-colors">
  70. <span class="transform transition-transform duration-200" :class="{ 'rotate-90': showIngredientPicker }">▶</span>
  71. 快速选择食材
  72. </button>
  73. <div v-if="showIngredientPicker" class="mt-3 border-2 border-gray-200 rounded-lg overflow-hidden bg-white">
  74. <!-- 食材展示区域 -->
  75. <div class="p-3 max-h-80 overflow-y-auto">
  76. <div class="space-y-4">
  77. <div v-for="category in ingredientCategories" :key="category.id">
  78. <!-- 分类标题 -->
  79. <div class="flex items-center gap-2 mb-2">
  80. <span class="text-sm">{{ category.icon }}</span>
  81. <span class="text-sm font-bold text-gray-700">{{ category.name }}</span>
  82. <div class="flex-1 h-px bg-gray-200"></div>
  83. </div>
  84. <!-- 食材按钮 -->
  85. <div class="flex flex-wrap gap-1.5">
  86. <button
  87. v-for="item in category.items"
  88. :key="item"
  89. @click="quickAddIngredient(item)"
  90. :disabled="ingredients.includes(item) || ingredients.length >= 10"
  91. class="px-2 py-1 text-xs rounded border border-gray-300 hover:border-pink-400 hover:bg-pink-50 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200 transition-all duration-200"
  92. :class="{
  93. 'bg-yellow-100 border-yellow-400 text-yellow-800': ingredients.includes(item),
  94. 'hover:scale-105': !ingredients.includes(item) && ingredients.length < 10
  95. }"
  96. >
  97. {{ item }}
  98. </button>
  99. </div>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- 底部状态栏 -->
  104. <div class="px-3 py-2 bg-gray-50 border-t border-gray-200 text-xs text-gray-500 flex justify-between items-center">
  105. <span>点击食材快速添加到列表</span>
  106. <span class="font-medium">已选择 {{ ingredients.length }}/10</span>
  107. </div>
  108. </div>
  109. </div>
  110. <!-- 生成按钮 -->
  111. <div class="text-center pt-4">
  112. <button
  113. @click="generateRecipes"
  114. :disabled="ingredients.length === 0 || isLoading"
  115. class="bg-dark-800 hover:bg-dark-700 disabled:bg-gray-400 text-white px-8 py-3 rounded-lg font-bold text-lg border-2 border-black transition-all duration-300 transform hover:scale-105 disabled:scale-100 disabled:cursor-not-allowed"
  116. >
  117. <span class="flex items-center gap-2 justify-center">
  118. <template v-if="isLoading">
  119. <div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
  120. 生成中...
  121. </template>
  122. <template v-else> ✨ {{ customPrompt.trim() ? '按要求生成' : '交给大师' }} </template>
  123. </span>
  124. </button>
  125. <p v-if="customPrompt.trim()" class="text-xs text-gray-600 mt-2">将根据您的自定义要求生成菜谱</p>
  126. <p v-else-if="selectedCuisines.length > 0" class="text-xs text-gray-600 mt-2">将生成 {{ selectedCuisines.length }} 个菜系的菜谱</p>
  127. <p v-else class="text-xs text-gray-600 mt-2">将随机选择菜系生成菜谱</p>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. <!-- 步骤2和3: 选择风格 OR 自定义提示 -->
  133. <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
  134. <!-- 选择菜系 -->
  135. <div>
  136. <div class="bg-green-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  137. <span class="font-bold">2. 选择菜系</span>
  138. </div>
  139. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6 h-full">
  140. <div v-if="customPrompt.trim()" class="text-center py-8 text-gray-500">
  141. <p class="text-sm">已设置自定义要求,将忽略菜系选择</p>
  142. <button @click="customPrompt = ''" class="text-blue-600 hover:text-blue-700 underline text-sm mt-2">清除自定义要求以选择菜系</button>
  143. </div>
  144. <div v-else class="grid grid-cols-2 gap-3">
  145. <button
  146. v-for="cuisine in cuisines.slice(0, 10)"
  147. :key="cuisine.id"
  148. @click="selectCuisine(cuisine)"
  149. :class="[
  150. 'p-3 rounded-lg border-2 border-black font-medium text-sm transition-all duration-200',
  151. selectedCuisines.includes(cuisine.id) ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  152. ]"
  153. >
  154. {{ cuisine.name.replace('大师', '') }}
  155. </button>
  156. </div>
  157. </div>
  158. </div>
  159. <!-- 自定义提示 -->
  160. <div>
  161. <div class="bg-blue-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  162. <span class="font-bold">3. 或自定义要求</span>
  163. </div>
  164. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6 h-full flex flex-col">
  165. <div class="flex-1">
  166. <label class="block text-sm font-bold text-dark-800 mb-2">描述你的需求:</label>
  167. <textarea
  168. v-model="customPrompt"
  169. placeholder="例如:做一道清淡的汤,适合老人食用..."
  170. class="w-full p-4 border-2 border-gray-300 rounded-lg text-base resize-none focus:outline-none focus:border-blue-400 h-40"
  171. ></textarea>
  172. <div v-if="customPrompt.trim()" class="mt-2 flex justify-between items-center">
  173. <span class="text-xs text-green-600">✓ 已设置自定义要求</span>
  174. <button @click="customPrompt = ''" class="text-xs text-red-600 hover:text-red-700 underline">清除</button>
  175. </div>
  176. </div>
  177. <p class="text-xs text-gray-500 mt-auto">越具体越好!</p>
  178. </div>
  179. </div>
  180. </div>
  181. <!-- 中间的OR -->
  182. <div class="text-center mb-6">
  183. <span class="bg-yellow-400 text-dark-800 px-4 py-2 rounded-full font-bold text-xl border-2 border-black"> OR </span>
  184. </div>
  185. <!-- 步骤4: 结果 -->
  186. <div ref="resultsSection">
  187. <div class="bg-dark-800 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  188. <span class="font-bold">4. 菜谱结果</span>
  189. </div>
  190. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 md:p-8">
  191. <!-- 加载状态 -->
  192. <div v-if="isLoading" class="text-center py-12">
  193. <div class="w-16 h-16 border-4 border-gray-300 border-t-dark-800 rounded-full animate-spin mx-auto mb-4"></div>
  194. <h3 class="text-xl font-bold text-dark-800 mb-2">大师正在创作中...</h3>
  195. <p class="text-gray-600">{{ loadingText }}</p>
  196. </div>
  197. <!-- 错误状态 -->
  198. <div v-else-if="errorMessage" class="text-center py-12">
  199. <div class="w-16 h-16 bg-red-100 rounded-lg flex items-center justify-center mx-auto mb-4">
  200. <span class="text-red-500 text-2xl">⚠️</span>
  201. </div>
  202. <h3 class="text-xl font-bold text-red-600 mb-2">生成失败</h3>
  203. <p class="text-red-500 mb-4">{{ errorMessage }}</p>
  204. <button
  205. @click="generateRecipes"
  206. :disabled="ingredients.length === 0"
  207. class="bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white px-6 py-2 rounded-lg font-medium border-2 border-black transition-all duration-200 disabled:cursor-not-allowed"
  208. >
  209. 🔄 重新生成
  210. </button>
  211. </div>
  212. <!-- 空状态 -->
  213. <div v-else-if="recipes.length === 0" class="text-center py-12">
  214. <div class="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center mx-auto mb-4">
  215. <span class="text-gray-400 text-2xl">⭐</span>
  216. </div>
  217. <h3 class="text-xl font-bold text-gray-400 mb-2">等待魔法发生...</h3>
  218. <p class="text-gray-500">添加食材并选择菜系开始创作</p>
  219. </div>
  220. <!-- 菜谱结果 -->
  221. <div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
  222. <div v-for="recipe in recipes" :key="recipe.id" class="border-2 border-black rounded-lg overflow-hidden">
  223. <RecipeCard :recipe="recipe" />
  224. </div>
  225. </div>
  226. </div>
  227. </div>
  228. </div>
  229. <!-- 底部 -->
  230. <footer class="bg-white border-2 border-black mx-4 mb-4 rounded-lg p-4 text-center">
  231. <p class="text-sm text-gray-600">
  232. © 2025 一饭封神 | <a href="https://github.com/liu-ziting/what-to-eat" target="_blank" class="text-retro-blue hover:underline">Powered by Liuziting</a>
  233. </p>
  234. </footer>
  235. </div>
  236. </template>
  237. <script setup lang="ts">
  238. import { ref, onUnmounted } from 'vue'
  239. import { cuisines } from '@/config/cuisines'
  240. import { ingredientCategories } from '@/config/ingredients'
  241. import RecipeCard from '@/components/RecipeCard.vue'
  242. import { generateMultipleRecipes, generateCustomRecipe } from '@/services/aiService'
  243. import type { Recipe, CuisineType, NutritionAnalysis } from '@/types'
  244. // 响应式数据
  245. const ingredients = ref<string[]>([])
  246. const currentIngredient = ref('')
  247. const selectedCuisines = ref<string[]>([])
  248. const customPrompt = ref('')
  249. const recipes = ref<Recipe[]>([])
  250. const isLoading = ref(false)
  251. const loadingText = ref('大师正在挑选食材...')
  252. const resultsSection = ref<HTMLElement | null>(null)
  253. const errorMessage = ref('')
  254. const showIngredientPicker = ref(false)
  255. // 加载文字轮播
  256. const loadingTexts = [
  257. '大师正在挑选食材...',
  258. '大师正在起火热锅...',
  259. '大师正在爆香配料...',
  260. '大师正在调制秘制酱料...',
  261. '大师正在掌控火候...',
  262. '大师正在精心摆盘...',
  263. '美味佳肴即将出炉...'
  264. ]
  265. let loadingInterval: NodeJS.Timeout | null = null
  266. // 添加食材
  267. const addIngredient = () => {
  268. const ingredient = currentIngredient.value.trim()
  269. if (ingredient && !ingredients.value.includes(ingredient) && ingredients.value.length < 10) {
  270. ingredients.value.push(ingredient)
  271. currentIngredient.value = ''
  272. }
  273. }
  274. // 移除食材
  275. const removeIngredient = (ingredient: string) => {
  276. const index = ingredients.value.indexOf(ingredient)
  277. if (index > -1) {
  278. ingredients.value.splice(index, 1)
  279. }
  280. }
  281. // 快速添加食材
  282. const quickAddIngredient = (ingredient: string) => {
  283. if (!ingredients.value.includes(ingredient) && ingredients.value.length < 10) {
  284. ingredients.value.push(ingredient)
  285. }
  286. }
  287. // 切换食材选择器显示
  288. const toggleIngredientPicker = () => {
  289. showIngredientPicker.value = !showIngredientPicker.value
  290. }
  291. // 选择菜系
  292. const selectCuisine = (cuisine: CuisineType) => {
  293. const index = selectedCuisines.value.indexOf(cuisine.id)
  294. if (index > -1) {
  295. selectedCuisines.value.splice(index, 1)
  296. } else {
  297. selectedCuisines.value.push(cuisine.id)
  298. }
  299. }
  300. // 生成菜谱
  301. const generateRecipes = async () => {
  302. if (ingredients.value.length === 0) {
  303. return
  304. }
  305. isLoading.value = true
  306. recipes.value = []
  307. errorMessage.value = ''
  308. // 滚动到结果区域
  309. if (resultsSection.value) {
  310. resultsSection.value.scrollIntoView({
  311. behavior: 'smooth',
  312. block: 'start'
  313. })
  314. }
  315. // 开始加载文字轮播
  316. let textIndex = 0
  317. loadingInterval = setInterval(() => {
  318. loadingText.value = loadingTexts[textIndex]
  319. textIndex = (textIndex + 1) % loadingTexts.length
  320. }, 2000)
  321. try {
  322. // 检查是否有自定义提示词
  323. if (customPrompt.value.trim()) {
  324. // 使用自定义提示词生成菜谱
  325. const customRecipe = await generateCustomRecipe(ingredients.value, customPrompt.value.trim())
  326. recipes.value = [customRecipe]
  327. } else {
  328. // 使用菜系生成菜谱
  329. let selectedCuisineObjects = cuisines.filter(c => selectedCuisines.value.includes(c.id))
  330. if (selectedCuisineObjects.length === 0) {
  331. // 随机选择2个菜系
  332. const shuffled = [...cuisines].sort(() => 0.5 - Math.random())
  333. selectedCuisineObjects = shuffled.slice(0, 2)
  334. }
  335. // 调用AI服务生成菜谱
  336. const generatedRecipes = await generateMultipleRecipes(ingredients.value, selectedCuisineObjects, customPrompt.value.trim() || undefined)
  337. recipes.value = generatedRecipes
  338. }
  339. } catch (error) {
  340. console.error('生成菜谱失败:', error)
  341. // 显示错误信息
  342. errorMessage.value = error instanceof Error ? error.message : 'AI生成菜谱失败,请稍后重试'
  343. } finally {
  344. isLoading.value = false
  345. if (loadingInterval) {
  346. clearInterval(loadingInterval)
  347. loadingInterval = null
  348. }
  349. }
  350. }
  351. // 模拟AI调用(后续替换为真实接口)
  352. const simulateAICall = async () => {
  353. return new Promise(resolve => {
  354. setTimeout(() => {
  355. // 获取要使用的菜系
  356. let cuisinesToUse = cuisines.filter(c => selectedCuisines.value.includes(c.id))
  357. if (cuisinesToUse.length === 0) {
  358. // 随机选择2个菜系
  359. const shuffled = [...cuisines].sort(() => 0.5 - Math.random())
  360. cuisinesToUse = shuffled.slice(0, 2)
  361. }
  362. // 检查是否有自定义提示词
  363. let mockRecipes: Recipe[] = []
  364. if (customPrompt.value.trim()) {
  365. // 生成自定义菜谱
  366. mockRecipes = [
  367. {
  368. id: `recipe-custom-${Date.now()}`,
  369. name: `自定义:${ingredients.value.join('')}料理`,
  370. cuisine: '自定义',
  371. ingredients: ingredients.value,
  372. steps: [
  373. { step: 1, description: '准备所有食材,清洗干净', time: 5 },
  374. { step: 2, description: '根据要求进行烹饪处理', time: 10 },
  375. { step: 3, description: '调味并完成最后的制作', time: 8 },
  376. { step: 4, description: '装盘即可享用', time: 2 }
  377. ],
  378. cookingTime: 25,
  379. difficulty: 'medium',
  380. tips: ['根据个人喜好调整口味', '注意食材的新鲜度', '掌握好火候'],
  381. nutritionAnalysis: generateMockNutrition(ingredients.value)
  382. }
  383. ]
  384. } else {
  385. // 生成菜系菜谱
  386. mockRecipes = cuisinesToUse.map((cuisine, index) => {
  387. return {
  388. id: `recipe-${cuisine.id}-${Date.now()}-${index}`,
  389. name: `${cuisine.name}推荐:${ingredients.value.join('')}料理`,
  390. cuisine: cuisine.name,
  391. ingredients: ingredients.value,
  392. steps: [
  393. { step: 1, description: '准备所有食材,清洗干净', time: 5 },
  394. { step: 2, description: '热锅下油,爆香配料', time: 3 },
  395. { step: 3, description: '下主料翻炒至半熟', time: 8 },
  396. { step: 4, description: '调味炒制至熟透', time: 5 },
  397. { step: 5, description: '装盘即可享用', time: 1 }
  398. ],
  399. cookingTime: 22,
  400. difficulty: 'medium',
  401. tips: ['火候要掌握好,避免炒糊', '调料要适量,突出食材本味', '炒制过程中要勤翻动'],
  402. nutritionAnalysis: generateMockNutrition(ingredients.value)
  403. }
  404. })
  405. }
  406. recipes.value = mockRecipes
  407. resolve(mockRecipes)
  408. }, 3000)
  409. })
  410. }
  411. // 生成模拟营养分析数据
  412. const generateMockNutrition = (ingredients: string[]): NutritionAnalysis => {
  413. // 基于食材数量和类型估算营养成分
  414. const baseCalories = ingredients.length * 50 + Math.floor(Math.random() * 100) + 200
  415. const hasVegetables = ingredients.some(ing => ['菜', '瓜', '豆', '萝卜', '白菜', '菠菜', '西红柿', '黄瓜', '茄子', '土豆'].some(veg => ing.includes(veg)))
  416. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  417. const hasGrains = ingredients.some(ing => ['米', '面', '粉', '饭', '面条', '馒头'].some(grain => ing.includes(grain)))
  418. // 生成饮食标签
  419. const dietaryTags: string[] = []
  420. if (hasVegetables && !hasMeat) dietaryTags.push('素食')
  421. if (hasMeat) dietaryTags.push('高蛋白')
  422. if (hasVegetables) dietaryTags.push('富含维生素')
  423. if (!hasGrains) dietaryTags.push('低碳水')
  424. if (baseCalories < 300) dietaryTags.push('低卡路里')
  425. // 生成营养建议
  426. const balanceAdvice: string[] = []
  427. if (!hasVegetables) balanceAdvice.push('建议搭配新鲜蔬菜增加维生素和膳食纤维')
  428. if (!hasMeat && !ingredients.some(ing => ['豆', '蛋', '奶'].some(protein => ing.includes(protein)))) {
  429. balanceAdvice.push('建议增加蛋白质来源,如豆类或蛋类')
  430. }
  431. if (hasGrains && hasMeat) balanceAdvice.push('营养搭配均衡,适合日常食用')
  432. if (ingredients.length > 5) balanceAdvice.push('食材丰富,营养全面')
  433. return {
  434. nutrition: {
  435. calories: baseCalories,
  436. protein: hasMeat ? 20 + Math.floor(Math.random() * 15) : 8 + Math.floor(Math.random() * 8),
  437. carbs: hasGrains ? 35 + Math.floor(Math.random() * 20) : 15 + Math.floor(Math.random() * 10),
  438. fat: hasMeat ? 12 + Math.floor(Math.random() * 8) : 5 + Math.floor(Math.random() * 5),
  439. fiber: hasVegetables ? 6 + Math.floor(Math.random() * 4) : 2 + Math.floor(Math.random() * 2),
  440. sodium: 600 + Math.floor(Math.random() * 400),
  441. sugar: 3 + Math.floor(Math.random() * 5),
  442. vitaminC: hasVegetables ? 20 + Math.floor(Math.random() * 30) : undefined,
  443. calcium: hasMeat || ingredients.some(ing => ['奶', '豆'].some(ca => ing.includes(ca))) ? 100 + Math.floor(Math.random() * 100) : undefined,
  444. iron: hasMeat ? 2 + Math.floor(Math.random() * 3) : undefined
  445. },
  446. healthScore: Math.floor(Math.random() * 3) + (hasVegetables ? 6 : 4) + (hasMeat ? 1 : 0),
  447. balanceAdvice: balanceAdvice.length > 0 ? balanceAdvice : ['营养搭配合理,可以放心享用'],
  448. dietaryTags: dietaryTags.length > 0 ? dietaryTags : ['家常菜'],
  449. servingSize: '1人份'
  450. }
  451. }
  452. onUnmounted(() => {
  453. if (loadingInterval) {
  454. clearInterval(loadingInterval)
  455. }
  456. })
  457. </script>