TableDesign.vue 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  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. <!-- 步骤1和2: 左右布局 -->
  7. <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
  8. <!-- 左侧: 步骤1 菜品配置 -->
  9. <div class="">
  10. <div class="bg-gradient-to-r from-orange-400 to-pink-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  11. <span class="font-bold">1. 菜品配置</span>
  12. </div>
  13. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 h-full">
  14. <!-- 生成模式选择 - 紧凑布局 -->
  15. <div class="mb-4">
  16. <h3 class="text-lg font-bold text-dark-800 mb-3 flex items-center gap-2">
  17. <span class="text-xl">🍽️</span>
  18. <span>选择生成模式</span>
  19. </h3>
  20. <div class="grid grid-cols-1 gap-3">
  21. <button
  22. @click="config.flexibleCount = false"
  23. :class="[
  24. 'px-4 py-3 rounded-lg font-bold border-2 border-black transition-all duration-200 text-left flex items-center gap-3',
  25. !config.flexibleCount ? 'bg-yellow-400 text-dark-800 shadow-lg' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  26. ]"
  27. >
  28. <span class="text-xl">🎯</span>
  29. <div>
  30. <div class="font-bold text-sm">固定数量模式</div>
  31. <div class="text-xs opacity-75">指定确切菜品数量</div>
  32. </div>
  33. </button>
  34. <button
  35. @click="config.flexibleCount = true"
  36. :class="[
  37. 'px-4 py-3 rounded-lg font-bold border-2 border-black transition-all duration-200 text-left flex items-center gap-3',
  38. config.flexibleCount ? 'bg-yellow-400 text-dark-800 shadow-lg' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  39. ]"
  40. >
  41. <span class="text-xl">✨</span>
  42. <div>
  43. <div class="font-bold text-sm">智能搭配模式</div>
  44. <div class="text-xs opacity-75">AI智能决定数量和搭配</div>
  45. </div>
  46. </button>
  47. </div>
  48. </div>
  49. <!-- 配置内容 - 紧凑布局 -->
  50. <div class="space-y-4">
  51. <!-- 固定数量模式配置 -->
  52. <div v-if="!config.flexibleCount">
  53. <!-- 数量选择 -->
  54. <div class="bg-gray-50 rounded-lg p-3 border-2 border-gray-200">
  55. <h5 class="text-sm font-bold text-gray-700 mb-2 flex items-center gap-1">🍽️ 菜品数量</h5>
  56. <div class="flex items-center gap-3 flex-wrap">
  57. <div class="flex gap-2">
  58. <button
  59. v-for="count in [2, 4, 6, 8]"
  60. :key="count"
  61. @click="config.dishCount = count"
  62. :class="[
  63. 'px-3 py-1 rounded-lg font-bold border-2 border-black transition-all duration-200 text-sm',
  64. config.dishCount === count ? 'bg-yellow-400 text-dark-800' : 'bg-white text-gray-700 hover:bg-gray-100'
  65. ]"
  66. >
  67. {{ count }}道
  68. </button>
  69. </div>
  70. <div class="h-4 w-px bg-gray-300"></div>
  71. <div class="flex items-center gap-2">
  72. <span class="text-sm text-gray-600">自定义</span>
  73. <input
  74. v-model.number="config.dishCount"
  75. @input="validateDishCount"
  76. type="number"
  77. min="1"
  78. max="20"
  79. class="w-14 px-2 py-1 text-center border-2 border-black rounded-lg font-bold text-sm focus:outline-none focus:ring-2 focus:ring-pink-400"
  80. />
  81. <span class="text-sm text-gray-600">道</span>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 可选菜品 -->
  86. <div class="bg-gray-50 rounded-lg p-3 border-2 border-gray-200">
  87. <h5 class="text-sm font-bold text-gray-700 mb-2 flex items-center gap-1">🥘 指定菜品(可选)</h5>
  88. <div v-if="config.customDishes.length > 0" class="mb-3">
  89. <div class="flex flex-wrap gap-2">
  90. <div
  91. v-for="dish in config.customDishes"
  92. :key="dish"
  93. class="inline-flex items-center gap-1 bg-yellow-400 text-dark-800 px-2 py-1 rounded-full text-sm font-medium border-2 border-black"
  94. >
  95. {{ dish }}
  96. <button @click="removeCustomDish(dish)" class="hover:bg-yellow-500 rounded-full p-1 transition-colors">
  97. <span class="text-xs">✕</span>
  98. </button>
  99. </div>
  100. </div>
  101. </div>
  102. <div class="relative">
  103. <input
  104. v-model="currentCustomDish"
  105. @keyup.enter="addCustomDish"
  106. placeholder="输入菜品名称,按回车添加..."
  107. class="w-full p-2 border-2 border-black rounded-lg text-sm font-medium focus:outline-none focus:ring-2 focus:ring-pink-400"
  108. />
  109. <button
  110. @click="addCustomDish"
  111. :disabled="!currentCustomDish.trim() || config.customDishes.length >= 10"
  112. class="absolute right-2 top-1/2 transform -translate-y-1/2 px-2 py-1 bg-pink-400 hover:bg-pink-500 disabled:bg-gray-300 text-white rounded text-xs font-bold transition-colors disabled:cursor-not-allowed"
  113. >
  114. 添加
  115. </button>
  116. </div>
  117. <div class="flex justify-between items-center mt-1 text-xs text-gray-500">
  118. <span>💡 例如:红烧肉、清蒸鱼</span>
  119. <span>{{ config.customDishes.length }}/10</span>
  120. </div>
  121. </div>
  122. </div>
  123. <!-- 智能搭配模式配置 -->
  124. <div v-else>
  125. <div class="bg-gray-50 rounded-lg p-3 border-2 border-gray-200">
  126. <h5 class="text-sm font-bold text-gray-700 mb-2 flex items-center gap-1">🥘 输入想要的菜品</h5>
  127. <div v-if="config.customDishes.length === 0" class="mb-3 p-2 bg-orange-50 border-2 border-orange-200 rounded-lg">
  128. <p class="text-xs text-orange-700">
  129. <span class="font-medium">⚠️ 智能搭配模式需要您先输入至少一道菜品</span>
  130. </p>
  131. </div>
  132. <div v-if="config.customDishes.length > 0" class="mb-3">
  133. <div class="flex flex-wrap gap-2">
  134. <div
  135. v-for="dish in config.customDishes"
  136. :key="dish"
  137. class="inline-flex items-center gap-1 bg-green-400 text-dark-800 px-2 py-1 rounded-full text-sm font-medium border-2 border-black"
  138. >
  139. {{ dish }}
  140. <button @click="removeCustomDish(dish)" class="hover:bg-green-500 rounded-full p-1 transition-colors">
  141. <span class="text-xs">✕</span>
  142. </button>
  143. </div>
  144. </div>
  145. </div>
  146. <div class="relative">
  147. <input
  148. v-model="currentCustomDish"
  149. @keyup.enter="addCustomDish"
  150. placeholder="输入菜品名称,按回车添加..."
  151. class="w-full p-2 border-2 border-black rounded-lg text-sm font-medium focus:outline-none focus:ring-2 focus:ring-green-400"
  152. />
  153. <button
  154. @click="addCustomDish"
  155. :disabled="!currentCustomDish.trim() || config.customDishes.length >= 10"
  156. class="absolute right-2 top-1/2 transform -translate-y-1/2 px-2 py-1 bg-green-400 hover:bg-green-500 disabled:bg-gray-300 text-white rounded text-xs font-bold transition-colors disabled:cursor-not-allowed"
  157. >
  158. 添加
  159. </button>
  160. </div>
  161. <div class="flex justify-between items-center mt-1 text-xs text-gray-500">
  162. <span>💡 例如:包菜、娃娃菜、土豆</span>
  163. <span>{{ config.customDishes.length }}/10</span>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. </div>
  170. <!-- 右侧: 步骤2 偏好设置(可选) -->
  171. <div class="mt-10">
  172. <div class="bg-gradient-to-r from-green-400 to-blue-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  173. <span class="font-bold">2. 偏好设置(可选)</span>
  174. </div>
  175. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 h-full">
  176. <!-- 提示信息 -->
  177. <div class="mb-4 p-3 bg-blue-50 border-2 border-blue-200 rounded-lg">
  178. <p class="text-sm text-blue-700">
  179. <span class="font-medium">💡 可选配置:</span>
  180. 以下设置为可选项,不设置也能生成精彩菜单。
  181. </p>
  182. </div>
  183. <!-- 可折叠的配置选项 -->
  184. <div class="space-y-4">
  185. <!-- 口味和风格设置 -->
  186. <div class="border-2 border-gray-200 rounded-lg">
  187. <button
  188. @click="showTasteSettings = !showTasteSettings"
  189. class="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 rounded-lg font-medium text-left flex items-center justify-between transition-colors"
  190. >
  191. <div class="flex items-center gap-2">
  192. <span class="text-lg">🍽️</span>
  193. <span class="font-bold text-gray-800 text-sm">口味和风格设置</span>
  194. </div>
  195. <span class="text-gray-500 transform transition-transform" :class="{ 'rotate-180': showTasteSettings }">▼</span>
  196. </button>
  197. <Transition name="collapse">
  198. <div v-show="showTasteSettings" class="p-4 border-t-2 border-gray-200 space-y-6">
  199. <!-- 口味偏好 -->
  200. <div>
  201. <h5 class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-1">👅 口味偏好</h5>
  202. <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
  203. <button
  204. v-for="taste in tasteOptions"
  205. :key="taste.id"
  206. @click="toggleTaste(taste.id)"
  207. :class="[
  208. 'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 flex items-center justify-center gap-1',
  209. config.tastes.includes(taste.id) ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  210. ]"
  211. >
  212. <span>{{ taste.icon }}</span>
  213. <span>{{ taste.name }}</span>
  214. </button>
  215. </div>
  216. </div>
  217. <!-- 菜系风格 -->
  218. <div>
  219. <h5 class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-1">🌍 菜系风格</h5>
  220. <div class="grid grid-cols-2 md:grid-cols-4 gap-2">
  221. <button
  222. v-for="style in cuisineStyles"
  223. :key="style.id"
  224. @click="config.cuisineStyle = style.id"
  225. :class="[
  226. 'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 flex items-center justify-center gap-1',
  227. config.cuisineStyle === style.id ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  228. ]"
  229. >
  230. <span>{{ style.icon }}</span>
  231. <span>{{ style.name }}</span>
  232. </button>
  233. </div>
  234. </div>
  235. <!-- 用餐场景 -->
  236. <div>
  237. <h5 class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-1">🎭 用餐场景</h5>
  238. <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
  239. <button
  240. v-for="scene in diningScenes"
  241. :key="scene.id"
  242. @click="config.diningScene = scene.id"
  243. :class="[
  244. 'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 flex items-center justify-center gap-1',
  245. config.diningScene === scene.id ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  246. ]"
  247. >
  248. <span>{{ scene.icon }}</span>
  249. <span>{{ scene.name }}</span>
  250. </button>
  251. </div>
  252. </div>
  253. </div>
  254. </Transition>
  255. </div>
  256. <!-- 营养和特殊要求设置 -->
  257. <div class="border-2 border-gray-200 rounded-lg">
  258. <button
  259. @click="showNutritionSettings = !showNutritionSettings"
  260. class="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 rounded-lg font-medium text-left flex items-center justify-between transition-colors"
  261. >
  262. <div class="flex items-center gap-2">
  263. <span class="text-lg">⚖️</span>
  264. <span class="font-bold text-gray-800 text-sm">营养和特殊要求</span>
  265. </div>
  266. <span class="text-gray-500 transform transition-transform" :class="{ 'rotate-180': showNutritionSettings }">▼</span>
  267. </button>
  268. <Transition name="collapse">
  269. <div v-show="showNutritionSettings" class="p-4 border-t-2 border-gray-200 space-y-6">
  270. <!-- 营养搭配 -->
  271. <div>
  272. <h5 class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-1">⚖️ 营养搭配</h5>
  273. <div class="grid grid-cols-2 md:grid-cols-5 gap-2">
  274. <button
  275. v-for="nutrition in nutritionOptions"
  276. :key="nutrition.id"
  277. @click="config.nutritionFocus = nutrition.id"
  278. :class="[
  279. 'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 flex items-center justify-center gap-1',
  280. config.nutritionFocus === nutrition.id ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
  281. ]"
  282. >
  283. <span>{{ nutrition.icon }}</span>
  284. <span>{{ nutrition.name }}</span>
  285. </button>
  286. </div>
  287. </div>
  288. <!-- 特殊要求 -->
  289. <div>
  290. <h5 class="text-sm font-bold text-gray-700 mb-3 flex items-center gap-1">💭 特殊要求</h5>
  291. <textarea
  292. v-model="config.customRequirement"
  293. placeholder="例如:不要太油腻,适合老人小孩,有一道汤..."
  294. class="w-full p-3 border-2 border-black rounded-lg text-sm resize-none focus:outline-none focus:ring-2 focus:ring-pink-400"
  295. rows="3"
  296. maxlength="200"
  297. ></textarea>
  298. <div class="text-xs text-gray-500 mt-1 text-right">{{ config.customRequirement.length }}/200</div>
  299. </div>
  300. </div>
  301. </Transition>
  302. </div>
  303. </div>
  304. <!-- 当前配置预览(简化版) -->
  305. <div class="bg-gray-50 rounded-lg p-3 mt-6">
  306. <h6 class="font-bold text-sm text-gray-700 mb-2 flex items-center gap-2">
  307. <span>📋</span>
  308. <span>当前配置</span>
  309. </h6>
  310. <div class="text-xs text-gray-600 space-y-1">
  311. <div>生成模式:{{ config.flexibleCount ? '✨ 智能搭配' : '🎯 固定数量' }}</div>
  312. <div v-if="!config.flexibleCount">菜品数量:{{ config.dishCount }}道菜</div>
  313. <div v-if="config.customDishes.length > 0">{{ config.flexibleCount ? '输入菜品' : '指定菜品' }}:{{ config.customDishes.join('、') }}</div>
  314. <div v-if="config.tastes.length > 0">口味:{{ config.tastes.map(t => tasteOptions.find(opt => opt.id === t)?.name).join('、') }}</div>
  315. <div v-if="config.cuisineStyle !== 'mixed'">风格:{{ cuisineStyles.find(s => s.id === config.cuisineStyle)?.name }}</div>
  316. <div v-if="config.diningScene !== 'family'">场景:{{ diningScenes.find(s => s.id === config.diningScene)?.name }}</div>
  317. <div v-if="config.nutritionFocus !== 'balanced'">营养:{{ nutritionOptions.find(n => n.id === config.nutritionFocus)?.name }}</div>
  318. <div v-if="config.customRequirement">特殊要求:{{ config.customRequirement }}</div>
  319. </div>
  320. </div>
  321. </div>
  322. </div>
  323. </div>
  324. <!-- 步骤2: 生成一桌菜 -->
  325. <div class="mb-6 mt-16">
  326. <div class="bg-dark-800 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
  327. <span class="font-bold">3. 生成一桌菜</span>
  328. </div>
  329. <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 md:p-6">
  330. <!-- 生成按钮区域 -->
  331. <div v-if="!isGenerating && generatedDishes.length === 0" class="text-center">
  332. <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">
  333. <span class="text-white text-2xl">👨‍🍳</span>
  334. </div>
  335. <h2 class="text-xl font-bold text-dark-800 mb-2">准备生成一桌菜</h2>
  336. <p class="text-gray-600 mb-6 text-sm">AI大师已准备就绪,点击按钮开始设计您的专属菜单</p>
  337. <div class="space-y-3">
  338. <button
  339. @click="generateTableMenuAction"
  340. :disabled="isGenerating || (config.flexibleCount && config.customDishes.length === 0)"
  341. class="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 disabled:from-gray-400 disabled:to-gray-400 text-white px-6 py-3 rounded-lg font-bold text-base md:text-lg border-2 border-black transition-all duration-300 transform disabled:scale-100 disabled:cursor-not-allowed shadow-lg"
  342. >
  343. <span class="flex items-center gap-2 justify-center">
  344. <span class="text-xl">✨</span>
  345. <span>交给大师</span>
  346. </span>
  347. </button>
  348. <!-- 智能搭配模式提示 -->
  349. <div v-if="config.flexibleCount && config.customDishes.length === 0" class="mt-3 p-3 bg-orange-50 border-2 border-orange-200 rounded-lg">
  350. <p class="text-sm text-orange-700 text-center">
  351. <span class="font-medium">⚠️ 请先在步骤1中输入至少一道菜品</span>
  352. </p>
  353. </div>
  354. </div>
  355. </div>
  356. <!-- 生成中状态 -->
  357. <div v-if="isGenerating" class="text-center py-8">
  358. <div class="animate-spin w-16 h-16 border-4 border-orange-500 border-t-transparent rounded-full mx-auto mb-4"></div>
  359. <h3 class="text-xl font-bold text-gray-800 mb-2">{{ generatingText }}</h3>
  360. <p class="text-gray-600">AI大师正在为您精心搭配...</p>
  361. </div>
  362. <!-- 生成结果 -->
  363. <div v-if="!isGenerating && generatedDishes.length > 0">
  364. <div class="flex justify-between items-center mb-6">
  365. <h3 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
  366. <span>🎉</span>
  367. <span>您的专属一桌菜</span>
  368. </h3>
  369. <button
  370. @click="resetConfig"
  371. class="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg font-bold border-2 border-black transition-all duration-200 text-sm"
  372. >
  373. 🔄 重新生成
  374. </button>
  375. </div>
  376. <!-- 菜品列表 -->
  377. <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
  378. <div v-for="(dish, index) in generatedDishes" :key="index" class="border-2 border-black rounded-lg p-4 bg-white hover:bg-yellow-50 transition-colors">
  379. <div class="flex justify-between items-start mb-2">
  380. <h4 class="font-bold text-gray-800 text-lg">{{ dish.name }}</h4>
  381. <span class="text-xs bg-pink-400 text-white px-2 py-1 rounded-full font-medium">{{ dish.category }}</span>
  382. </div>
  383. <p class="text-gray-600 text-sm mb-3 line-clamp-2">{{ dish.description }}</p>
  384. <div class="flex justify-between items-center">
  385. <div class="flex gap-1 flex-wrap">
  386. <span v-for="tag in dish.tags" :key="tag" class="text-xs bg-yellow-200 text-yellow-800 px-2 py-1 rounded font-medium">
  387. {{ tag }}
  388. </span>
  389. </div>
  390. <button
  391. @click="generateDishRecipeAction(dish, index)"
  392. :disabled="dish.isGeneratingRecipe"
  393. :class="[
  394. 'px-3 py-2 text-white rounded-lg text-sm font-bold border-2 border-black transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
  395. dish.recipe
  396. ? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600'
  397. : 'bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600'
  398. ]"
  399. >
  400. <span v-if="dish.isGeneratingRecipe" class="flex items-center gap-1">
  401. <div class="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
  402. <span>生成中</span>
  403. </span>
  404. <span v-else-if="dish.recipe">📖 查看菜谱</span>
  405. <span v-else>📝 生成菜谱</span>
  406. </button>
  407. </div>
  408. </div>
  409. </div>
  410. </div>
  411. </div>
  412. </div>
  413. </div>
  414. <!-- 底部 -->
  415. <GlobalFooter />
  416. </div>
  417. <!-- 菜谱弹窗 -->
  418. <Teleport to="body">
  419. <div v-if="selectedRecipe" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4 modal-overlay" @click="closeRecipeModal">
  420. <div
  421. class="bg-white rounded-2xl border-2 border-black shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden modal-content transform transition-all duration-300"
  422. @click.stop
  423. >
  424. <!-- 弹窗头部 -->
  425. <div class="bg-gradient-to-r from-orange-500 to-red-500 text-white px-6 py-4 flex justify-between items-center">
  426. <div class="flex items-center gap-3">
  427. <span class="text-2xl">📖</span>
  428. <h3 class="text-xl font-bold">{{ selectedRecipe.name }}</h3>
  429. </div>
  430. <button
  431. @click="closeRecipeModal"
  432. class="w-8 h-8 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110"
  433. >
  434. <span class="text-white text-lg font-bold">✕</span>
  435. </button>
  436. </div>
  437. <!-- 弹窗内容 -->
  438. <div class="max-h-[calc(90vh-80px)] overflow-y-auto scrollbar-hide">
  439. <div class="">
  440. <RecipeCard :recipe="selectedRecipe" />
  441. </div>
  442. </div>
  443. </div>
  444. </div>
  445. </Teleport>
  446. </template>
  447. <script setup lang="ts">
  448. import { ref, reactive, Teleport, Transition, onMounted, onUnmounted } from 'vue'
  449. import type { Recipe } from '@/types'
  450. import RecipeCard from '@/components/RecipeCard.vue'
  451. import GlobalNavigation from '@/components/GlobalNavigation.vue'
  452. import GlobalFooter from '@/components/GlobalFooter.vue'
  453. import { generateTableMenu, generateDishRecipe, testAIConnection } from '@/services/aiService'
  454. // 配置选项
  455. interface TableConfig {
  456. dishCount: number
  457. flexibleCount: boolean
  458. tastes: string[]
  459. cuisineStyle: string
  460. diningScene: string
  461. nutritionFocus: string
  462. customRequirement: string
  463. customDishes: string[]
  464. }
  465. // 菜品信息
  466. interface DishInfo {
  467. name: string
  468. description: string
  469. category: string
  470. tags: string[]
  471. recipe?: Recipe
  472. isGeneratingRecipe?: boolean
  473. }
  474. // 状态管理
  475. const isGenerating = ref(false)
  476. const generatingText = ref('正在生成菜单...')
  477. const generatedDishes = ref<DishInfo[]>([])
  478. const selectedRecipe = ref<Recipe | null>(null)
  479. // 折叠状态管理
  480. const showTasteSettings = ref(false)
  481. const showNutritionSettings = ref(false)
  482. // 配置
  483. const config = reactive<TableConfig>({
  484. dishCount: 6,
  485. flexibleCount: true, // 默认开启智能搭配模式
  486. tastes: [],
  487. cuisineStyle: 'mixed',
  488. diningScene: 'family',
  489. nutritionFocus: 'balanced',
  490. customRequirement: '',
  491. customDishes: []
  492. })
  493. // 自定义菜品输入
  494. const currentCustomDish = ref('')
  495. // 配置选项数据
  496. const tasteOptions = [
  497. { id: 'spicy', name: '麻辣', icon: '🌶️' },
  498. { id: 'sweet', name: '甜味', icon: '🍯' },
  499. { id: 'sour', name: '酸味', icon: '🍋' },
  500. { id: 'salty', name: '咸鲜', icon: '🧂' },
  501. { id: 'light', name: '清淡', icon: '🌿' },
  502. { id: 'rich', name: '浓郁', icon: '🍖' }
  503. ]
  504. const cuisineStyles = [
  505. { id: 'mixed', name: '混合菜系', icon: '🌍' },
  506. { id: 'chinese', name: '中式为主', icon: '🥢' },
  507. { id: 'western', name: '西式为主', icon: '🍽️' },
  508. { id: 'japanese', name: '日式为主', icon: '🍱' }
  509. ]
  510. const diningScenes = [
  511. { id: 'family', name: '家庭聚餐', icon: '👨‍👩‍👧‍👦' },
  512. { id: 'friends', name: '朋友聚会', icon: '🎉' },
  513. { id: 'romantic', name: '浪漫晚餐', icon: '💕' },
  514. { id: 'business', name: '商务宴请', icon: '💼' },
  515. { id: 'festival', name: '节日庆祝', icon: '🎊' },
  516. { id: 'casual', name: '日常用餐', icon: '🍚' }
  517. ]
  518. const nutritionOptions = [
  519. { id: 'balanced', name: '营养均衡', icon: '⚖️' },
  520. { id: 'protein', name: '高蛋白', icon: '💪' },
  521. { id: 'vegetarian', name: '素食为主', icon: '🥬' },
  522. { id: 'low_fat', name: '低脂健康', icon: '🏃‍♀️' },
  523. { id: 'comfort', name: '滋补养生', icon: '🍲' }
  524. ]
  525. // 切换口味选择
  526. const toggleTaste = (tasteId: string) => {
  527. const index = config.tastes.indexOf(tasteId)
  528. if (index > -1) {
  529. config.tastes.splice(index, 1)
  530. } else {
  531. config.tastes.push(tasteId)
  532. }
  533. }
  534. // 增加菜品数量
  535. const increaseDishCount = () => {
  536. if (config.dishCount < 20) {
  537. config.dishCount++
  538. }
  539. }
  540. // 减少菜品数量
  541. const decreaseDishCount = () => {
  542. if (config.dishCount > 1) {
  543. config.dishCount--
  544. }
  545. }
  546. // 验证菜品数量输入
  547. const validateDishCount = (event: Event) => {
  548. const target = event.target as HTMLInputElement
  549. let value = parseInt(target.value)
  550. if (isNaN(value) || value < 1) {
  551. config.dishCount = 1
  552. } else if (value > 20) {
  553. config.dishCount = 20
  554. } else {
  555. config.dishCount = value
  556. }
  557. }
  558. // 添加自定义菜品
  559. const addCustomDish = () => {
  560. const dish = currentCustomDish.value.trim()
  561. if (dish && !config.customDishes.includes(dish) && config.customDishes.length < 10) {
  562. config.customDishes.push(dish)
  563. currentCustomDish.value = ''
  564. }
  565. }
  566. // 移除自定义菜品
  567. const removeCustomDish = (dish: string) => {
  568. const index = config.customDishes.indexOf(dish)
  569. if (index > -1) {
  570. config.customDishes.splice(index, 1)
  571. }
  572. }
  573. // 测试AI连接
  574. const testConnection = async () => {
  575. try {
  576. const isConnected = await testAIConnection()
  577. if (isConnected) {
  578. alert('AI连接测试成功!')
  579. } else {
  580. alert('AI连接测试失败,请检查配置')
  581. }
  582. } catch (error) {
  583. alert('AI连接测试失败:' + error)
  584. }
  585. }
  586. // 生成一桌菜
  587. const generateTableMenuAction = async () => {
  588. isGenerating.value = true
  589. generatingText.value = '正在生成菜单...'
  590. try {
  591. // 调用AI服务生成菜单
  592. const aiDishes = await generateTableMenu(config)
  593. // 转换为本地格式
  594. const dishes: DishInfo[] = aiDishes.map(dish => ({
  595. name: dish.name,
  596. description: dish.description,
  597. category: dish.category,
  598. tags: dish.tags,
  599. isGeneratingRecipe: false
  600. }))
  601. generatedDishes.value = dishes
  602. } catch (error) {
  603. console.error('生成菜单失败:', error)
  604. // 显示错误提示
  605. alert('AI生成菜单失败,请检查网络连接或稍后重试')
  606. } finally {
  607. isGenerating.value = false
  608. }
  609. }
  610. // 阻止背景滚动
  611. const disableBodyScroll = () => {
  612. document.body.style.overflow = 'hidden'
  613. }
  614. // 恢复背景滚动
  615. const enableBodyScroll = () => {
  616. document.body.style.overflow = ''
  617. }
  618. // 生成单个菜品的菜谱
  619. const generateDishRecipeAction = async (dish: DishInfo, index: number) => {
  620. if (dish.recipe) {
  621. selectedRecipe.value = dish.recipe
  622. disableBodyScroll()
  623. return
  624. }
  625. dish.isGeneratingRecipe = true
  626. try {
  627. // 调用AI服务生成菜谱
  628. const recipe = await generateDishRecipe(dish.name, dish.description, dish.category)
  629. dish.recipe = recipe
  630. // 移除自动弹出,让用户手动点击查看
  631. // selectedRecipe.value = recipe
  632. // disableBodyScroll()
  633. } catch (error) {
  634. console.error('生成菜谱失败:', error)
  635. // 显示错误提示
  636. alert(`生成${dish.name}菜谱失败,请稍后重试`)
  637. } finally {
  638. dish.isGeneratingRecipe = false
  639. }
  640. }
  641. // 关闭菜谱弹窗
  642. const closeRecipeModal = () => {
  643. selectedRecipe.value = null
  644. enableBodyScroll()
  645. }
  646. // 测试弹窗功能
  647. const testModal = () => {
  648. // 创建一个测试菜谱
  649. const testRecipe: Recipe = {
  650. id: 'test-recipe',
  651. name: '测试菜谱 - 红烧肉',
  652. cuisine: '中式',
  653. ingredients: ['五花肉 500g', '生抽 2勺', '老抽 1勺', '冰糖 30g', '料酒 1勺', '葱段 适量', '姜片 适量'],
  654. steps: [
  655. { step: 1, description: '五花肉切块,冷水下锅焯水去腥', time: 5 },
  656. { step: 2, description: '热锅下油,放入冰糖炒糖色', time: 3 },
  657. { step: 3, description: '下入肉块翻炒上色', time: 5 },
  658. { step: 4, description: '加入生抽、老抽、料酒调色调味', time: 2 },
  659. { step: 5, description: '加入开水没过肉块,大火烧开转小火炖煮', time: 45 }
  660. ],
  661. cookingTime: 60,
  662. difficulty: 'medium',
  663. tips: ['糖色要炒到微微冒烟', '炖煮时要小火慢炖', '最后大火收汁']
  664. }
  665. selectedRecipe.value = testRecipe
  666. }
  667. // 键盘事件处理
  668. const handleKeydown = (event: KeyboardEvent) => {
  669. if (event.key === 'Escape' && selectedRecipe.value) {
  670. closeRecipeModal()
  671. }
  672. }
  673. // 组件挂载时添加键盘事件监听
  674. onMounted(() => {
  675. document.addEventListener('keydown', handleKeydown)
  676. })
  677. // 组件卸载时移除键盘事件监听
  678. onUnmounted(() => {
  679. document.removeEventListener('keydown', handleKeydown)
  680. enableBodyScroll() // 确保组件销毁时恢复滚动
  681. })
  682. // 重置配置
  683. const resetConfig = () => {
  684. // 只清除生成的结果,保留用户的配置选择
  685. generatedDishes.value = []
  686. selectedRecipe.value = null
  687. // 不重置用户的配置选择,让用户可以基于当前配置重新生成
  688. }
  689. </script>
  690. <style scoped>
  691. .line-clamp-2 {
  692. display: -webkit-box;
  693. -webkit-line-clamp: 2;
  694. -webkit-box-orient: vertical;
  695. overflow: hidden;
  696. }
  697. @keyframes spin {
  698. from {
  699. transform: rotate(0deg);
  700. }
  701. to {
  702. transform: rotate(360deg);
  703. }
  704. }
  705. @keyframes fadeIn {
  706. from {
  707. opacity: 0;
  708. }
  709. to {
  710. opacity: 1;
  711. }
  712. }
  713. @keyframes slideUp {
  714. from {
  715. opacity: 0;
  716. transform: translateY(20px) scale(0.95);
  717. }
  718. to {
  719. opacity: 1;
  720. transform: translateY(0) scale(1);
  721. }
  722. }
  723. .animate-spin {
  724. animation: spin 1s linear infinite;
  725. }
  726. /* 弹窗动画 */
  727. .modal-overlay {
  728. animation: fadeIn 0.3s ease-out;
  729. }
  730. .modal-content {
  731. animation: slideUp 0.3s ease-out;
  732. }
  733. @keyframes fadeIn {
  734. from {
  735. opacity: 0;
  736. }
  737. to {
  738. opacity: 1;
  739. }
  740. }
  741. @keyframes slideUp {
  742. from {
  743. opacity: 0;
  744. transform: translateY(20px) scale(0.95);
  745. }
  746. to {
  747. opacity: 1;
  748. transform: translateY(0) scale(1);
  749. }
  750. }
  751. /* 确保弹窗在最顶层 */
  752. .modal-overlay {
  753. backdrop-filter: blur(4px);
  754. }
  755. /* 隐藏滚动条但保持滚动功能 */
  756. .scrollbar-hide {
  757. -ms-overflow-style: none; /* IE and Edge */
  758. scrollbar-width: none; /* Firefox */
  759. }
  760. .scrollbar-hide::-webkit-scrollbar {
  761. display: none; /* Chrome, Safari and Opera */
  762. }
  763. /* 折叠动画 */
  764. .collapse-enter-active,
  765. .collapse-leave-active {
  766. transition: all 0.3s ease;
  767. overflow: hidden;
  768. }
  769. .collapse-enter-from,
  770. .collapse-leave-to {
  771. max-height: 0;
  772. opacity: 0;
  773. }
  774. .collapse-enter-to,
  775. .collapse-leave-from {
  776. max-height: 500px;
  777. opacity: 1;
  778. }
  779. </style>