Home.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <div class="min-h-screen bg-gradient-to-br from-primary-50 to-primary-200">
  3. <!-- 头部 -->
  4. <header class="relative overflow-hidden">
  5. <div class="absolute inset-0 bg-gradient-to-r from-accent-600 via-accent-500 to-primary-500"></div>
  6. <div class="absolute inset-0 bg-black/10"></div>
  7. <div class="relative max-w-6xl mx-auto px-4 py-8">
  8. <div class="text-center">
  9. <div class="inline-flex items-center gap-4 mb-3">
  10. <div class="text-4xl cooking-animation">🍳</div>
  11. <h1 class="text-4xl font-black text-white tracking-tight">一饭封神</h1>
  12. </div>
  13. <p class="text-white/80 text-lg font-medium">让AI厨艺大师为您的食材创造美味</p>
  14. </div>
  15. </div>
  16. </header>
  17. <main class="max-w-6xl mx-auto px-4 py-8">
  18. <!-- 主要输入区域 -->
  19. <div class="bg-white/95 backdrop-blur-sm rounded-3xl shadow-2xl p-12 mb-8 pulse-glow">
  20. <div class="text-center mb-10">
  21. <h2 class="text-4xl font-bold text-neutral-800 mb-4">🥬 告诉我你有什么食材</h2>
  22. <p class="text-neutral-600 text-xl">输入你现有的食材,让AI大师为你创造美味</p>
  23. </div>
  24. <!-- 食材输入框 -->
  25. <div class="relative mb-8">
  26. <div
  27. class="flex flex-wrap gap-4 min-h-[120px] p-10 border-4 border-dashed border-accent-300 rounded-3xl bg-gradient-to-r from-accent-50 to-primary-50 hover:border-accent-400 transition-all duration-300 focus-within:border-accent-500 focus-within:shadow-xl"
  28. >
  29. <div
  30. v-for="ingredient in ingredients"
  31. :key="ingredient"
  32. class="inline-flex items-center gap-3 bg-accent-500 text-white px-6 py-3 rounded-full text-xl font-medium shadow-lg hover:bg-accent-600 transition-colors"
  33. >
  34. {{ ingredient }}
  35. <button @click="removeIngredient(ingredient)" class="hover:bg-accent-700 rounded-full p-1 transition-colors">
  36. <span class="text-lg">✕</span>
  37. </button>
  38. </div>
  39. <input
  40. v-model="currentIngredient"
  41. @keyup.enter="addIngredient"
  42. placeholder="输入食材名称,按回车添加..."
  43. class="flex-1 min-w-[350px] bg-transparent outline-none text-neutral-800 placeholder-neutral-500 text-2xl font-medium"
  44. />
  45. </div>
  46. </div>
  47. <!-- 生成按钮 -->
  48. <div class="text-center">
  49. <button
  50. @click="generateRecipes"
  51. :disabled="ingredients.length === 0 || isLoading"
  52. class="bg-gradient-to-r from-accent-600 to-accent-700 hover:from-accent-700 hover:to-accent-800 disabled:from-gray-300 disabled:to-gray-400 text-white px-12 py-4 rounded-2xl font-bold text-xl shadow-xl transition-all duration-300 transform hover:scale-105 disabled:scale-100 disabled:cursor-not-allowed"
  53. >
  54. <span v-if="!isLoading" class="flex items-center gap-3"> 👨‍🍳 交给大师 </span>
  55. </button>
  56. </div>
  57. </div>
  58. <!-- 厨艺大师选择区域 -->
  59. <div class="bg-white/70 backdrop-blur-sm rounded-2xl shadow-md p-6 mb-8">
  60. <details class="group">
  61. <summary class="cursor-pointer text-center p-4 hover:bg-neutral-50 rounded-xl transition-colors">
  62. <h3 class="text-lg font-medium text-neutral-700 inline-flex items-center gap-2">
  63. 👨‍🍳 选择厨艺大师
  64. <span class="text-sm text-neutral-500">(可选,默认随机选择)</span>
  65. <span class="group-open:rotate-180 transition-transform duration-300">▼</span>
  66. </h3>
  67. </summary>
  68. <div class="mt-4 grid grid-cols-3 gap-4">
  69. <div
  70. v-for="cuisine in cuisines"
  71. :key="cuisine.id"
  72. @click="selectCuisine(cuisine)"
  73. :class="[
  74. 'cursor-pointer p-4 rounded-xl border-2 transition-all duration-200 hover:scale-105',
  75. selectedCuisines.includes(cuisine.id)
  76. ? 'border-accent-500 bg-accent-100 shadow-lg ring-2 ring-accent-200'
  77. : 'border-neutral-200 bg-white hover:border-accent-300 hover:shadow-md'
  78. ]"
  79. >
  80. <div class="text-center">
  81. <div class="text-3xl mb-2">{{ cuisine.avatar }}</div>
  82. <h4 class="font-semibold text-neutral-800 text-base">{{ cuisine.name }}</h4>
  83. <p class="text-sm text-neutral-600 mt-1">{{ cuisine.specialty }}</p>
  84. </div>
  85. </div>
  86. </div>
  87. </details>
  88. </div>
  89. <!-- 加载动效 -->
  90. <div v-if="isLoading" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
  91. <CookingLoader />
  92. </div>
  93. <!-- 菜谱展示区域 -->
  94. <div v-if="recipes.length > 0" class="space-y-8">
  95. <h2 class="text-4xl font-bold text-neutral-800 text-center mb-8">🍽️ 大师们的杰作</h2>
  96. <!-- 菜谱网格布局 -->
  97. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
  98. <div
  99. v-for="recipe in recipes"
  100. :key="recipe.id"
  101. class="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl border border-accent-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02] flex flex-col"
  102. >
  103. <RecipeCard :recipe="recipe" />
  104. </div>
  105. </div>
  106. <!-- 如果菜谱较多,添加一个回到顶部按钮 -->
  107. <div v-if="recipes.length > 3" class="text-center mt-8">
  108. <button
  109. @click="scrollToTop"
  110. class="bg-accent-500 hover:bg-accent-600 text-white px-6 py-3 rounded-full font-medium shadow-lg transition-all duration-300 transform hover:scale-105"
  111. >
  112. ⬆️ 回到顶部
  113. </button>
  114. </div>
  115. </div>
  116. </main>
  117. </div>
  118. </template>
  119. <script setup lang="ts">
  120. import { ref } from 'vue'
  121. import { cuisines } from '@/config/cuisines'
  122. import RecipeCard from '@/components/RecipeCard.vue'
  123. import CookingLoader from '@/components/CookingLoader.vue'
  124. import { generateMultipleRecipes } from '@/services/aiService'
  125. import type { Recipe, CuisineType } from '@/types'
  126. // 响应式数据
  127. const ingredients = ref<string[]>([])
  128. const currentIngredient = ref('')
  129. const selectedCuisines = ref<string[]>([])
  130. const recipes = ref<Recipe[]>([])
  131. const isLoading = ref(false)
  132. // 添加食材
  133. const addIngredient = () => {
  134. const ingredient = currentIngredient.value.trim()
  135. if (ingredient && !ingredients.value.includes(ingredient)) {
  136. ingredients.value.push(ingredient)
  137. currentIngredient.value = ''
  138. }
  139. }
  140. // 移除食材
  141. const removeIngredient = (ingredient: string) => {
  142. const index = ingredients.value.indexOf(ingredient)
  143. if (index > -1) {
  144. ingredients.value.splice(index, 1)
  145. }
  146. }
  147. // 选择菜系
  148. const selectCuisine = (cuisine: CuisineType) => {
  149. const index = selectedCuisines.value.indexOf(cuisine.id)
  150. if (index > -1) {
  151. selectedCuisines.value.splice(index, 1)
  152. } else {
  153. selectedCuisines.value.push(cuisine.id)
  154. }
  155. }
  156. // 生成菜谱
  157. const generateRecipes = async () => {
  158. if (ingredients.value.length === 0) {
  159. return
  160. }
  161. isLoading.value = true
  162. recipes.value = []
  163. try {
  164. // 根据选择的菜系数量决定生成菜谱
  165. let selectedCuisineObjects = cuisines.filter(c => selectedCuisines.value.includes(c.id))
  166. if (selectedCuisineObjects.length === 0) {
  167. // 随机选择2-3个菜系
  168. const shuffled = [...cuisines].sort(() => 0.5 - Math.random())
  169. selectedCuisineObjects = shuffled.slice(0, Math.floor(Math.random() * 2) + 2)
  170. }
  171. // 调用AI服务生成菜谱
  172. const generatedRecipes = await generateMultipleRecipes(ingredients.value, selectedCuisineObjects)
  173. recipes.value = generatedRecipes
  174. } catch (error) {
  175. console.error('生成菜谱失败:', error)
  176. // 如果AI调用失败,使用模拟数据作为后备
  177. await simulateAICall()
  178. } finally {
  179. isLoading.value = false
  180. }
  181. }
  182. // 模拟AI调用(后续替换为真实接口)
  183. const simulateAICall = async () => {
  184. return new Promise(resolve => {
  185. setTimeout(() => {
  186. // 获取要使用的菜系
  187. let cuisinesToUse = cuisines.filter(c => selectedCuisines.value.includes(c.id))
  188. if (cuisinesToUse.length === 0) {
  189. // 随机选择2-3个菜系
  190. const shuffled = [...cuisines].sort(() => 0.5 - Math.random())
  191. cuisinesToUse = shuffled.slice(0, Math.floor(Math.random() * 2) + 2)
  192. }
  193. // 模拟生成菜谱数据
  194. const mockRecipes: Recipe[] = cuisinesToUse.map((cuisine, index) => {
  195. return {
  196. id: `recipe-${cuisine.id}-${Date.now()}-${index}`,
  197. name: `${cuisine.name}推荐:${ingredients.value.join('')}料理`,
  198. cuisine: cuisine.name,
  199. ingredients: ingredients.value,
  200. steps: [
  201. { step: 1, description: '准备所有食材,清洗干净', time: 5 },
  202. { step: 2, description: '热锅下油,爆香配料', time: 3 },
  203. { step: 3, description: '下主料翻炒至半熟', time: 8 },
  204. { step: 4, description: '调味炒制至熟透', time: 5 },
  205. { step: 5, description: '装盘即可享用', time: 1 }
  206. ],
  207. cookingTime: 22,
  208. difficulty: 'medium',
  209. tips: ['火候要掌握好,避免炒糊', '调料要适量,突出食材本味', '炒制过程中要勤翻动']
  210. }
  211. })
  212. recipes.value = mockRecipes
  213. resolve(mockRecipes)
  214. }, 3000)
  215. })
  216. }
  217. // 回到顶部功能
  218. const scrollToTop = () => {
  219. window.scrollTo({
  220. top: 0,
  221. behavior: 'smooth'
  222. })
  223. }
  224. </script>