TodayEat.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 space-y-6 rounded-lg">
  6. <!-- 开始按钮 -->
  7. <div v-if="!isSelecting && selectedDishes.length === 0" class="text-center">
  8. <div class="bg-white rounded-lg shadow-lg p-8 border-2 border-[#0A0910]">
  9. <div class="text-6xl mb-4">🎲</div>
  10. <h2 class="text-2xl font-bold text-gray-800 mb-4">准备好了吗?</h2>
  11. <button
  12. @click="startRandomSelection"
  13. 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"
  14. >
  15. {{ randomDice }} 开始随机选择
  16. </button>
  17. <!-- 折叠配置选项 -->
  18. <div class="mt-4 mb-6">
  19. <div
  20. @click="showPreference = !showPreference"
  21. class="text-sm text-gray-500 hover:text-gray-700 flex items-center justify-center gap-1 mx-auto cursor-pointer"
  22. >
  23. <span>偏好设置</span>
  24. <svg
  25. xmlns="http://www.w3.org/2000/svg"
  26. class="h-4 w-4 transition-transform"
  27. :class="{ 'transform rotate-180': showPreference }"
  28. fill="none"
  29. viewBox="0 0 24 24"
  30. stroke="currentColor"
  31. >
  32. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
  33. </svg>
  34. </div>
  35. <div v-if="showPreference" class="bg-white rounded-xl max-w-md mx-auto p-4 mt-2 border border-gray-200">
  36. <div class="grid grid-cols-2 gap-2 md:flex md:flex-row md:gap-4">
  37. <button
  38. @click="preference = 'meat-heavy'"
  39. :class="preference === 'meat-heavy' ? 'bg-orange-500 text-white' : 'bg-gray-100 text-gray-800'"
  40. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors md:flex-1"
  41. >
  42. 🥩 荤菜多
  43. </button>
  44. <button
  45. @click="preference = 'veg-heavy'"
  46. :class="preference === 'veg-heavy' ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-800'"
  47. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors md:flex-1"
  48. >
  49. 🥬 素菜多
  50. </button>
  51. <button
  52. @click="preference = 'veg-only'"
  53. :class="preference === 'veg-only' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-800'"
  54. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors md:flex-1"
  55. >
  56. 🌱 纯素
  57. </button>
  58. <button
  59. @click="preference = 'meat-only'"
  60. :class="preference === 'meat-only' ? 'bg-orange-600 text-white' : 'bg-gray-100 text-gray-800'"
  61. class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors md:flex-1"
  62. >
  63. 🍖 纯荤
  64. </button>
  65. </div>
  66. </div>
  67. </div>
  68. <hr class="mb-4" />
  69. <p class="text-gray-500 text-sm italic transition">{{ randomFoodComment }}</p>
  70. </div>
  71. </div>
  72. <!-- 选择过程 -->
  73. <div v-if="isSelecting" class="bg-white rounded-2xl shadow-lg p-6">
  74. <div class="text-center mb-6 max-w-2xl mx-auto">
  75. <h3 class="text-xl font-bold text-gray-800 mb-2">{{ selectionStatus }}</h3>
  76. <div class="w-full bg-gray-200 rounded-full h-2">
  77. <div class="bg-gradient-to-r from-orange-500 to-red-500 h-2 rounded-full transition-all duration-500" :style="{ width: `${selectionProgress}%` }"></div>
  78. </div>
  79. </div>
  80. <!-- 当前选择显示 -->
  81. <div v-if="currentSelection" class="text-center p-6 bg-gray-50 rounded-xl">
  82. <div class="text-4xl mb-2">{{ currentSelection.type === 'dish' ? '🍽️' : currentSelection.avatar }}</div>
  83. <div class="text-lg font-semibold text-gray-800">{{ currentSelection.name }}</div>
  84. <div v-if="currentSelection.specialty" class="text-sm text-gray-600">{{ currentSelection.specialty }}</div>
  85. </div>
  86. </div>
  87. <!-- 选择结果 -->
  88. <div v-if="!isSelecting && selectedDishes.length > 0" class="bg-white rounded-2xl shadow-lg p-6 border-2 border-[#0A0910]">
  89. <h3 class="text-xl font-bold text-gray-800 mb-6 text-center">🎉 今日推荐</h3>
  90. <div class="grid md:grid-cols-2 gap-6 mb-6">
  91. <!-- 菜品 -->
  92. <div class="bg-green-50 rounded-xl p-4">
  93. <h4 class="font-semibold text-green-800 mb-3 flex items-center gap-2">
  94. <span>🥗</span>
  95. <span>推荐菜品 ({{ selectedDishes.length }}道)</span>
  96. </h4>
  97. <div class="grid grid-cols-2 gap-2">
  98. <div v-for="dish in selectedDishes" :key="dish" class="bg-white border-2 border-green-200 rounded-lg p-2 hover:bg-green-100 transition-colors">
  99. <div class="text-sm font-medium text-green-800 text-center">{{ dish }}</div>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- 大师 -->
  104. <div class="bg-purple-50 rounded-xl p-4">
  105. <h4 class="font-semibold text-purple-800 mb-3 flex items-center gap-2">
  106. <span>👨‍🍳</span>
  107. <span>推荐主厨</span>
  108. </h4>
  109. <div class="flex items-center gap-3">
  110. <div class="text-3xl">{{ selectedMaster?.avatar }}</div>
  111. <div>
  112. <div class="font-semibold text-purple-800">{{ selectedMaster?.name }}</div>
  113. <div class="text-sm text-purple-600">{{ selectedMaster?.specialty }}</div>
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. <!-- 操作按钮 -->
  119. <div class="flex flex-col sm:flex-row gap-4 justify-center">
  120. <button
  121. @click="generateRecipeFromSelection"
  122. :disabled="isGenerating"
  123. 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"
  124. >
  125. <span v-if="isGenerating" class="flex items-center gap-2">
  126. <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
  127. <span>{{ generatingText }}</span>
  128. </span>
  129. <span v-else class="flex items-center gap-2">
  130. <span>✨</span>
  131. <span>生成菜谱</span>
  132. </span>
  133. </button>
  134. <button
  135. @click="resetSelection"
  136. :disabled="isGenerating"
  137. 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"
  138. >
  139. 🎲 重新选择
  140. </button>
  141. </div>
  142. </div>
  143. <!-- 菜谱结果 -->
  144. <div v-if="recipe" class="bg-white rounded-2xl shadow-lg p-4 md:p-6 border-2 border-[#0A0910]">
  145. <h3 class="text-xl font-bold text-gray-800 mb-6 text-center flex items-center justify-center gap-2">
  146. <span>📖</span>
  147. <span>专属菜谱</span>
  148. </h3>
  149. <div class="max-w-2xl mx-auto border-2 border-[#333333] rounded-lg overflow-hidden">
  150. <RecipeCard :recipe="recipe" />
  151. </div>
  152. </div>
  153. </div>
  154. <!-- 底部 -->
  155. <GlobalFooter />
  156. </div>
  157. </template>
  158. <script setup lang="ts">
  159. import { ref, onMounted, onUnmounted, computed } from 'vue'
  160. import { cuisines } from '@/config/cuisines'
  161. import { ingredientCategories } from '@/config/ingredients'
  162. import type { Recipe, CuisineType } from '@/types'
  163. import { generateRecipe } from '@/services/aiService'
  164. import RecipeCard from '@/components/RecipeCard.vue'
  165. import GlobalNavigation from '@/components/GlobalNavigation.vue'
  166. import GlobalFooter from '@/components/GlobalFooter.vue'
  167. // 状态管理
  168. const isSelecting = ref(false)
  169. const isGenerating = ref(false)
  170. const selectedDishes = ref<string[]>([])
  171. const selectedMaster = ref<CuisineType | null>(null)
  172. const recipe = ref<Recipe | null>(null)
  173. const preference = ref<string | null>(null)
  174. const showPreference = ref(false)
  175. // 选择过程状态
  176. const selectionStatus = ref('')
  177. const selectionProgress = ref(0)
  178. const currentSelection = ref<any>(null)
  179. // 文字轮播
  180. const generatingText = ref('正在生成菜谱...')
  181. const generatingTexts = ['正在生成菜谱...', '大师正在创作...', '调配独特配方...', '完善制作步骤...']
  182. // 随机筛子表情
  183. const diceEmojis = ['🎯']
  184. const randomDice = ref('🎯')
  185. // 美食评论
  186. const foodComments = [
  187. "💬 鲁菜大师看到我的五花肉,直接拍案而起:'今天必须教你什么叫真正的把子肉!' 🐷🔥",
  188. "💬 川菜大师盯着我的鸡胸肉冷笑:'莫得问题,马上让你体验什么叫麻辣鸡丝怀疑人生' 🌶️😭",
  189. '💬 给粤菜大师一根白萝卜,他能还你一桌国宴级开水白菜...而我只会凉拌 🦢🤷',
  190. '💬 日料大师处理我的三文鱼时,刀光闪过,鱼生薄得能当手机贴膜 🍣📱',
  191. "💬 湘菜大师的炒锅起火三米高:'辣椒放少了!你这是对湖南人的侮辱!' 🔥🌶️",
  192. "💬 当法餐大师看到我用速冻牛排:'亲爱的,这需要先做个SPA再按摩48小时' 🥩💆",
  193. '💬 闽菜大师的海鲜汤里,虾兵蟹将都在跳佛跳墙 🦐🙏',
  194. '💬 意大利面在真正意厨手里旋转的样子,比我前任还会绕 🍝💔',
  195. '💬 徽菜大师的火腿吊汤术,香得邻居以为我家在炼丹 🐷☁️',
  196. '💬 泰式大师的冬阴功里,柠檬草、香茅、南姜正在开演唱会 🎤🌿'
  197. ]
  198. const currentFoodComment = ref(foodComments[0])
  199. const randomFoodComment = computed(() => currentFoodComment.value)
  200. onMounted(() => {
  201. const commentInterval = setInterval(() => {
  202. currentFoodComment.value = foodComments[Math.floor(Math.random() * foodComments.length)]
  203. }, 3000)
  204. onUnmounted(() => clearInterval(commentInterval))
  205. })
  206. // 所有菜品数据
  207. const allDishes = ref<string[]>([])
  208. // 初始化
  209. onMounted(() => {
  210. allDishes.value = ingredientCategories.flatMap(category => category.items)
  211. randomDice.value = diceEmojis[Math.floor(Math.random() * diceEmojis.length)]
  212. })
  213. // 开始随机选择
  214. const startRandomSelection = async () => {
  215. isSelecting.value = true
  216. selectedDishes.value = []
  217. selectedMaster.value = null
  218. recipe.value = null
  219. selectionProgress.value = 0
  220. // 第一阶段:选择菜品
  221. selectionStatus.value = '正在随机选择菜品...'
  222. await selectRandomDishes()
  223. // 第二阶段:选择大师
  224. selectionStatus.value = '正在匹配主厨大师...'
  225. await selectRandomMaster()
  226. // 完成
  227. selectionStatus.value = '选择完成!'
  228. selectionProgress.value = 100
  229. setTimeout(() => {
  230. isSelecting.value = false
  231. }, 1000)
  232. }
  233. // 随机选择菜品
  234. const selectRandomDishes = async () => {
  235. const dishCount = 4 // 固定生成4个菜品
  236. let filteredDishes = [...allDishes.value]
  237. // 根据偏好过滤菜品
  238. if (preference.value) {
  239. const meatCategories = ['meat', 'seafood']
  240. const vegCategories = ['vegetables', 'mushrooms', 'beans']
  241. if (preference.value === 'meat-heavy') {
  242. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => meatCategories.includes(cat.id) && cat.items.includes(dish)))
  243. } else if (preference.value === 'veg-heavy') {
  244. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => vegCategories.includes(cat.id) && cat.items.includes(dish)))
  245. } else if (preference.value === 'meat-only') {
  246. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => meatCategories.includes(cat.id) && cat.items.includes(dish)))
  247. } else if (preference.value === 'veg-only') {
  248. filteredDishes = filteredDishes.filter(dish => ingredientCategories.some(cat => vegCategories.includes(cat.id) && cat.items.includes(dish)))
  249. }
  250. }
  251. const shuffledDishes = [...filteredDishes].sort(() => 0.5 - Math.random())
  252. const uniqueDishes = new Set<string>()
  253. // 确保获取4个不同的菜品
  254. while (uniqueDishes.size < dishCount && shuffledDishes.length > 0) {
  255. const dish = shuffledDishes.pop()
  256. if (dish) uniqueDishes.add(dish)
  257. }
  258. // 模拟选择过程
  259. for (let i = 0; i < 5; i++) {
  260. const randomDish = [...uniqueDishes][Math.floor(Math.random() * uniqueDishes.size)]
  261. currentSelection.value = {
  262. type: 'dish',
  263. name: randomDish
  264. }
  265. selectionProgress.value = (i / 5) * 50
  266. await new Promise(resolve => setTimeout(resolve, 100))
  267. }
  268. // 确定选择
  269. selectedDishes.value = [...uniqueDishes]
  270. currentSelection.value = {
  271. type: 'dish',
  272. name: selectedDishes.value[0]
  273. }
  274. await new Promise(resolve => setTimeout(resolve, 300))
  275. }
  276. // 随机选择大师
  277. const selectRandomMaster = async () => {
  278. // 模拟选择过程
  279. for (let i = 0; i < 10; i++) {
  280. const randomMaster = cuisines[Math.floor(Math.random() * cuisines.length)]
  281. currentSelection.value = {
  282. type: 'master',
  283. name: randomMaster.name,
  284. avatar: randomMaster.avatar,
  285. specialty: randomMaster.specialty
  286. }
  287. selectionProgress.value = 50 + (i / 10) * 50
  288. await new Promise(resolve => setTimeout(resolve, 80))
  289. }
  290. // 确定选择
  291. const finalMaster = cuisines[Math.floor(Math.random() * cuisines.length)]
  292. selectedMaster.value = finalMaster
  293. currentSelection.value = {
  294. type: 'master',
  295. name: finalMaster.name,
  296. avatar: finalMaster.avatar,
  297. specialty: finalMaster.specialty
  298. }
  299. await new Promise(resolve => setTimeout(resolve, 500))
  300. }
  301. // 生成菜谱
  302. const generateRecipeFromSelection = async () => {
  303. if (!selectedMaster.value || selectedDishes.value.length === 0 || isGenerating.value) return
  304. isGenerating.value = true
  305. // 文字轮播
  306. let textIndex = 0
  307. const textInterval = setInterval(() => {
  308. generatingText.value = generatingTexts[textIndex]
  309. textIndex = (textIndex + 1) % generatingTexts.length
  310. }, 1000)
  311. try {
  312. // 调用AI服务生成真实菜谱
  313. const cuisineInfo = {
  314. id: selectedMaster.value.id,
  315. name: selectedMaster.value.name,
  316. description: selectedMaster.value.description || '',
  317. avatar: selectedMaster.value.avatar,
  318. specialty: selectedMaster.value.specialty,
  319. prompt: selectedMaster.value.specialty
  320. }
  321. recipe.value = await generateRecipe(selectedDishes.value, cuisineInfo)
  322. // 显示成功提示
  323. showToast('菜谱生成成功', 'success')
  324. } catch (error) {
  325. console.error('生成菜谱失败:', error)
  326. showToast('生成菜谱失败,请重试', 'error')
  327. } finally {
  328. clearInterval(textInterval)
  329. isGenerating.value = false
  330. }
  331. }
  332. // 简单的提示功能
  333. const showToast = (message: string, type: 'success' | 'error' | 'warning' | 'info') => {
  334. const toast = document.createElement('div')
  335. toast.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white text-sm font-medium z-50 transition-all duration-300 transform translate-x-full`
  336. const styles = {
  337. success: 'bg-green-500',
  338. error: 'bg-red-500',
  339. warning: 'bg-yellow-500',
  340. info: 'bg-blue-500'
  341. }
  342. toast.className += ` ${styles[type]}`
  343. toast.textContent = message
  344. document.body.appendChild(toast)
  345. setTimeout(() => {
  346. toast.style.transform = 'translateX(0)'
  347. }, 10)
  348. setTimeout(() => {
  349. toast.style.transform = 'translateX(full)'
  350. setTimeout(() => {
  351. document.body.removeChild(toast)
  352. }, 300)
  353. }, 2000)
  354. }
  355. // 重置选择
  356. const resetSelection = () => {
  357. selectedDishes.value = []
  358. selectedMaster.value = null
  359. recipe.value = null
  360. currentSelection.value = null
  361. selectionProgress.value = 0
  362. }
  363. </script>
  364. <style scoped>
  365. /* 基础动画 */
  366. @keyframes fadeIn {
  367. from {
  368. opacity: 0;
  369. transform: translateY(20px);
  370. }
  371. to {
  372. opacity: 1;
  373. transform: translateY(0);
  374. }
  375. }
  376. @keyframes pulse {
  377. 0%,
  378. 100% {
  379. transform: scale(1);
  380. }
  381. 50% {
  382. transform: scale(1.05);
  383. }
  384. }
  385. @keyframes spin {
  386. from {
  387. transform: rotate(0deg);
  388. }
  389. to {
  390. transform: rotate(360deg);
  391. }
  392. }
  393. /* 应用动画 */
  394. .animate-spin {
  395. animation: spin 1s linear infinite;
  396. }
  397. /* 按钮悬停效果 */
  398. button {
  399. transition: all 0.3s ease;
  400. }
  401. button:hover:not(:disabled) {
  402. transform: translateY(-2px);
  403. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
  404. }
  405. button:active:not(:disabled) {
  406. transform: translateY(0);
  407. background-color: transparent;
  408. }
  409. /* 偏好设置特殊样式 */
  410. div.text-sm.text-gray-500:hover {
  411. background-color: transparent;
  412. }
  413. /* 标签悬停效果 */
  414. .bg-green-200,
  415. .bg-purple-50 {
  416. transition: all 0.3s ease;
  417. }
  418. .bg-green-200:hover {
  419. transform: translateY(-1px);
  420. box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2);
  421. }
  422. /* 进度条动画 */
  423. .bg-gradient-to-r {
  424. transition: width 0.5s ease-out;
  425. }
  426. /* 当前选择项的脉冲效果 */
  427. .text-4xl {
  428. animation: pulse 2s ease-in-out infinite;
  429. }
  430. /* 响应式调整 */
  431. @media (max-width: 640px) {
  432. .text-4xl {
  433. font-size: 2rem;
  434. }
  435. .text-6xl {
  436. font-size: 3rem;
  437. }
  438. .px-8 {
  439. padding-left: 1.5rem;
  440. padding-right: 1.5rem;
  441. }
  442. }
  443. </style>