Home.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. <template>
  2. <div class="home-container">
  3. <!-- 背景装饰 -->
  4. <div class="bg-decoration">
  5. <div class="circle circle-1"></div>
  6. <div class="circle circle-2"></div>
  7. <div class="circle circle-3"></div>
  8. </div>
  9. <!-- 页面标题 -->
  10. <div class="page-header">
  11. <div class="icon-wrapper">
  12. <span class="icon">✈️</span>
  13. </div>
  14. <h1 class="page-title">智能旅行助手</h1>
  15. <p class="page-subtitle">基于AI的个性化旅行规划,让每一次出行都完美无忧</p>
  16. </div>
  17. <a-card class="form-card" :bordered="false">
  18. <a-form
  19. :model="formData"
  20. layout="vertical"
  21. @finish="handleSubmit"
  22. >
  23. <!-- 第一步:目的地和日期 -->
  24. <div class="form-section">
  25. <div class="section-header">
  26. <span class="section-icon">📍</span>
  27. <span class="section-title">目的地与日期</span>
  28. </div>
  29. <a-row :gutter="24">
  30. <a-col :span="8">
  31. <a-form-item name="city" :rules="[{ required: true, message: '请输入目的地城市' }]">
  32. <template #label>
  33. <span class="form-label">目的地城市</span>
  34. </template>
  35. <a-input
  36. v-model:value="formData.city"
  37. placeholder="例如: 北京"
  38. size="large"
  39. class="custom-input"
  40. >
  41. <template #prefix>
  42. <span style="color: #1890ff;">🏙️</span>
  43. </template>
  44. </a-input>
  45. </a-form-item>
  46. </a-col>
  47. <a-col :span="6">
  48. <a-form-item name="start_date" :rules="[{ required: true, message: '请选择开始日期' }]">
  49. <template #label>
  50. <span class="form-label">开始日期</span>
  51. </template>
  52. <a-date-picker
  53. v-model:value="formData.start_date"
  54. style="width: 100%"
  55. size="large"
  56. class="custom-input"
  57. placeholder="选择日期"
  58. />
  59. </a-form-item>
  60. </a-col>
  61. <a-col :span="6">
  62. <a-form-item name="end_date" :rules="[{ required: true, message: '请选择结束日期' }]">
  63. <template #label>
  64. <span class="form-label">结束日期</span>
  65. </template>
  66. <a-date-picker
  67. v-model:value="formData.end_date"
  68. style="width: 100%"
  69. size="large"
  70. class="custom-input"
  71. placeholder="选择日期"
  72. />
  73. </a-form-item>
  74. </a-col>
  75. <a-col :span="4">
  76. <a-form-item>
  77. <template #label>
  78. <span class="form-label">旅行天数</span>
  79. </template>
  80. <div class="days-display-compact">
  81. <span class="days-value">{{ formData.travel_days }}</span>
  82. <span class="days-unit">天</span>
  83. </div>
  84. </a-form-item>
  85. </a-col>
  86. </a-row>
  87. </div>
  88. <!-- 第二步:偏好设置 -->
  89. <div class="form-section">
  90. <div class="section-header">
  91. <span class="section-icon">⚙️</span>
  92. <span class="section-title">偏好设置</span>
  93. </div>
  94. <a-row :gutter="24">
  95. <a-col :span="8">
  96. <a-form-item name="transportation">
  97. <template #label>
  98. <span class="form-label">交通方式</span>
  99. </template>
  100. <a-select v-model:value="formData.transportation" size="large" class="custom-select">
  101. <a-select-option value="公共交通">🚇 公共交通</a-select-option>
  102. <a-select-option value="自驾">🚗 自驾</a-select-option>
  103. <a-select-option value="步行">🚶 步行</a-select-option>
  104. <a-select-option value="混合">🔀 混合</a-select-option>
  105. </a-select>
  106. </a-form-item>
  107. </a-col>
  108. <a-col :span="8">
  109. <a-form-item name="accommodation">
  110. <template #label>
  111. <span class="form-label">住宿偏好</span>
  112. </template>
  113. <a-select v-model:value="formData.accommodation" size="large" class="custom-select">
  114. <a-select-option value="经济型酒店">💰 经济型酒店</a-select-option>
  115. <a-select-option value="舒适型酒店">🏨 舒适型酒店</a-select-option>
  116. <a-select-option value="豪华酒店">⭐ 豪华酒店</a-select-option>
  117. <a-select-option value="民宿">🏡 民宿</a-select-option>
  118. </a-select>
  119. </a-form-item>
  120. </a-col>
  121. <a-col :span="8">
  122. <a-form-item name="preferences">
  123. <template #label>
  124. <span class="form-label">旅行偏好</span>
  125. </template>
  126. <div class="preference-tags">
  127. <a-checkbox-group v-model:value="formData.preferences" class="custom-checkbox-group">
  128. <a-checkbox value="历史文化" class="preference-tag">🏛️ 历史文化</a-checkbox>
  129. <a-checkbox value="自然风光" class="preference-tag">🏞️ 自然风光</a-checkbox>
  130. <a-checkbox value="美食" class="preference-tag">🍜 美食</a-checkbox>
  131. <a-checkbox value="购物" class="preference-tag">🛍️ 购物</a-checkbox>
  132. <a-checkbox value="艺术" class="preference-tag">🎨 艺术</a-checkbox>
  133. <a-checkbox value="休闲" class="preference-tag">☕ 休闲</a-checkbox>
  134. </a-checkbox-group>
  135. </div>
  136. </a-form-item>
  137. </a-col>
  138. </a-row>
  139. </div>
  140. <!-- 第三步:额外要求 -->
  141. <div class="form-section">
  142. <div class="section-header">
  143. <span class="section-icon">💬</span>
  144. <span class="section-title">额外要求</span>
  145. </div>
  146. <a-form-item name="free_text_input">
  147. <a-textarea
  148. v-model:value="formData.free_text_input"
  149. placeholder="请输入您的额外要求,例如:想去看升旗、需要无障碍设施、对海鲜过敏等..."
  150. :rows="3"
  151. size="large"
  152. class="custom-textarea"
  153. />
  154. </a-form-item>
  155. </div>
  156. <!-- 提交按钮 -->
  157. <a-form-item>
  158. <a-button
  159. type="primary"
  160. html-type="submit"
  161. :loading="loading"
  162. size="large"
  163. block
  164. class="submit-button"
  165. >
  166. <template v-if="!loading">
  167. <span class="button-icon">🚀</span>
  168. <span>开始规划我的旅行</span>
  169. </template>
  170. <template v-else>
  171. <span>正在生成中...</span>
  172. </template>
  173. </a-button>
  174. </a-form-item>
  175. <!-- 加载进度条 -->
  176. <a-form-item v-if="loading">
  177. <div class="loading-container">
  178. <a-progress
  179. :percent="loadingProgress"
  180. status="active"
  181. :stroke-color="{
  182. '0%': '#667eea',
  183. '100%': '#764ba2',
  184. }"
  185. :stroke-width="10"
  186. />
  187. <p class="loading-status">
  188. {{ loadingStatus }}
  189. </p>
  190. </div>
  191. </a-form-item>
  192. </a-form>
  193. </a-card>
  194. </div>
  195. </template>
  196. <script setup lang="ts">
  197. import { ref, reactive, watch } from 'vue'
  198. import { useRouter } from 'vue-router'
  199. import { message } from 'ant-design-vue'
  200. import { generateTripPlan } from '@/services/api'
  201. import type { TripFormData } from '@/types'
  202. import type { Dayjs } from 'dayjs'
  203. const router = useRouter()
  204. const loading = ref(false)
  205. const loadingProgress = ref(0)
  206. const loadingStatus = ref('')
  207. const formData = reactive<TripFormData & { start_date: Dayjs | null; end_date: Dayjs | null }>({
  208. city: '',
  209. start_date: null,
  210. end_date: null,
  211. travel_days: 1,
  212. transportation: '公共交通',
  213. accommodation: '经济型酒店',
  214. preferences: [],
  215. free_text_input: ''
  216. })
  217. // 监听日期变化,自动计算旅行天数
  218. watch([() => formData.start_date, () => formData.end_date], ([start, end]) => {
  219. if (start && end) {
  220. const days = end.diff(start, 'day') + 1
  221. if (days > 0 && days <= 30) {
  222. formData.travel_days = days
  223. } else if (days > 30) {
  224. message.warning('旅行天数不能超过30天')
  225. formData.end_date = null
  226. } else {
  227. message.warning('结束日期不能早于开始日期')
  228. formData.end_date = null
  229. }
  230. }
  231. })
  232. const handleSubmit = async () => {
  233. if (!formData.start_date || !formData.end_date) {
  234. message.error('请选择日期')
  235. return
  236. }
  237. loading.value = true
  238. loadingProgress.value = 0
  239. loadingStatus.value = '正在初始化...'
  240. // 模拟进度更新
  241. const progressInterval = setInterval(() => {
  242. if (loadingProgress.value < 90) {
  243. loadingProgress.value += 10
  244. // 更新状态文本
  245. if (loadingProgress.value <= 30) {
  246. loadingStatus.value = '🔍 正在搜索景点...'
  247. } else if (loadingProgress.value <= 50) {
  248. loadingStatus.value = '🌤️ 正在查询天气...'
  249. } else if (loadingProgress.value <= 70) {
  250. loadingStatus.value = '🏨 正在推荐酒店...'
  251. } else {
  252. loadingStatus.value = '📋 正在生成行程计划...'
  253. }
  254. }
  255. }, 500)
  256. try {
  257. const requestData: TripFormData = {
  258. city: formData.city,
  259. start_date: formData.start_date.format('YYYY-MM-DD'),
  260. end_date: formData.end_date.format('YYYY-MM-DD'),
  261. travel_days: formData.travel_days,
  262. transportation: formData.transportation,
  263. accommodation: formData.accommodation,
  264. preferences: formData.preferences,
  265. free_text_input: formData.free_text_input
  266. }
  267. const response = await generateTripPlan(requestData)
  268. clearInterval(progressInterval)
  269. loadingProgress.value = 100
  270. loadingStatus.value = '✅ 完成!'
  271. if (response.success && response.data) {
  272. // 保存到sessionStorage
  273. sessionStorage.setItem('tripPlan', JSON.stringify(response.data))
  274. message.success('旅行计划生成成功!')
  275. // 短暂延迟后跳转
  276. setTimeout(() => {
  277. router.push('/result')
  278. }, 500)
  279. } else {
  280. message.error(response.message || '生成失败')
  281. }
  282. } catch (error: any) {
  283. clearInterval(progressInterval)
  284. message.error(error.message || '生成旅行计划失败,请稍后重试')
  285. } finally {
  286. setTimeout(() => {
  287. loading.value = false
  288. loadingProgress.value = 0
  289. loadingStatus.value = ''
  290. }, 1000)
  291. }
  292. }
  293. </script>
  294. <style scoped>
  295. .home-container {
  296. min-height: 100vh;
  297. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  298. padding: 60px 20px;
  299. position: relative;
  300. overflow: hidden;
  301. }
  302. /* 背景装饰 */
  303. .bg-decoration {
  304. position: absolute;
  305. top: 0;
  306. left: 0;
  307. width: 100%;
  308. height: 100%;
  309. pointer-events: none;
  310. overflow: hidden;
  311. }
  312. .circle {
  313. position: absolute;
  314. border-radius: 50%;
  315. background: rgba(255, 255, 255, 0.1);
  316. animation: float 20s infinite ease-in-out;
  317. }
  318. .circle-1 {
  319. width: 300px;
  320. height: 300px;
  321. top: -100px;
  322. left: -100px;
  323. animation-delay: 0s;
  324. }
  325. .circle-2 {
  326. width: 200px;
  327. height: 200px;
  328. top: 50%;
  329. right: -50px;
  330. animation-delay: 5s;
  331. }
  332. .circle-3 {
  333. width: 150px;
  334. height: 150px;
  335. bottom: -50px;
  336. left: 30%;
  337. animation-delay: 10s;
  338. }
  339. @keyframes float {
  340. 0%, 100% {
  341. transform: translateY(0) rotate(0deg);
  342. }
  343. 50% {
  344. transform: translateY(-30px) rotate(180deg);
  345. }
  346. }
  347. /* 页面标题 */
  348. .page-header {
  349. text-align: center;
  350. margin-bottom: 50px;
  351. animation: fadeInDown 0.8s ease-out;
  352. position: relative;
  353. z-index: 1;
  354. }
  355. .icon-wrapper {
  356. margin-bottom: 20px;
  357. }
  358. .icon {
  359. font-size: 80px;
  360. display: inline-block;
  361. animation: bounce 2s infinite;
  362. }
  363. @keyframes bounce {
  364. 0%, 100% {
  365. transform: translateY(0);
  366. }
  367. 50% {
  368. transform: translateY(-20px);
  369. }
  370. }
  371. .page-title {
  372. font-size: 56px;
  373. font-weight: 800;
  374. color: #ffffff;
  375. margin-bottom: 16px;
  376. text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.3);
  377. letter-spacing: 2px;
  378. }
  379. .page-subtitle {
  380. font-size: 20px;
  381. color: rgba(255, 255, 255, 0.95);
  382. margin: 0;
  383. font-weight: 300;
  384. }
  385. /* 表单卡片 */
  386. .form-card {
  387. max-width: 1400px;
  388. margin: 0 auto;
  389. border-radius: 24px;
  390. box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4);
  391. animation: fadeInUp 0.8s ease-out;
  392. position: relative;
  393. z-index: 1;
  394. backdrop-filter: blur(10px);
  395. background: rgba(255, 255, 255, 0.98) !important;
  396. }
  397. /* 表单分区 */
  398. .form-section {
  399. margin-bottom: 32px;
  400. padding: 24px;
  401. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  402. border-radius: 16px;
  403. border: 1px solid #e8e8e8;
  404. transition: all 0.3s ease;
  405. }
  406. .form-section:hover {
  407. box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
  408. transform: translateY(-2px);
  409. }
  410. .section-header {
  411. display: flex;
  412. align-items: center;
  413. margin-bottom: 20px;
  414. padding-bottom: 12px;
  415. border-bottom: 2px solid #667eea;
  416. }
  417. .section-icon {
  418. font-size: 24px;
  419. margin-right: 12px;
  420. }
  421. .section-title {
  422. font-size: 18px;
  423. font-weight: 600;
  424. color: #333;
  425. }
  426. /* 表单标签 */
  427. .form-label {
  428. font-size: 15px;
  429. font-weight: 500;
  430. color: #555;
  431. }
  432. /* 自定义输入框 */
  433. .custom-input :deep(.ant-input),
  434. .custom-input :deep(.ant-picker) {
  435. border-radius: 12px;
  436. border: 2px solid #e8e8e8;
  437. transition: all 0.3s ease;
  438. }
  439. .custom-input :deep(.ant-input:hover),
  440. .custom-input :deep(.ant-picker:hover) {
  441. border-color: #667eea;
  442. }
  443. .custom-input :deep(.ant-input:focus),
  444. .custom-input :deep(.ant-picker-focused) {
  445. border-color: #667eea;
  446. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  447. }
  448. /* 自定义选择框 */
  449. .custom-select :deep(.ant-select-selector) {
  450. border-radius: 12px !important;
  451. border: 2px solid #e8e8e8 !important;
  452. transition: all 0.3s ease;
  453. }
  454. .custom-select:hover :deep(.ant-select-selector) {
  455. border-color: #667eea !important;
  456. }
  457. .custom-select :deep(.ant-select-focused .ant-select-selector) {
  458. border-color: #667eea !important;
  459. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
  460. }
  461. /* 天数显示 - 紧凑版 */
  462. .days-display-compact {
  463. display: flex;
  464. align-items: center;
  465. justify-content: center;
  466. height: 40px;
  467. padding: 8px 16px;
  468. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  469. border-radius: 12px;
  470. color: white;
  471. }
  472. .days-display-compact .days-value {
  473. font-size: 24px;
  474. font-weight: 700;
  475. margin-right: 4px;
  476. }
  477. .days-display-compact .days-unit {
  478. font-size: 14px;
  479. }
  480. /* 偏好标签 */
  481. .preference-tags {
  482. display: flex;
  483. flex-wrap: wrap;
  484. gap: 8px;
  485. }
  486. .custom-checkbox-group {
  487. display: flex;
  488. flex-wrap: wrap;
  489. gap: 8px;
  490. width: 100%;
  491. }
  492. .preference-tag :deep(.ant-checkbox-wrapper) {
  493. margin: 0 !important;
  494. padding: 8px 16px;
  495. border: 2px solid #e8e8e8;
  496. border-radius: 20px;
  497. transition: all 0.3s ease;
  498. background: white;
  499. font-size: 14px;
  500. }
  501. .preference-tag :deep(.ant-checkbox-wrapper:hover) {
  502. border-color: #667eea;
  503. background: #f5f7ff;
  504. }
  505. .preference-tag :deep(.ant-checkbox-wrapper-checked) {
  506. border-color: #667eea;
  507. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  508. color: white;
  509. }
  510. /* 自定义文本域 */
  511. .custom-textarea :deep(.ant-input) {
  512. border-radius: 12px;
  513. border: 2px solid #e8e8e8;
  514. transition: all 0.3s ease;
  515. }
  516. .custom-textarea :deep(.ant-input:hover) {
  517. border-color: #667eea;
  518. }
  519. .custom-textarea :deep(.ant-input:focus) {
  520. border-color: #667eea;
  521. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  522. }
  523. /* 提交按钮 */
  524. .submit-button {
  525. height: 56px;
  526. border-radius: 28px;
  527. font-size: 18px;
  528. font-weight: 600;
  529. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  530. border: none;
  531. box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
  532. transition: all 0.3s ease;
  533. }
  534. .submit-button:hover {
  535. transform: translateY(-2px);
  536. box-shadow: 0 12px 32px rgba(102, 126, 234, 0.5);
  537. }
  538. .submit-button:active {
  539. transform: translateY(0);
  540. }
  541. .button-icon {
  542. margin-right: 8px;
  543. font-size: 20px;
  544. }
  545. /* 加载容器 */
  546. .loading-container {
  547. text-align: center;
  548. padding: 24px;
  549. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  550. border-radius: 16px;
  551. border: 2px dashed #667eea;
  552. }
  553. .loading-status {
  554. margin-top: 16px;
  555. color: #667eea;
  556. font-size: 18px;
  557. font-weight: 500;
  558. }
  559. /* 动画 */
  560. @keyframes fadeInDown {
  561. from {
  562. opacity: 0;
  563. transform: translateY(-30px);
  564. }
  565. to {
  566. opacity: 1;
  567. transform: translateY(0);
  568. }
  569. }
  570. @keyframes fadeInUp {
  571. from {
  572. opacity: 0;
  573. transform: translateY(30px);
  574. }
  575. to {
  576. opacity: 1;
  577. transform: translateY(0);
  578. }
  579. }
  580. </style>