TodayEat.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. <template>
  2. <div class="min-h-screen bg-gradient-to-br from-orange-100 to-yellow-100 p-4">
  3. <!-- 头部 -->
  4. <div class="max-w-4xl mx-auto mb-8">
  5. <div class="text-center mb-6">
  6. <h1 class="text-4xl font-bold text-orange-800 mb-2">{{ randomDice }} 今天吃什么</h1>
  7. <p class="text-orange-600">让AI为你推荐今日美食!</p>
  8. </div>
  9. <div class="text-center">
  10. <router-link to="/" class="inline-flex items-center gap-2 px-4 py-2 bg-white rounded-lg shadow hover:shadow-md transition-shadow text-gray-700">
  11. <span>←</span>
  12. <span>返回首页</span>
  13. </router-link>
  14. </div>
  15. </div>
  16. <div class="max-w-4xl mx-auto space-y-6">
  17. <!-- 开始按钮 -->
  18. <div v-if="!isSelecting && selectedDishes.length === 0" class="text-center">
  19. <div class="bg-white rounded-2xl shadow-lg p-8">
  20. <div class="text-6xl mb-4">🎲</div>
  21. <h2 class="text-2xl font-bold text-gray-800 mb-4">准备好了吗?</h2>
  22. <p class="text-gray-600 mb-6">点击按钮,让AI为你随机选择今日美食</p>
  23. <button
  24. @click="startRandomSelection"
  25. class="px-8 py-3 bg-gradient-to-r from-orange-500 to-red-500 text-white rounded-xl font-semibold hover:from-orange-600 hover:to-red-600 transition-all transform hover:scale-105 shadow-lg mb-4"
  26. >
  27. {{ randomDice }} 开始随机选择
  28. </button>
  29. <!-- 折叠配置选项 -->
  30. <div class="mt-4">
  31. <div
  32. @click="showPreference = !showPreference"
  33. class="text-sm text-gray-500 hover:text-gray-700 flex items-center justify-center gap-1 mx-auto cursor-pointer"
  34. >
  35. <span>偏好设置</span>
  36. <svg
  37. xmlns="http://www.w3.org/2000/svg"
  38. class="h-4 w-4 transition-transform"
  39. :class="{ 'transform rotate-180': showPreference }"
  40. fill="none"
  41. viewBox="0 0 24 24"
  42. stroke="currentColor"
  43. >
  44. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
  45. </svg>
  46. </div>
  47. <div v-if="showPreference" class="bg-white rounded-xl p-4 mt-2 border border-gray-200">
  48. <div class="grid grid-cols-2 gap-2">
  49. <button
  50. @click="preference = 'meat-heavy'"
  51. :class="preference === 'meat-heavy' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-800'"
  52. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
  53. >
  54. 🥩 荤菜多
  55. </button>
  56. <button
  57. @click="preference = 'veg-heavy'"
  58. :class="preference === 'veg-heavy' ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-800'"
  59. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
  60. >
  61. 🥬 素菜多
  62. </button>
  63. <button
  64. @click="preference = 'veg-only'"
  65. :class="preference === 'veg-only' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-800'"
  66. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
  67. >
  68. 🌱 纯素
  69. </button>
  70. <button
  71. @click="preference = 'meat-only'"
  72. :class="preference === 'meat-only' ? 'bg-orange-600 text-white' : 'bg-gray-100 text-gray-800'"
  73. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
  74. >
  75. 🍖 纯荤
  76. </button>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. <!-- 选择过程 -->
  83. <div v-if="isSelecting" class="bg-white rounded-2xl shadow-lg p-6">
  84. <div class="text-center mb-6">
  85. <h3 class="text-xl font-bold text-gray-800 mb-2">{{ selectionStatus }}</h3>
  86. <div class="w-full bg-gray-200 rounded-full h-2">
  87. <div class="bg-gradient-to-r from-orange-500 to-red-500 h-2 rounded-full transition-all duration-500" :style="{ width: `${selectionProgress}%` }"></div>
  88. </div>
  89. </div>
  90. <!-- 当前选择显示 -->
  91. <div v-if="currentSelection" class="text-center p-6 bg-gray-50 rounded-xl">
  92. <div class="text-4xl mb-2">{{ currentSelection.type === 'dish' ? '🍽️' : currentSelection.avatar }}</div>
  93. <div class="text-lg font-semibold text-gray-800">{{ currentSelection.name }}</div>
  94. <div v-if="currentSelection.specialty" class="text-sm text-gray-600">{{ currentSelection.specialty }}</div>
  95. </div>
  96. </div>
  97. <!-- 选择结果 -->
  98. <div v-if="!isSelecting && selectedDishes.length > 0" class="bg-white rounded-2xl shadow-lg p-6">
  99. <h3 class="text-xl font-bold text-gray-800 mb-6 text-center">🎉 今日推荐</h3>
  100. <div class="grid md:grid-cols-2 gap-6 mb-6">
  101. <!-- 菜品 -->
  102. <div class="bg-green-50 rounded-xl p-4">
  103. <h4 class="font-semibold text-green-800 mb-3 flex items-center gap-2">
  104. <span>🥗</span>
  105. <span>推荐菜品 ({{ selectedDishes.length }}道)</span>
  106. </h4>
  107. <div class="flex flex-wrap gap-2">
  108. <span v-for="dish in selectedDishes" :key="dish" class="px-3 py-1 bg-green-200 text-green-800 rounded-full text-sm">
  109. {{ dish }}
  110. </span>
  111. </div>
  112. </div>
  113. <!-- 大师 -->
  114. <div class="bg-purple-50 rounded-xl p-4">
  115. <h4 class="font-semibold text-purple-800 mb-3 flex items-center gap-2">
  116. <span>👨‍🍳</span>
  117. <span>推荐主厨</span>
  118. </h4>
  119. <div class="flex items-center gap-3">
  120. <div class="text-3xl">{{ selectedMaster?.avatar }}</div>
  121. <div>
  122. <div class="font-semibold text-purple-800">{{ selectedMaster?.name }}</div>
  123. <div class="text-sm text-purple-600">{{ selectedMaster?.specialty }}</div>
  124. </div>
  125. </div>
  126. </div>
  127. </div>
  128. <!-- 操作按钮 -->
  129. <div class="flex flex-col sm:flex-row gap-4 justify-center">
  130. <button
  131. @click="generateRecipe"
  132. :disabled="isGenerating"
  133. class="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl font-semibold hover:from-blue-600 hover:to-purple-600 transition-all transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
  134. >
  135. <span v-if="isGenerating" class="flex items-center gap-2">
  136. <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
  137. <span>{{ generatingText }}</span>
  138. </span>
  139. <span v-else class="flex items-center gap-2">
  140. <span>✨</span>
  141. <span>生成菜谱</span>
  142. </span>
  143. </button>
  144. <button
  145. @click="resetSelection"
  146. :disabled="isGenerating"
  147. class="px-6 py-3 bg-gray-500 text-white rounded-xl font-semibold hover:bg-gray-600 transition-all transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
  148. >
  149. 🎲 重新选择
  150. </button>
  151. </div>
  152. </div>
  153. <!-- 菜谱结果 -->
  154. <div v-if="recipe" class="bg-white rounded-2xl shadow-lg p-4 md:p-6">
  155. <h3 class="text-xl font-bold text-gray-800 mb-6 text-center flex items-center justify-center gap-2">
  156. <span>📖</span>
  157. <span>专属菜谱</span>
  158. </h3>
  159. <div class="border-2 border-[#333333] rounded-lg overflow-hidden">
  160. <RecipeCard :recipe="recipe" />
  161. </div>
  162. </div>
  163. </div>
  164. </div>
  165. </template>
  166. <script setup lang="ts">
  167. import { ref, onMounted } from 'vue'
  168. import { cuisines } from '@/config/cuisines'
  169. import { ingredientCategories } from '@/config/ingredients'
  170. import type { Recipe, CuisineType } from '@/types'
  171. import RecipeCard from '@/components/RecipeCard.vue'
  172. // 状态管理
  173. const isSelecting = ref(false)
  174. const isGenerating = ref(false)
  175. const selectedDishes = ref<string[]>([])
  176. const selectedMaster = ref<CuisineType | null>(null)
  177. const recipe = ref<Recipe | null>(null)
  178. const preference = ref<string | null>(null)
  179. const showPreference = ref(false)
  180. // 选择过程状态
  181. const selectionStatus = ref('')
  182. const selectionProgress = ref(0)
  183. const currentSelection = ref<any>(null)
  184. // 文字轮播
  185. const generatingText = ref('正在生成菜谱...')
  186. const generatingTexts = ['正在生成菜谱...', '大师正在创作...', '调配独特配方...', '完善制作步骤...']
  187. // 随机筛子表情
  188. const diceEmojis = ['🎯']
  189. const randomDice = ref('🎯')
  190. // 所有菜品数据
  191. const allDishes = ref<string[]>([])
  192. // 初始化
  193. onMounted(() => {
  194. allDishes.value = ingredientCategories.flatMap(category => category.items)
  195. randomDice.value = diceEmojis[Math.floor(Math.random() * diceEmojis.length)]
  196. })
  197. // 开始随机选择
  198. const startRandomSelection = async () => {
  199. isSelecting.value = true
  200. selectedDishes.value = []
  201. selectedMaster.value = null
  202. recipe.value = null
  203. selectionProgress.value = 0
  204. // 第一阶段:选择菜品
  205. selectionStatus.value = '正在随机选择菜品...'
  206. await selectRandomDishes()
  207. // 第二阶段:选择大师
  208. selectionStatus.value = '正在匹配主厨大师...'
  209. await selectRandomMaster()
  210. // 完成
  211. selectionStatus.value = '选择完成!'
  212. selectionProgress.value = 100
  213. setTimeout(() => {
  214. isSelecting.value = false
  215. }, 1000)
  216. }
  217. // 随机选择菜品
  218. const selectRandomDishes = async () => {
  219. const dishCount = Math.floor(Math.random() * 3) + 4 // 4-6个菜品
  220. let filteredDishes = [...allDishes.value]
  221. // 根据偏好过滤菜品
  222. if (preference.value) {
  223. const meatCategories = ['meat', 'seafood']
  224. const vegCategories = ['vegetables', 'mushrooms', 'beans']
  225. if (preference.value === 'meat-heavy') {
  226. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => meatCategories.includes(cat.id) && cat.items.includes(dish)))
  227. } else if (preference.value === 'veg-heavy') {
  228. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => vegCategories.includes(cat.id) && cat.items.includes(dish)))
  229. } else if (preference.value === 'meat-only') {
  230. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => meatCategories.includes(cat.id) && cat.items.includes(dish)))
  231. } else if (preference.value === 'veg-only') {
  232. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => vegCategories.includes(cat.id) && cat.items.includes(dish)))
  233. }
  234. }
  235. const shuffledDishes = filteredDishes.sort(() => 0.5 - Math.random())
  236. for (let i = 0; i < dishCount; i++) {
  237. // 模拟选择过程
  238. for (let j = 0; j < 5; j++) {
  239. const randomDish = shuffledDishes[Math.floor(Math.random() * shuffledDishes.length)]
  240. currentSelection.value = {
  241. type: 'dish',
  242. name: randomDish
  243. }
  244. selectionProgress.value = ((i * 5 + j) / (dishCount * 5)) * 50
  245. await new Promise(resolve => setTimeout(resolve, 50))
  246. }
  247. // 确定选择
  248. const finalDish = shuffledDishes[i]
  249. if (!selectedDishes.value.includes(finalDish)) {
  250. selectedDishes.value.push(finalDish)
  251. }
  252. currentSelection.value = {
  253. type: 'dish',
  254. name: finalDish
  255. }
  256. await new Promise(resolve => setTimeout(resolve, 300))
  257. }
  258. }
  259. // 随机选择大师
  260. const selectRandomMaster = async () => {
  261. // 模拟选择过程
  262. for (let i = 0; i < 10; i++) {
  263. const randomMaster = cuisines[Math.floor(Math.random() * cuisines.length)]
  264. currentSelection.value = {
  265. type: 'master',
  266. name: randomMaster.name,
  267. avatar: randomMaster.avatar,
  268. specialty: randomMaster.specialty
  269. }
  270. selectionProgress.value = 50 + (i / 10) * 50
  271. await new Promise(resolve => setTimeout(resolve, 80))
  272. }
  273. // 确定选择
  274. const finalMaster = cuisines[Math.floor(Math.random() * cuisines.length)]
  275. selectedMaster.value = finalMaster
  276. currentSelection.value = {
  277. type: 'master',
  278. name: finalMaster.name,
  279. avatar: finalMaster.avatar,
  280. specialty: finalMaster.specialty
  281. }
  282. await new Promise(resolve => setTimeout(resolve, 500))
  283. }
  284. // 生成菜谱
  285. const generateRecipe = async () => {
  286. if (!selectedMaster.value || selectedDishes.value.length === 0 || isGenerating.value) return
  287. isGenerating.value = true
  288. // 文字轮播
  289. let textIndex = 0
  290. const textInterval = setInterval(() => {
  291. generatingText.value = generatingTexts[textIndex]
  292. textIndex = (textIndex + 1) % generatingTexts.length
  293. }, 1000)
  294. try {
  295. // 模拟生成过程
  296. await new Promise(resolve => setTimeout(resolve, 4000))
  297. // 创建菜谱
  298. const dishNames = selectedDishes.value.slice(0, 3).join('、')
  299. const recipeName =
  300. selectedDishes.value.length > 3
  301. ? `${selectedMaster.value.name}特制${dishNames}等${selectedDishes.value.length}样组合`
  302. : `${selectedMaster.value.name}特制${dishNames}组合`
  303. const mockRecipe: Recipe = {
  304. id: `today-recipe-${Date.now()}`,
  305. name: recipeName,
  306. cuisine: selectedMaster.value.name,
  307. ingredients: [...selectedDishes.value, '盐', '生抽', '料酒', '葱', '姜', '蒜', '香油', '胡椒粉'],
  308. steps: [
  309. {
  310. step: 1,
  311. description: `将所有食材清洗干净:${selectedDishes.value.join('、')}分别处理,切成适当大小`,
  312. time: 8
  313. },
  314. {
  315. step: 2,
  316. description: '热锅下油,先爆香葱姜蒜,制作底味',
  317. time: 2,
  318. temperature: '中火'
  319. },
  320. {
  321. step: 3,
  322. description: `按照食材特性分批下锅:先下${selectedDishes.value[0]}等较难熟的食材`,
  323. time: 4,
  324. temperature: '大火'
  325. },
  326. {
  327. step: 4,
  328. description: `再加入${selectedDishes.value.slice(1).join('、')}等食材,快速翻炒`,
  329. time: 3,
  330. temperature: '大火'
  331. },
  332. {
  333. step: 5,
  334. description: '调入生抽、料酒、盐等调料,炒匀入味',
  335. time: 2
  336. },
  337. {
  338. step: 6,
  339. description: '最后淋香油,撒胡椒粉,装盘即可',
  340. time: 1
  341. }
  342. ],
  343. cookingTime: 20,
  344. difficulty: selectedDishes.value.length > 5 ? 'medium' : 'easy',
  345. tips: [
  346. '多种食材搭配,营养更加均衡丰富',
  347. '不同食材的下锅时间要掌握好,避免有的过熟有的不熟',
  348. '调料用量要根据食材总量和个人口味调整',
  349. `${selectedMaster.value.specialty}的特色在于食材搭配的层次感`
  350. ]
  351. }
  352. recipe.value = mockRecipe
  353. } catch (error) {
  354. console.error('生成菜谱失败:', error)
  355. } finally {
  356. clearInterval(textInterval)
  357. isGenerating.value = false
  358. }
  359. }
  360. // 重置选择
  361. const resetSelection = () => {
  362. selectedDishes.value = []
  363. selectedMaster.value = null
  364. recipe.value = null
  365. currentSelection.value = null
  366. selectionProgress.value = 0
  367. }
  368. </script>
  369. <style scoped>
  370. /* 基础动画 */
  371. @keyframes fadeIn {
  372. from {
  373. opacity: 0;
  374. transform: translateY(20px);
  375. }
  376. to {
  377. opacity: 1;
  378. transform: translateY(0);
  379. }
  380. }
  381. @keyframes pulse {
  382. 0%,
  383. 100% {
  384. transform: scale(1);
  385. }
  386. 50% {
  387. transform: scale(1.05);
  388. }
  389. }
  390. @keyframes spin {
  391. from {
  392. transform: rotate(0deg);
  393. }
  394. to {
  395. transform: rotate(360deg);
  396. }
  397. }
  398. /* 应用动画 */
  399. .animate-spin {
  400. animation: spin 1s linear infinite;
  401. }
  402. /* 卡片入场动画 */
  403. .bg-white {
  404. animation: fadeIn 0.6s ease-out;
  405. }
  406. /* 按钮悬停效果 */
  407. button {
  408. transition: all 0.3s ease;
  409. }
  410. button:hover:not(:disabled) {
  411. transform: translateY(-2px);
  412. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
  413. }
  414. button:active:not(:disabled) {
  415. transform: translateY(0);
  416. background-color: transparent;
  417. }
  418. /* 偏好设置特殊样式 */
  419. div.text-sm.text-gray-500:hover {
  420. background-color: transparent;
  421. }
  422. /* 标签悬停效果 */
  423. .bg-green-200,
  424. .bg-purple-50 {
  425. transition: all 0.3s ease;
  426. }
  427. .bg-green-200:hover {
  428. transform: translateY(-1px);
  429. box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2);
  430. }
  431. /* 进度条动画 */
  432. .bg-gradient-to-r {
  433. transition: width 0.5s ease-out;
  434. }
  435. /* 当前选择项的脉冲效果 */
  436. .text-4xl {
  437. animation: pulse 2s ease-in-out infinite;
  438. }
  439. /* 响应式调整 */
  440. @media (max-width: 640px) {
  441. .text-4xl {
  442. font-size: 2rem;
  443. }
  444. .text-6xl {
  445. font-size: 3rem;
  446. }
  447. .px-8 {
  448. padding-left: 1.5rem;
  449. padding-right: 1.5rem;
  450. }
  451. }
  452. </style>