Result.vue 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434
  1. <template>
  2. <div class="result-container">
  3. <!-- 页面头部 -->
  4. <div class="page-header">
  5. <a-button class="back-button" size="large" @click="goBack">
  6. ← 返回首页
  7. </a-button>
  8. <a-space size="middle">
  9. <a-button v-if="!editMode" @click="toggleEditMode" type="default">
  10. ✏️ 编辑行程
  11. </a-button>
  12. <a-button v-else @click="saveChanges" type="primary">
  13. 💾 保存修改
  14. </a-button>
  15. <a-button v-if="editMode" @click="cancelEdit" type="default">
  16. ❌ 取消编辑
  17. </a-button>
  18. <!-- 导出按钮 -->
  19. <a-dropdown v-if="!editMode">
  20. <template #overlay>
  21. <a-menu>
  22. <a-menu-item key="image" @click="exportAsImage">
  23. 📷 导出为图片
  24. </a-menu-item>
  25. <a-menu-item key="pdf" @click="exportAsPDF">
  26. 📄 导出为PDF
  27. </a-menu-item>
  28. </a-menu>
  29. </template>
  30. <a-button type="default">
  31. 📥 导出行程 <DownOutlined />
  32. </a-button>
  33. </a-dropdown>
  34. </a-space>
  35. </div>
  36. <div v-if="tripPlan" class="content-wrapper">
  37. <!-- 侧边导航 -->
  38. <div class="side-nav">
  39. <a-affix :offset-top="80">
  40. <a-menu mode="inline" :selected-keys="[activeSection]" @click="scrollToSection">
  41. <a-menu-item key="overview">
  42. <span>📋 行程概览</span>
  43. </a-menu-item>
  44. <a-menu-item key="budget" v-if="tripPlan.budget">
  45. <span>💰 预算明细</span>
  46. </a-menu-item>
  47. <a-menu-item key="map">
  48. <span>📍 景点地图</span>
  49. </a-menu-item>
  50. <a-sub-menu key="days" title="📅 每日行程">
  51. <a-menu-item v-for="(day, index) in tripPlan.days" :key="`day-${index}`">
  52. 第{{ day.day_index + 1 }}天
  53. </a-menu-item>
  54. </a-sub-menu>
  55. <a-menu-item key="weather" v-if="tripPlan.weather_info && tripPlan.weather_info.length > 0">
  56. <span>🌤️ 天气信息</span>
  57. </a-menu-item>
  58. </a-menu>
  59. </a-affix>
  60. </div>
  61. <!-- 主内容区 -->
  62. <div class="main-content">
  63. <!-- 顶部信息区:左侧概览+预算,右侧地图 -->
  64. <div class="top-info-section">
  65. <!-- 左侧:行程概览和预算明细 -->
  66. <div class="left-info">
  67. <!-- 行程概览 -->
  68. <a-card id="overview" :title="`${tripPlan.city}旅行计划`" :bordered="false" class="overview-card">
  69. <div class="overview-content">
  70. <div class="info-item">
  71. <span class="info-label">📅 日期:</span>
  72. <span class="info-value">{{ tripPlan.start_date }} 至 {{ tripPlan.end_date }}</span>
  73. </div>
  74. <div class="info-item">
  75. <span class="info-label">💡 建议:</span>
  76. <span class="info-value">{{ tripPlan.overall_suggestions }}</span>
  77. </div>
  78. </div>
  79. </a-card>
  80. <!-- 预算明细 -->
  81. <a-card id="budget" v-if="tripPlan.budget" title="💰 预算明细" :bordered="false" class="budget-card">
  82. <div class="budget-grid">
  83. <div class="budget-item">
  84. <div class="budget-label">景点门票</div>
  85. <div class="budget-value">¥{{ tripPlan.budget.total_attractions }}</div>
  86. </div>
  87. <div class="budget-item">
  88. <div class="budget-label">酒店住宿</div>
  89. <div class="budget-value">¥{{ tripPlan.budget.total_hotels }}</div>
  90. </div>
  91. <div class="budget-item">
  92. <div class="budget-label">餐饮费用</div>
  93. <div class="budget-value">¥{{ tripPlan.budget.total_meals }}</div>
  94. </div>
  95. <div class="budget-item">
  96. <div class="budget-label">交通费用</div>
  97. <div class="budget-value">¥{{ tripPlan.budget.total_transportation }}</div>
  98. </div>
  99. </div>
  100. <div class="budget-total">
  101. <span class="total-label">预估总费用</span>
  102. <span class="total-value">¥{{ tripPlan.budget.total }}</span>
  103. </div>
  104. </a-card>
  105. </div>
  106. <!-- 右侧:地图 -->
  107. <div class="right-map">
  108. <a-card id="map" title="📍 景点地图" :bordered="false" class="map-card">
  109. <div id="amap-container" style="width: 100%; height: 100%"></div>
  110. </a-card>
  111. </div>
  112. </div>
  113. <!-- 每日行程:可折叠 -->
  114. <a-card title="📅 每日行程" :bordered="false" class="days-card">
  115. <a-collapse v-model:activeKey="activeDays" accordion>
  116. <a-collapse-panel
  117. v-for="(day, index) in tripPlan.days"
  118. :key="index"
  119. :id="`day-${index}`"
  120. >
  121. <template #header>
  122. <div class="day-header">
  123. <span class="day-title">第{{ day.day_index + 1 }}天</span>
  124. <span class="day-date">{{ day.date }}</span>
  125. </div>
  126. </template>
  127. <!-- 行程基本信息 -->
  128. <div class="day-info">
  129. <div class="info-row">
  130. <span class="label">📝 行程描述:</span>
  131. <span class="value">{{ day.description }}</span>
  132. </div>
  133. <div class="info-row">
  134. <span class="label">🚗 交通方式:</span>
  135. <span class="value">{{ day.transportation }}</span>
  136. </div>
  137. <div class="info-row">
  138. <span class="label">🏨 住宿:</span>
  139. <span class="value">{{ day.accommodation }}</span>
  140. </div>
  141. </div>
  142. <!-- 景点安排 -->
  143. <a-divider orientation="left">🎯 景点安排</a-divider>
  144. <a-list
  145. :data-source="day.attractions"
  146. :grid="{ gutter: 16, column: 2 }"
  147. >
  148. <template #renderItem="{ item, index }">
  149. <a-list-item>
  150. <a-card :title="item.name" size="small" class="attraction-card">
  151. <!-- 编辑模式下的操作按钮 -->
  152. <template #extra v-if="editMode">
  153. <a-space>
  154. <a-button
  155. size="small"
  156. @click="moveAttraction(day.day_index, index, 'up')"
  157. :disabled="index === 0"
  158. >
  159. ↑
  160. </a-button>
  161. <a-button
  162. size="small"
  163. @click="moveAttraction(day.day_index, index, 'down')"
  164. :disabled="index === day.attractions.length - 1"
  165. >
  166. ↓
  167. </a-button>
  168. <a-button
  169. size="small"
  170. danger
  171. @click="deleteAttraction(day.day_index, index)"
  172. >
  173. 🗑️
  174. </a-button>
  175. </a-space>
  176. </template>
  177. <!-- 景点图片 -->
  178. <div class="attraction-image-wrapper">
  179. <img
  180. :src="getAttractionImage(item.name, index)"
  181. :alt="item.name"
  182. class="attraction-image"
  183. @error="handleImageError"
  184. />
  185. <div class="attraction-badge">
  186. <span class="badge-number">{{ index + 1 }}</span>
  187. </div>
  188. <div v-if="item.ticket_price" class="price-tag">
  189. ¥{{ item.ticket_price }}
  190. </div>
  191. </div>
  192. <!-- 编辑模式下可编辑的字段 -->
  193. <div v-if="editMode">
  194. <p><strong>地址:</strong></p>
  195. <a-input v-model:value="item.address" size="small" style="margin-bottom: 8px" />
  196. <p><strong>游览时长(分钟):</strong></p>
  197. <a-input-number v-model:value="item.visit_duration" :min="10" :max="480" size="small" style="width: 100%; margin-bottom: 8px" />
  198. <p><strong>描述:</strong></p>
  199. <a-textarea v-model:value="item.description" :rows="2" size="small" style="margin-bottom: 8px" />
  200. </div>
  201. <!-- 查看模式 -->
  202. <div v-else>
  203. <p><strong>地址:</strong> {{ item.address }}</p>
  204. <p><strong>游览时长:</strong> {{ item.visit_duration }}分钟</p>
  205. <p><strong>描述:</strong> {{ item.description }}</p>
  206. <p v-if="item.rating"><strong>评分:</strong> {{ item.rating }}⭐</p>
  207. </div>
  208. </a-card>
  209. </a-list-item>
  210. </template>
  211. </a-list>
  212. <!-- 酒店推荐 -->
  213. <a-divider v-if="day.hotel" orientation="left">🏨 住宿推荐</a-divider>
  214. <a-card v-if="day.hotel" size="small" class="hotel-card">
  215. <template #title>
  216. <span class="hotel-title">{{ day.hotel.name }}</span>
  217. </template>
  218. <a-descriptions :column="2" size="small">
  219. <a-descriptions-item label="地址">{{ day.hotel.address }}</a-descriptions-item>
  220. <a-descriptions-item label="类型">{{ day.hotel.type }}</a-descriptions-item>
  221. <a-descriptions-item label="价格范围">{{ day.hotel.price_range }}</a-descriptions-item>
  222. <a-descriptions-item label="评分">{{ day.hotel.rating }}⭐</a-descriptions-item>
  223. <a-descriptions-item label="距离" :span="2">{{ day.hotel.distance }}</a-descriptions-item>
  224. </a-descriptions>
  225. </a-card>
  226. <!-- 餐饮安排 -->
  227. <a-divider orientation="left">🍽️ 餐饮安排</a-divider>
  228. <a-descriptions :column="1" bordered size="small">
  229. <a-descriptions-item
  230. v-for="meal in day.meals"
  231. :key="meal.type"
  232. :label="getMealLabel(meal.type)"
  233. >
  234. {{ meal.name }}
  235. <span v-if="meal.description"> - {{ meal.description }}</span>
  236. </a-descriptions-item>
  237. </a-descriptions>
  238. </a-collapse-panel>
  239. </a-collapse>
  240. </a-card>
  241. <a-card id="weather" v-if="tripPlan.weather_info && tripPlan.weather_info.length > 0" title="天气信息" style="margin-top: 20px" :bordered="false">
  242. <a-list
  243. :data-source="tripPlan.weather_info"
  244. :grid="{ gutter: 16, column: 3 }"
  245. >
  246. <template #renderItem="{ item }">
  247. <a-list-item>
  248. <a-card size="small" class="weather-card">
  249. <div class="weather-date">{{ item.date }}</div>
  250. <div class="weather-info-row">
  251. <span class="weather-icon">☀️</span>
  252. <div>
  253. <div class="weather-label">白天</div>
  254. <div class="weather-value">{{ item.day_weather }} {{ item.day_temp }}°C</div>
  255. </div>
  256. </div>
  257. <div class="weather-info-row">
  258. <span class="weather-icon">🌙</span>
  259. <div>
  260. <div class="weather-label">夜间</div>
  261. <div class="weather-value">{{ item.night_weather }} {{ item.night_temp }}°C</div>
  262. </div>
  263. </div>
  264. <div class="weather-wind">
  265. 💨 {{ item.wind_direction }} {{ item.wind_power }}
  266. </div>
  267. </a-card>
  268. </a-list-item>
  269. </template>
  270. </a-list>
  271. </a-card>
  272. </div>
  273. </div>
  274. <a-empty v-else description="没有找到旅行计划数据">
  275. <template #image>
  276. <div style="font-size: 80px;">🗺️</div>
  277. </template>
  278. <template #description>
  279. <span style="color: #999;">暂无旅行计划数据,请先创建行程</span>
  280. </template>
  281. <a-button type="primary" @click="goBack">返回首页创建行程</a-button>
  282. </a-empty>
  283. <!-- 回到顶部按钮 -->
  284. <a-back-top :visibility-height="300">
  285. <div class="back-top-button">
  286. ↑
  287. </div>
  288. </a-back-top>
  289. </div>
  290. </template>
  291. <script setup lang="ts">
  292. import { ref, onMounted, nextTick } from 'vue'
  293. import { useRouter } from 'vue-router'
  294. import { message } from 'ant-design-vue'
  295. import { DownOutlined } from '@ant-design/icons-vue'
  296. import AMapLoader from '@amap/amap-jsapi-loader'
  297. import html2canvas from 'html2canvas'
  298. import jsPDF from 'jspdf'
  299. import type { TripPlan } from '@/types'
  300. const router = useRouter()
  301. const tripPlan = ref<TripPlan | null>(null)
  302. const editMode = ref(false)
  303. const originalPlan = ref<TripPlan | null>(null)
  304. const attractionPhotos = ref<Record<string, string>>({})
  305. const activeSection = ref('overview')
  306. const activeDays = ref<number[]>([0]) // 默认展开第一天
  307. let map: any = null
  308. onMounted(async () => {
  309. const data = sessionStorage.getItem('tripPlan')
  310. if (data) {
  311. tripPlan.value = JSON.parse(data)
  312. // 加载景点图片
  313. await loadAttractionPhotos()
  314. // 等待DOM渲染完成后初始化地图
  315. await nextTick()
  316. initMap()
  317. }
  318. })
  319. const goBack = () => {
  320. router.push('/')
  321. }
  322. // 滚动到指定区域
  323. const scrollToSection = ({ key }: { key: string }) => {
  324. activeSection.value = key
  325. const element = document.getElementById(key)
  326. if (element) {
  327. element.scrollIntoView({ behavior: 'smooth', block: 'start' })
  328. }
  329. }
  330. // 切换编辑模式
  331. const toggleEditMode = () => {
  332. editMode.value = true
  333. // 保存原始数据用于取消编辑
  334. originalPlan.value = JSON.parse(JSON.stringify(tripPlan.value))
  335. message.info('进入编辑模式')
  336. }
  337. // 保存修改
  338. const saveChanges = () => {
  339. editMode.value = false
  340. // 更新sessionStorage
  341. if (tripPlan.value) {
  342. sessionStorage.setItem('tripPlan', JSON.stringify(tripPlan.value))
  343. }
  344. message.success('修改已保存')
  345. // 重新初始化地图以反映更改
  346. if (map) {
  347. map.destroy()
  348. }
  349. nextTick(() => {
  350. initMap()
  351. })
  352. }
  353. // 取消编辑
  354. const cancelEdit = () => {
  355. if (originalPlan.value) {
  356. tripPlan.value = JSON.parse(JSON.stringify(originalPlan.value))
  357. }
  358. editMode.value = false
  359. message.info('已取消编辑')
  360. }
  361. // 删除景点
  362. const deleteAttraction = (dayIndex: number, attrIndex: number) => {
  363. if (!tripPlan.value) return
  364. const day = tripPlan.value.days[dayIndex]
  365. if (day.attractions.length <= 1) {
  366. message.warning('每天至少需要保留一个景点')
  367. return
  368. }
  369. day.attractions.splice(attrIndex, 1)
  370. message.success('景点已删除')
  371. }
  372. // 移动景点顺序
  373. const moveAttraction = (dayIndex: number, attrIndex: number, direction: 'up' | 'down') => {
  374. if (!tripPlan.value) return
  375. const day = tripPlan.value.days[dayIndex]
  376. const attractions = day.attractions
  377. if (direction === 'up' && attrIndex > 0) {
  378. [attractions[attrIndex], attractions[attrIndex - 1]] = [attractions[attrIndex - 1], attractions[attrIndex]]
  379. } else if (direction === 'down' && attrIndex < attractions.length - 1) {
  380. [attractions[attrIndex], attractions[attrIndex + 1]] = [attractions[attrIndex + 1], attractions[attrIndex]]
  381. }
  382. }
  383. const getMealLabel = (type: string): string => {
  384. const labels: Record<string, string> = {
  385. breakfast: '早餐',
  386. lunch: '午餐',
  387. dinner: '晚餐',
  388. snack: '小吃'
  389. }
  390. return labels[type] || type
  391. }
  392. // 加载所有景点图片
  393. const loadAttractionPhotos = async () => {
  394. if (!tripPlan.value) return
  395. const promises: Promise<void>[] = []
  396. tripPlan.value.days.forEach(day => {
  397. day.attractions.forEach(attraction => {
  398. const promise = fetch(`http://localhost:8000/api/poi/photo?name=${encodeURIComponent(attraction.name)}`)
  399. .then(res => res.json())
  400. .then(data => {
  401. if (data.success && data.data.photo_url) {
  402. attractionPhotos.value[attraction.name] = data.data.photo_url
  403. }
  404. })
  405. .catch(err => {
  406. console.error(`获取${attraction.name}图片失败:`, err)
  407. })
  408. promises.push(promise)
  409. })
  410. })
  411. await Promise.all(promises)
  412. }
  413. // 获取景点图片
  414. const getAttractionImage = (name: string, index: number): string => {
  415. // 如果已加载真实图片,返回真实图片
  416. if (attractionPhotos.value[name]) {
  417. return attractionPhotos.value[name]
  418. }
  419. // 返回一个纯色占位图(避免跨域问题)
  420. const colors = [
  421. { start: '#667eea', end: '#764ba2' },
  422. { start: '#f093fb', end: '#f5576c' },
  423. { start: '#4facfe', end: '#00f2fe' },
  424. { start: '#43e97b', end: '#38f9d7' },
  425. { start: '#fa709a', end: '#fee140' }
  426. ]
  427. const colorIndex = index % colors.length
  428. const { start, end } = colors[colorIndex]
  429. // 使用base64编码避免中文问题
  430. const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300">
  431. <defs>
  432. <linearGradient id="grad${index}" x1="0%" y1="0%" x2="100%" y2="100%">
  433. <stop offset="0%" style="stop-color:${start};stop-opacity:1" />
  434. <stop offset="100%" style="stop-color:${end};stop-opacity:1" />
  435. </linearGradient>
  436. </defs>
  437. <rect width="400" height="300" fill="url(#grad${index})"/>
  438. <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="24" font-weight="bold" fill="white">${name}</text>
  439. </svg>`
  440. return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`
  441. }
  442. // 图片加载失败时的处理
  443. const handleImageError = (event: Event) => {
  444. const img = event.target as HTMLImageElement
  445. // 使用灰色占位图
  446. img.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="400" height="300"%3E%3Crect width="400" height="300" fill="%23f0f0f0"/%3E%3Ctext x="50%25" y="50%25" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="18" fill="%23999"%3E图片加载失败%3C/text%3E%3C/svg%3E'
  447. }
  448. // 导出为图片
  449. const exportAsImage = async () => {
  450. try {
  451. message.loading({ content: '正在生成图片...', key: 'export', duration: 0 })
  452. const element = document.querySelector('.main-content') as HTMLElement
  453. if (!element) {
  454. throw new Error('未找到内容元素')
  455. }
  456. // 创建一个独立的容器
  457. const exportContainer = document.createElement('div')
  458. exportContainer.style.width = element.offsetWidth + 'px'
  459. exportContainer.style.backgroundColor = '#f5f7fa'
  460. exportContainer.style.padding = '20px'
  461. // 复制所有内容
  462. exportContainer.innerHTML = element.innerHTML
  463. // 处理地图截图
  464. const mapContainer = document.getElementById('amap-container')
  465. if (mapContainer && map) {
  466. const mapCanvas = mapContainer.querySelector('canvas')
  467. if (mapCanvas) {
  468. const mapSnapshot = mapCanvas.toDataURL('image/png')
  469. const exportMapContainer = exportContainer.querySelector('#amap-container')
  470. if (exportMapContainer) {
  471. exportMapContainer.innerHTML = `<img src="${mapSnapshot}" style="width:100%;height:100%;object-fit:cover;" />`
  472. }
  473. }
  474. }
  475. // 移除所有ant-card类,替换为纯div
  476. const cards = exportContainer.querySelectorAll('.ant-card')
  477. cards.forEach((card) => {
  478. const cardEl = card as HTMLElement
  479. try {
  480. cardEl.className = '' // 移除所有类
  481. cardEl.style.setProperty('background-color', '#ffffff')
  482. cardEl.style.setProperty('border-radius', '12px')
  483. cardEl.style.setProperty('box-shadow', '0 4px 12px rgba(0, 0, 0, 0.1)')
  484. cardEl.style.setProperty('margin-bottom', '20px')
  485. cardEl.style.setProperty('overflow', 'hidden')
  486. } catch (err) {
  487. console.error('设置卡片样式失败:', err)
  488. }
  489. })
  490. // 处理卡片头部
  491. const cardHeads = exportContainer.querySelectorAll('.ant-card-head')
  492. cardHeads.forEach((head) => {
  493. const headEl = head as HTMLElement
  494. try {
  495. headEl.style.setProperty('background-color', '#667eea')
  496. headEl.style.setProperty('color', '#ffffff')
  497. headEl.style.setProperty('padding', '16px 24px')
  498. headEl.style.setProperty('font-size', '18px')
  499. headEl.style.setProperty('font-weight', '600')
  500. } catch (err) {
  501. console.error('设置卡片头部样式失败:', err)
  502. }
  503. })
  504. // 处理卡片内容
  505. const cardBodies = exportContainer.querySelectorAll('.ant-card-body')
  506. cardBodies.forEach((body) => {
  507. const bodyEl = body as HTMLElement
  508. bodyEl.style.setProperty('background-color', '#ffffff')
  509. bodyEl.style.setProperty('padding', '24px')
  510. })
  511. // 处理酒店卡片头部
  512. const hotelCards = exportContainer.querySelectorAll('.hotel-card')
  513. hotelCards.forEach((card) => {
  514. const head = card.querySelector('.ant-card-head') as HTMLElement
  515. if (head) {
  516. head.style.setProperty('background-color', '#1976d2')
  517. }
  518. (card as HTMLElement).style.setProperty('background-color', '#e3f2fd')
  519. })
  520. // 处理天气卡片
  521. const weatherCards = exportContainer.querySelectorAll('.weather-card')
  522. weatherCards.forEach((card) => {
  523. (card as HTMLElement).style.setProperty('background-color', '#e0f7fa')
  524. })
  525. // 处理预算总计
  526. const budgetTotal = exportContainer.querySelector('.budget-total')
  527. if (budgetTotal) {
  528. const el = budgetTotal as HTMLElement
  529. el.style.setProperty('background-color', '#667eea')
  530. el.style.setProperty('color', '#ffffff')
  531. el.style.setProperty('padding', '20px')
  532. el.style.setProperty('border-radius', '12px')
  533. el.style.setProperty('margin-bottom', '20px')
  534. }
  535. // 处理预算项
  536. const budgetItems = exportContainer.querySelectorAll('.budget-item')
  537. budgetItems.forEach((item) => {
  538. const el = item as HTMLElement
  539. el.style.setProperty('background-color', '#f5f7fa')
  540. el.style.setProperty('padding', '16px')
  541. el.style.setProperty('border-radius', '8px')
  542. el.style.setProperty('margin-bottom', '12px')
  543. })
  544. // 添加到body(隐藏)
  545. exportContainer.style.position = 'absolute'
  546. exportContainer.style.left = '-9999px'
  547. document.body.appendChild(exportContainer)
  548. const canvas = await html2canvas(exportContainer, {
  549. backgroundColor: '#f5f7fa',
  550. scale: 2,
  551. logging: false,
  552. useCORS: true,
  553. allowTaint: true
  554. })
  555. // 移除容器
  556. document.body.removeChild(exportContainer)
  557. // 转换为图片并下载
  558. const link = document.createElement('a')
  559. link.download = `旅行计划_${tripPlan.value?.city}_${new Date().getTime()}.png`
  560. link.href = canvas.toDataURL('image/png')
  561. link.click()
  562. message.success({ content: '图片导出成功!', key: 'export' })
  563. } catch (error: any) {
  564. console.error('导出图片失败:', error)
  565. message.error({ content: `导出图片失败: ${error.message}`, key: 'export' })
  566. }
  567. }
  568. // 导出为PDF
  569. const exportAsPDF = async () => {
  570. try {
  571. message.loading({ content: '正在生成PDF...', key: 'export', duration: 0 })
  572. const element = document.querySelector('.main-content') as HTMLElement
  573. if (!element) {
  574. throw new Error('未找到内容元素')
  575. }
  576. // 创建一个独立的容器
  577. const exportContainer = document.createElement('div')
  578. exportContainer.style.width = element.offsetWidth + 'px'
  579. exportContainer.style.backgroundColor = '#f5f7fa'
  580. exportContainer.style.padding = '20px'
  581. // 复制所有内容
  582. exportContainer.innerHTML = element.innerHTML
  583. // 处理地图截图
  584. const mapContainer = document.getElementById('amap-container')
  585. if (mapContainer && map) {
  586. const mapCanvas = mapContainer.querySelector('canvas')
  587. if (mapCanvas) {
  588. const mapSnapshot = mapCanvas.toDataURL('image/png')
  589. const exportMapContainer = exportContainer.querySelector('#amap-container')
  590. if (exportMapContainer) {
  591. exportMapContainer.innerHTML = `<img src="${mapSnapshot}" style="width:100%;height:100%;object-fit:cover;" />`
  592. }
  593. }
  594. }
  595. // 移除所有ant-card类,替换为纯div
  596. const cards = exportContainer.querySelectorAll('.ant-card')
  597. cards.forEach((card) => {
  598. const cardEl = card as HTMLElement
  599. try {
  600. cardEl.className = ''
  601. cardEl.style.setProperty('background-color', '#ffffff')
  602. cardEl.style.setProperty('border-radius', '12px')
  603. cardEl.style.setProperty('box-shadow', '0 4px 12px rgba(0, 0, 0, 0.1)')
  604. cardEl.style.setProperty('margin-bottom', '20px')
  605. cardEl.style.setProperty('overflow', 'hidden')
  606. } catch (err) {
  607. console.error('设置卡片样式失败:', err)
  608. }
  609. })
  610. // 处理卡片头部
  611. const cardHeads = exportContainer.querySelectorAll('.ant-card-head')
  612. cardHeads.forEach((head) => {
  613. const headEl = head as HTMLElement
  614. try {
  615. headEl.style.setProperty('background-color', '#667eea')
  616. headEl.style.setProperty('color', '#ffffff')
  617. headEl.style.setProperty('padding', '16px 24px')
  618. headEl.style.setProperty('font-size', '18px')
  619. headEl.style.setProperty('font-weight', '600')
  620. } catch (err) {
  621. console.error('设置卡片头部样式失败:', err)
  622. }
  623. })
  624. // 处理卡片内容
  625. const cardBodies = exportContainer.querySelectorAll('.ant-card-body')
  626. cardBodies.forEach((body) => {
  627. const bodyEl = body as HTMLElement
  628. bodyEl.style.setProperty('background-color', '#ffffff')
  629. bodyEl.style.setProperty('padding', '24px')
  630. })
  631. // 处理酒店卡片头部
  632. const hotelCards = exportContainer.querySelectorAll('.hotel-card')
  633. hotelCards.forEach((card) => {
  634. const head = card.querySelector('.ant-card-head') as HTMLElement
  635. if (head) {
  636. head.style.setProperty('background-color', '#1976d2')
  637. }
  638. (card as HTMLElement).style.setProperty('background-color', '#e3f2fd')
  639. })
  640. // 处理天气卡片
  641. const weatherCards = exportContainer.querySelectorAll('.weather-card')
  642. weatherCards.forEach((card) => {
  643. (card as HTMLElement).style.setProperty('background-color', '#e0f7fa')
  644. })
  645. // 处理预算总计
  646. const budgetTotal = exportContainer.querySelector('.budget-total')
  647. if (budgetTotal) {
  648. const el = budgetTotal as HTMLElement
  649. el.style.setProperty('background-color', '#667eea')
  650. el.style.setProperty('color', '#ffffff')
  651. el.style.setProperty('padding', '20px')
  652. el.style.setProperty('border-radius', '12px')
  653. el.style.setProperty('margin-bottom', '20px')
  654. }
  655. // 处理预算项
  656. const budgetItems = exportContainer.querySelectorAll('.budget-item')
  657. budgetItems.forEach((item) => {
  658. const el = item as HTMLElement
  659. el.style.setProperty('background-color', '#f5f7fa')
  660. el.style.setProperty('padding', '16px')
  661. el.style.setProperty('border-radius', '8px')
  662. el.style.setProperty('margin-bottom', '12px')
  663. })
  664. // 添加到body(隐藏)
  665. exportContainer.style.position = 'absolute'
  666. exportContainer.style.left = '-9999px'
  667. document.body.appendChild(exportContainer)
  668. const canvas = await html2canvas(exportContainer, {
  669. backgroundColor: '#f5f7fa',
  670. scale: 2,
  671. logging: false,
  672. useCORS: true,
  673. allowTaint: true
  674. })
  675. // 移除容器
  676. document.body.removeChild(exportContainer)
  677. const imgData = canvas.toDataURL('image/png')
  678. const pdf = new jsPDF({
  679. orientation: 'portrait',
  680. unit: 'mm',
  681. format: 'a4'
  682. })
  683. const imgWidth = 210 // A4宽度(mm)
  684. const imgHeight = (canvas.height * imgWidth) / canvas.width
  685. // 如果内容高度超过一页,分页处理
  686. let heightLeft = imgHeight
  687. let position = 0
  688. pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
  689. heightLeft -= 297 // A4高度
  690. while (heightLeft > 0) {
  691. position = heightLeft - imgHeight
  692. pdf.addPage()
  693. pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
  694. heightLeft -= 297
  695. }
  696. pdf.save(`旅行计划_${tripPlan.value?.city}_${new Date().getTime()}.pdf`)
  697. message.success({ content: 'PDF导出成功!', key: 'export' })
  698. } catch (error: any) {
  699. console.error('导出PDF失败:', error)
  700. message.error({ content: `导出PDF失败: ${error.message}`, key: 'export' })
  701. }
  702. }
  703. // 截取地图图片
  704. const captureMapImage = async () => {
  705. if (!map) return
  706. try {
  707. // 获取地图容器
  708. const mapContainer = document.getElementById('amap-container')
  709. if (!mapContainer) return
  710. // 使用高德地图的截图功能
  711. const mapCanvas = mapContainer.querySelector('canvas')
  712. if (mapCanvas) {
  713. // 创建一个img元素替换地图容器
  714. const img = document.createElement('img')
  715. img.src = mapCanvas.toDataURL('image/png')
  716. img.style.width = '100%'
  717. img.style.height = '500px'
  718. img.style.objectFit = 'cover'
  719. img.id = 'map-snapshot'
  720. // 隐藏原地图,显示截图
  721. mapContainer.style.display = 'none'
  722. mapContainer.parentElement?.appendChild(img)
  723. }
  724. } catch (error) {
  725. console.error('截取地图失败:', error)
  726. }
  727. }
  728. // 恢复地图
  729. const restoreMap = () => {
  730. const mapContainer = document.getElementById('amap-container')
  731. const snapshot = document.getElementById('map-snapshot')
  732. if (mapContainer) {
  733. mapContainer.style.display = 'block'
  734. }
  735. if (snapshot) {
  736. snapshot.remove()
  737. }
  738. }
  739. // 初始化地图
  740. const initMap = async () => {
  741. try {
  742. const AMap = await AMapLoader.load({
  743. key: import.meta.env.VITE_AMAP_WEB_JS_KEY, // 高德地图Web端(JS API) Key
  744. version: '2.0',
  745. plugins: ['AMap.Marker', 'AMap.Polyline', 'AMap.InfoWindow']
  746. })
  747. // 创建地图实例
  748. map = new AMap.Map('amap-container', {
  749. zoom: 12,
  750. center: [116.397128, 39.916527], // 默认中心点(北京)
  751. viewMode: '3D'
  752. })
  753. // 添加景点标记
  754. addAttractionMarkers(AMap)
  755. message.success('地图加载成功')
  756. } catch (error) {
  757. console.error('地图加载失败:', error)
  758. message.error('地图加载失败')
  759. }
  760. }
  761. // 添加景点标记
  762. const addAttractionMarkers = (AMap: any) => {
  763. if (!tripPlan.value) return
  764. const markers: any[] = []
  765. const allAttractions: any[] = []
  766. // 收集所有景点
  767. tripPlan.value.days.forEach((day, dayIndex) => {
  768. day.attractions.forEach((attraction, attrIndex) => {
  769. if (attraction.location && attraction.location.longitude && attraction.location.latitude) {
  770. allAttractions.push({
  771. ...attraction,
  772. dayIndex,
  773. attrIndex
  774. })
  775. }
  776. })
  777. })
  778. // 创建标记
  779. allAttractions.forEach((attraction, index) => {
  780. const marker = new AMap.Marker({
  781. position: [attraction.location.longitude, attraction.location.latitude],
  782. title: attraction.name,
  783. label: {
  784. content: `<div style="background: #4CAF50; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${index + 1}</div>`,
  785. offset: new AMap.Pixel(0, -30)
  786. }
  787. })
  788. // 创建信息窗口
  789. const infoWindow = new AMap.InfoWindow({
  790. content: `
  791. <div style="padding: 10px;">
  792. <h4 style="margin: 0 0 8px 0;">${attraction.name}</h4>
  793. <p style="margin: 4px 0;"><strong>地址:</strong> ${attraction.address}</p>
  794. <p style="margin: 4px 0;"><strong>游览时长:</strong> ${attraction.visit_duration}分钟</p>
  795. <p style="margin: 4px 0;"><strong>描述:</strong> ${attraction.description}</p>
  796. <p style="margin: 4px 0; color: #1890ff;"><strong>第${attraction.dayIndex + 1}天 景点${attraction.attrIndex + 1}</strong></p>
  797. </div>
  798. `,
  799. offset: new AMap.Pixel(0, -30)
  800. })
  801. // 点击标记显示信息窗口
  802. marker.on('click', () => {
  803. infoWindow.open(map, marker.getPosition())
  804. })
  805. markers.push(marker)
  806. })
  807. // 添加标记到地图
  808. map.add(markers)
  809. // 自动调整视野以包含所有标记
  810. if (allAttractions.length > 0) {
  811. map.setFitView(markers)
  812. }
  813. // 绘制路线
  814. drawRoutes(AMap, allAttractions)
  815. }
  816. // 绘制路线
  817. const drawRoutes = (AMap: any, attractions: any[]) => {
  818. if (attractions.length < 2) return
  819. // 按天分组绘制路线
  820. const dayGroups: any = {}
  821. attractions.forEach(attr => {
  822. if (!dayGroups[attr.dayIndex]) {
  823. dayGroups[attr.dayIndex] = []
  824. }
  825. dayGroups[attr.dayIndex].push(attr)
  826. })
  827. // 为每天的景点绘制路线
  828. Object.values(dayGroups).forEach((dayAttractions: any) => {
  829. if (dayAttractions.length < 2) return
  830. const path = dayAttractions.map((attr: any) => [
  831. attr.location.longitude,
  832. attr.location.latitude
  833. ])
  834. const polyline = new AMap.Polyline({
  835. path: path,
  836. strokeColor: '#1890ff',
  837. strokeWeight: 4,
  838. strokeOpacity: 0.8,
  839. strokeStyle: 'solid',
  840. showDir: true // 显示方向箭头
  841. })
  842. map.add(polyline)
  843. })
  844. }
  845. </script>
  846. <style scoped>
  847. .result-container {
  848. min-height: 100vh;
  849. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  850. padding: 40px 20px;
  851. }
  852. .page-header {
  853. max-width: 1200px;
  854. margin: 0 auto 30px;
  855. display: flex;
  856. justify-content: space-between;
  857. align-items: center;
  858. animation: fadeInDown 0.6s ease-out;
  859. }
  860. .back-button {
  861. border-radius: 8px;
  862. font-weight: 500;
  863. }
  864. /* 内容布局 */
  865. .content-wrapper {
  866. max-width: 1400px;
  867. margin: 0 auto;
  868. display: flex;
  869. gap: 24px;
  870. }
  871. .side-nav {
  872. width: 240px;
  873. flex-shrink: 0;
  874. }
  875. .side-nav :deep(.ant-menu) {
  876. border-radius: 12px;
  877. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  878. background: white;
  879. }
  880. .side-nav :deep(.ant-menu-item) {
  881. margin: 4px 8px;
  882. border-radius: 8px;
  883. transition: all 0.3s ease;
  884. }
  885. .side-nav :deep(.ant-menu-item-selected) {
  886. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  887. color: white;
  888. }
  889. .side-nav :deep(.ant-menu-item:hover) {
  890. background: rgba(102, 126, 234, 0.1);
  891. }
  892. .main-content {
  893. flex: 1;
  894. min-width: 0;
  895. }
  896. /* 景点图片样式 */
  897. .attraction-image-wrapper {
  898. position: relative;
  899. margin-bottom: 12px;
  900. border-radius: 8px;
  901. overflow: hidden;
  902. }
  903. .attraction-image {
  904. width: 100%;
  905. height: 200px;
  906. object-fit: cover;
  907. transition: transform 0.3s ease;
  908. }
  909. .attraction-image-wrapper:hover .attraction-image {
  910. transform: scale(1.05);
  911. }
  912. .attraction-badge {
  913. position: absolute;
  914. top: 12px;
  915. left: 12px;
  916. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  917. color: white;
  918. width: 36px;
  919. height: 36px;
  920. border-radius: 50%;
  921. display: flex;
  922. align-items: center;
  923. justify-content: center;
  924. font-weight: bold;
  925. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  926. }
  927. .badge-number {
  928. font-size: 18px;
  929. }
  930. .price-tag {
  931. position: absolute;
  932. top: 12px;
  933. right: 12px;
  934. background: rgba(255, 77, 79, 0.9);
  935. color: white;
  936. padding: 4px 12px;
  937. border-radius: 12px;
  938. font-weight: bold;
  939. font-size: 14px;
  940. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  941. }
  942. /* 天气卡片样式 */
  943. .weather-card {
  944. background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%);
  945. border: none !important;
  946. transition: all 0.3s ease;
  947. }
  948. .weather-card:hover {
  949. transform: translateY(-4px);
  950. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
  951. }
  952. .weather-date {
  953. font-size: 16px;
  954. font-weight: bold;
  955. color: #00796b;
  956. margin-bottom: 12px;
  957. text-align: center;
  958. }
  959. .weather-info-row {
  960. display: flex;
  961. align-items: center;
  962. gap: 12px;
  963. margin-bottom: 8px;
  964. }
  965. .weather-icon {
  966. font-size: 24px;
  967. }
  968. .weather-label {
  969. font-size: 12px;
  970. color: #666;
  971. }
  972. .weather-value {
  973. font-size: 16px;
  974. font-weight: 600;
  975. color: #00796b;
  976. }
  977. .weather-wind {
  978. margin-top: 8px;
  979. padding-top: 8px;
  980. border-top: 1px solid rgba(0, 121, 107, 0.2);
  981. text-align: center;
  982. color: #00796b;
  983. font-size: 14px;
  984. }
  985. /* 回到顶部按钮 */
  986. .back-top-button {
  987. width: 50px;
  988. height: 50px;
  989. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  990. color: white;
  991. border-radius: 50%;
  992. display: flex;
  993. align-items: center;
  994. justify-content: center;
  995. font-size: 24px;
  996. font-weight: bold;
  997. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  998. cursor: pointer;
  999. transition: all 0.3s ease;
  1000. }
  1001. .back-top-button:hover {
  1002. transform: scale(1.1);
  1003. box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
  1004. }
  1005. /* 酒店卡片样式 */
  1006. .hotel-card {
  1007. background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
  1008. border: none !important;
  1009. }
  1010. .hotel-card :deep(.ant-card-head) {
  1011. background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
  1012. }
  1013. .hotel-title {
  1014. color: white !important;
  1015. font-weight: 600;
  1016. }
  1017. /* 顶部信息区布局 */
  1018. .top-info-section {
  1019. display: flex;
  1020. gap: 20px;
  1021. margin-bottom: 20px;
  1022. }
  1023. .left-info {
  1024. flex: 0 0 400px;
  1025. display: flex;
  1026. flex-direction: column;
  1027. gap: 20px;
  1028. }
  1029. .right-map {
  1030. flex: 1;
  1031. }
  1032. /* 行程概览卡片 */
  1033. .overview-card {
  1034. height: fit-content;
  1035. }
  1036. .overview-content {
  1037. display: flex;
  1038. flex-direction: column;
  1039. gap: 12px;
  1040. }
  1041. .info-item {
  1042. display: flex;
  1043. flex-direction: column;
  1044. gap: 4px;
  1045. }
  1046. .info-label {
  1047. font-size: 14px;
  1048. font-weight: 600;
  1049. color: #666;
  1050. }
  1051. .info-value {
  1052. font-size: 15px;
  1053. color: #333;
  1054. line-height: 1.6;
  1055. }
  1056. /* 预算卡片 */
  1057. .budget-card {
  1058. height: fit-content;
  1059. }
  1060. .budget-grid {
  1061. display: grid;
  1062. grid-template-columns: repeat(2, 1fr);
  1063. gap: 16px;
  1064. margin-bottom: 16px;
  1065. }
  1066. .budget-item {
  1067. text-align: center;
  1068. padding: 12px;
  1069. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  1070. border-radius: 8px;
  1071. border: 1px solid #e8e8e8;
  1072. }
  1073. .budget-label {
  1074. font-size: 13px;
  1075. color: #666;
  1076. margin-bottom: 8px;
  1077. }
  1078. .budget-value {
  1079. font-size: 20px;
  1080. font-weight: 700;
  1081. color: #1890ff;
  1082. }
  1083. .budget-total {
  1084. display: flex;
  1085. justify-content: space-between;
  1086. align-items: center;
  1087. padding: 16px;
  1088. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1089. border-radius: 8px;
  1090. color: white;
  1091. }
  1092. .total-label {
  1093. font-size: 16px;
  1094. font-weight: 600;
  1095. }
  1096. .total-value {
  1097. font-size: 28px;
  1098. font-weight: 700;
  1099. }
  1100. /* 地图卡片 */
  1101. .map-card {
  1102. height: 100%;
  1103. min-height: 500px;
  1104. }
  1105. .map-card :deep(.ant-card-body) {
  1106. height: calc(100% - 57px);
  1107. padding: 0;
  1108. }
  1109. /* 每日行程卡片 */
  1110. .days-card {
  1111. margin-top: 20px;
  1112. }
  1113. .day-header {
  1114. display: flex;
  1115. justify-content: space-between;
  1116. align-items: center;
  1117. width: 100%;
  1118. }
  1119. .day-title {
  1120. font-size: 18px;
  1121. font-weight: 600;
  1122. color: #333;
  1123. }
  1124. .day-date {
  1125. font-size: 14px;
  1126. color: #999;
  1127. }
  1128. .day-info {
  1129. margin-bottom: 20px;
  1130. padding: 16px;
  1131. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  1132. border-radius: 8px;
  1133. border: 1px solid #e8e8e8;
  1134. }
  1135. .info-row {
  1136. display: flex;
  1137. gap: 12px;
  1138. margin-bottom: 8px;
  1139. }
  1140. .info-row:last-child {
  1141. margin-bottom: 0;
  1142. }
  1143. .info-row .label {
  1144. font-weight: 600;
  1145. color: #666;
  1146. min-width: 100px;
  1147. }
  1148. .info-row .value {
  1149. color: #333;
  1150. flex: 1;
  1151. }
  1152. /* 卡片样式优化 */
  1153. :deep(.ant-card) {
  1154. border-radius: 12px;
  1155. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  1156. margin-bottom: 20px;
  1157. transition: all 0.3s ease;
  1158. animation: fadeInUp 0.6s ease-out;
  1159. }
  1160. :deep(.ant-card:hover) {
  1161. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  1162. }
  1163. :deep(.ant-card-head) {
  1164. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1165. color: white !important;
  1166. border-radius: 12px 12px 0 0;
  1167. font-weight: 600;
  1168. }
  1169. :deep(.ant-card-head-title) {
  1170. color: white !important;
  1171. font-size: 18px;
  1172. }
  1173. :deep(.ant-card-head-title span) {
  1174. color: white !important;
  1175. }
  1176. /* Collapse样式 */
  1177. :deep(.ant-collapse) {
  1178. border: none;
  1179. background: transparent;
  1180. }
  1181. :deep(.ant-collapse-item) {
  1182. margin-bottom: 16px;
  1183. border: 1px solid #e8e8e8;
  1184. border-radius: 12px;
  1185. overflow: hidden;
  1186. }
  1187. :deep(.ant-collapse-header) {
  1188. background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
  1189. padding: 16px 20px !important;
  1190. font-weight: 600;
  1191. }
  1192. :deep(.ant-collapse-content) {
  1193. border-top: 1px solid #e8e8e8;
  1194. }
  1195. :deep(.ant-collapse-content-box) {
  1196. padding: 20px;
  1197. }
  1198. /* 统计卡片样式 */
  1199. :deep(.ant-statistic-title) {
  1200. font-size: 14px;
  1201. color: #666;
  1202. margin-bottom: 8px;
  1203. }
  1204. :deep(.ant-statistic-content) {
  1205. font-size: 24px;
  1206. font-weight: 600;
  1207. color: #1890ff;
  1208. }
  1209. /* 景点卡片样式 */
  1210. :deep(.ant-list-item) {
  1211. transition: all 0.3s ease;
  1212. }
  1213. :deep(.ant-list-item:hover) {
  1214. transform: scale(1.02);
  1215. }
  1216. /* 动画 */
  1217. @keyframes fadeInDown {
  1218. from {
  1219. opacity: 0;
  1220. transform: translateY(-20px);
  1221. }
  1222. to {
  1223. opacity: 1;
  1224. transform: translateY(0);
  1225. }
  1226. }
  1227. @keyframes fadeInUp {
  1228. from {
  1229. opacity: 0;
  1230. transform: translateY(20px);
  1231. }
  1232. to {
  1233. opacity: 1;
  1234. transform: translateY(0);
  1235. }
  1236. }
  1237. /* 响应式设计 */
  1238. @media (max-width: 768px) {
  1239. .result-container {
  1240. padding: 20px 10px;
  1241. }
  1242. .page-header {
  1243. flex-direction: column;
  1244. gap: 16px;
  1245. }
  1246. }
  1247. </style>