HowToCook.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <template>
  2. <div class="min-h-screen bg-yellow-400 px-2 md:px-4 py-6">
  3. <!-- 全局导航 -->
  4. <GlobalNavigation />
  5. <div class="max-w-7xl mx-auto">
  6. <!-- 输入区域 -->
  7. <div class="mb-8">
  8. <div class="bg-pink-400 text-white px-4 py-2 rounded-t-lg border-2 border-[#0A0910] border-b-0 inline-block">
  9. <span class="font-bold">1. 输入菜名</span>
  10. </div>
  11. <div class="bg-white border-2 border-[#0A0910] rounded-lg rounded-tl-none p-4 md:p-6">
  12. <div class="space-y-4">
  13. <!-- 菜名输入框 -->
  14. <div class="relative">
  15. <input
  16. v-model="dishName"
  17. @keyup.enter="searchRecipe"
  18. placeholder="例如:红烧肉、宫保鸡丁、麻婆豆腐..."
  19. class="w-full p-4 border-2 border-[#0A0910] rounded-lg text-md font-medium focus:outline-none focus:ring-2 focus:ring-pink-400"
  20. />
  21. <div class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400">
  22. <span class="text-2xl">🔍</span>
  23. </div>
  24. </div>
  25. <!-- 搜索按钮 -->
  26. <button
  27. @click="searchRecipe"
  28. :disabled="!dishName.trim() || isLoading"
  29. class="w-full bg-gradient-to-r from-pink-500 to-red-500 hover:from-pink-600 hover:to-red-600 disabled:from-gray-400 disabled:to-gray-400 text-white px-6 py-4 rounded-lg font-bold text-md border-2 border-[#0A0910] transition-all duration-300 transform disabled:scale-100 disabled:cursor-not-allowed shadow-lg"
  30. >
  31. <span class="flex items-center gap-2 justify-center">
  32. <template v-if="isLoading">
  33. <div class="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
  34. <span>AI大师思考中...</span>
  35. </template>
  36. <template v-else>
  37. <span class="text-xl">🔍</span>
  38. <span>开始学做菜</span>
  39. </template>
  40. </span>
  41. </button>
  42. </div>
  43. </div>
  44. </div>
  45. <!-- 菜谱结果 -->
  46. <div class="mb-8" data-results>
  47. <div class="bg-green-400 text-white px-4 py-2 rounded-t-lg border-2 border-[#0A0910] border-b-0 inline-block">
  48. <span class="font-bold">2. 制作教程</span>
  49. </div>
  50. <div class="bg-white border-2 border-[#0A0910] rounded-lg rounded-tl-none p-4 md:p-6">
  51. <!-- 空状态提示 -->
  52. <div v-if="!recipe && !isLoading" class="text-center py-12">
  53. <div class="w-16 h-16 bg-gradient-to-br from-gray-300 to-gray-400 rounded-lg flex items-center justify-center mx-auto mb-4">
  54. <span class="text-white text-2xl">🍳</span>
  55. </div>
  56. <h3 class="text-lg font-bold text-gray-600 mb-3">等待您的菜名...</h3>
  57. <div class="space-y-2 text-sm text-gray-500 max-w-md mx-auto">
  58. <p class="flex items-center justify-center gap-2">
  59. <span>💡</span>
  60. <span>输入具体菜名效果更好,如"红烧肉"</span>
  61. </p>
  62. <p class="flex items-center justify-center gap-2">
  63. <span>🌟</span>
  64. <span>支持各种菜系:川菜、粤菜、湘菜等</span>
  65. </p>
  66. <p class="flex items-center justify-center gap-2">
  67. <span>📝</span>
  68. <span>包含详细步骤、用料和烹饪技巧</span>
  69. </p>
  70. </div>
  71. </div>
  72. <!-- 加载状态 -->
  73. <div v-if="isLoading && !recipe" class="text-center py-12">
  74. <div class="w-16 h-16 bg-gradient-to-br from-orange-400 to-red-500 rounded-lg flex items-center justify-center mx-auto mb-4 animate-pulse">
  75. <span class="text-white text-2xl">👨‍🍳</span>
  76. </div>
  77. <h3 class="text-xl font-bold text-gray-700 mb-2">AI大师正在为您准备教程...</h3>
  78. <p class="text-gray-500">请稍等片刻,精彩内容即将呈现</p>
  79. <div class="mt-4">
  80. <div class="animate-spin w-8 h-8 border-4 border-pink-400 border-t-transparent rounded-full mx-auto"></div>
  81. </div>
  82. </div>
  83. <!-- 菜谱内容 -->
  84. <div v-if="recipe" class="max-w-2xl mx-auto border-2 border-[#333333] rounded-lg overflow-hidden">
  85. <RecipeCard :recipe="recipe" :show-actions="true" />
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 历史记录 -->
  90. <div v-if="searchHistory.length > 0" class="mb-8">
  91. <div class="bg-blue-400 text-white px-4 py-2 rounded-t-lg border-2 border-[#0A0910] border-b-0 inline-block">
  92. <span class="font-bold">最近搜索</span>
  93. </div>
  94. <div class="bg-white border-2 border-[#0A0910] rounded-lg rounded-tl-none p-4">
  95. <div class="flex flex-wrap gap-2">
  96. <button
  97. v-for="historyItem in searchHistory.slice(0, 8)"
  98. :key="historyItem"
  99. @click="selectDish(historyItem)"
  100. class="px-3 py-2 text-sm bg-blue-100 text-blue-700 rounded-full border border-blue-300 hover:bg-blue-200 hover:border-blue-400 transition-all duration-200"
  101. >
  102. {{ historyItem }}
  103. </button>
  104. <button
  105. v-if="searchHistory.length > 0"
  106. @click="clearHistory"
  107. class="px-3 py-2 text-sm text-red-600 hover:text-red-700 underline"
  108. >
  109. 清除历史
  110. </button>
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. <!-- 全局底部 -->
  116. <GlobalFooter />
  117. </div>
  118. </template>
  119. <script setup lang="ts">
  120. import { ref, onMounted } from 'vue'
  121. import { generateDishRecipeByName } from '@/services/aiService'
  122. import type { Recipe } from '@/types'
  123. import RecipeCard from '@/components/RecipeCard.vue'
  124. import GlobalNavigation from '@/components/GlobalNavigation.vue'
  125. import GlobalFooter from '@/components/GlobalFooter.vue'
  126. // 响应式数据
  127. const dishName = ref('')
  128. const recipe = ref<Recipe | null>(null)
  129. const isLoading = ref(false)
  130. const searchHistory = ref<string[]>([])
  131. // 页面加载时恢复历史记录
  132. onMounted(() => {
  133. const saved = localStorage.getItem('howToCook_history')
  134. if (saved) {
  135. try {
  136. searchHistory.value = JSON.parse(saved)
  137. } catch (e) {
  138. console.error('恢复搜索历史失败:', e)
  139. }
  140. }
  141. })
  142. // 选择菜品
  143. const selectDish = (dish: string) => {
  144. dishName.value = dish
  145. searchRecipe()
  146. }
  147. // 搜索菜谱
  148. const searchRecipe = async () => {
  149. if (!dishName.value.trim() || isLoading.value) return
  150. const searchTerm = dishName.value.trim()
  151. // 添加到历史记录
  152. if (!searchHistory.value.includes(searchTerm)) {
  153. searchHistory.value.unshift(searchTerm)
  154. if (searchHistory.value.length > 20) {
  155. searchHistory.value = searchHistory.value.slice(0, 20)
  156. }
  157. localStorage.setItem('howToCook_history', JSON.stringify(searchHistory.value))
  158. }
  159. isLoading.value = true
  160. recipe.value = null
  161. try {
  162. const result = await generateDishRecipeByName(searchTerm)
  163. recipe.value = result
  164. // 滚动到结果区域
  165. setTimeout(() => {
  166. const resultsElement = document.querySelector('[data-results]')
  167. if (resultsElement) {
  168. resultsElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
  169. }
  170. }, 100)
  171. } catch (error) {
  172. console.error('搜索菜谱失败:', error)
  173. // 这里可以添加错误提示
  174. } finally {
  175. isLoading.value = false
  176. }
  177. }
  178. // 清除历史记录
  179. const clearHistory = () => {
  180. searchHistory.value = []
  181. localStorage.removeItem('howToCook_history')
  182. }
  183. </script>
  184. <style scoped>
  185. @keyframes fade-in-up {
  186. from {
  187. opacity: 0;
  188. transform: translateY(20px);
  189. }
  190. to {
  191. opacity: 1;
  192. transform: translateY(0);
  193. }
  194. }
  195. .animate-fade-in-up {
  196. animation: fade-in-up 0.6s ease-out;
  197. }
  198. </style>