map.vue 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418
  1. <template>
  2. <view class="map-container">
  3. <!-- 顶部导航栏 -->
  4. <view class="navbar">
  5. <view class="back-btn" @tap="goBack">
  6. <text class="back-icon">←</text>
  7. <text class="back-text">返回</text>
  8. </view>
  9. <view class="title">自定义行程</view>
  10. </view>
  11. <!-- 地图区域 -->
  12. <view class="map-view">
  13. <map
  14. class="map"
  15. :latitude="latitude"
  16. :longitude="longitude"
  17. :markers="markers"
  18. :scale="scale"
  19. :show-location="true"
  20. :enable-zoom="true"
  21. :enable-scroll="true"
  22. @markertap="onMarkerTap"
  23. :polyline="polylines"
  24. @loaded="onMapLoaded"
  25. :include-points="showAllMarkers ? includePoints : []"
  26. ></map>
  27. <!-- 地图上的搜索框 -->
  28. <view class="search-box">
  29. <view class="search-icon">🔍</view>
  30. <input
  31. class="search-input"
  32. type="text"
  33. placeholder="搜索景点、酒店、商场等"
  34. v-model="searchKeyword"
  35. @confirm="searchLocation"
  36. />
  37. </view>
  38. <!-- 测试按钮 -->
  39. <view class="debug-panel">
  40. <view class="debug-btn" @tap="addTestMarkerAndShow">
  41. <text>添加测试标记</text>
  42. </view>
  43. <view class="debug-btn" @tap="showMarkersInfo">
  44. <text>标记信息</text>
  45. </view>
  46. <view class="debug-btn" @tap="reloadMarkers">
  47. <text>重载标记</text>
  48. </view>
  49. <view class="debug-btn" @tap="showAllSpots">
  50. <text>显示所有景点</text>
  51. </view>
  52. <view class="debug-btn" @tap="resetView" v-if="showAllMarkers">
  53. <text>重置视图</text>
  54. </view>
  55. </view>
  56. <!-- 底部操作区 -->
  57. <view class="bottom-panel">
  58. <view class="panel-header">
  59. <text class="panel-title">添加到行程</text>
  60. <text class="panel-subtitle">已选择 {{selectedLocations.length}} 个地点</text>
  61. </view>
  62. <!-- 已选择的地点列表 -->
  63. <scroll-view class="location-list" scroll-y="true" v-if="selectedLocations.length > 0">
  64. <view
  65. class="location-item"
  66. v-for="(item, index) in selectedLocations"
  67. :key="index"
  68. :class="{'active': currentIndex === index}"
  69. @tap="selectLocation(index)"
  70. >
  71. <view class="location-index">{{index + 1}}</view>
  72. <view class="location-info">
  73. <text class="location-name">{{item.name}}</text>
  74. <text class="location-address">{{item.address}}</text>
  75. </view>
  76. <view class="location-actions">
  77. <view class="action-btn delete-btn" @tap.stop="removeLocation(index)">
  78. <text class="action-icon">×</text>
  79. </view>
  80. </view>
  81. </view>
  82. </scroll-view>
  83. <!-- 没有选择地点时的提示 -->
  84. <view class="empty-state" v-else>
  85. <view class="empty-icon">📍</view>
  86. <text class="empty-text">点击地图或搜索添加地点</text>
  87. </view>
  88. <!-- 底部按钮 -->
  89. <view class="action-buttons">
  90. <view class="action-btn secondary" @tap="clearAllLocations">
  91. <text>清空</text>
  92. </view>
  93. <view class="action-btn primary" @tap="saveTrip" :class="{'disabled': selectedLocations.length === 0}">
  94. <text>生成行程</text>
  95. </view>
  96. </view>
  97. </view>
  98. </view>
  99. </view>
  100. </template>
  101. <script>
  102. // 导入景点查询API
  103. import { findalls } from '@/pages/api/luyou.js';
  104. export default {
  105. data() {
  106. return {
  107. latitude: 38.818143, // 保定理工的纬度
  108. longitude: 115.484621, // 保定理工的经度
  109. scale: 14,
  110. markers: [],
  111. polylines: [],
  112. includePoints: [], // 添加要包含在地图视野内的点
  113. showAllMarkers: false, // 是否显示所有景点
  114. searchKeyword: '',
  115. selectedLocations: [],
  116. currentIndex: -1,
  117. jingdian: [], // 存储景点数据
  118. targetSpotName: '', // 目标景点名称
  119. defaultBaodingSpots: [
  120. {
  121. id: 999,
  122. name: '保定古莲花池',
  123. pinyin: 'baodinggulianhuchi',
  124. description: '始建于唐代,是保定市著名的风景名胜区,国家AAAA级旅游景区',
  125. address: '河北省保定市莲池区牌坊街67号',
  126. city: '保定',
  127. category: '景区',
  128. latitude: 38.8743,
  129. longitude: 115.4646,
  130. image: '/static/default_attraction.jpg'
  131. },
  132. {
  133. id: 998,
  134. name: '保定直隶总督署',
  135. pinyin: 'baodingzhilizongedushu',
  136. description: '中国保存最完整的清代省级衙署,是河北省重点文物保护单位',
  137. address: '河北省保定市莲池区东风路1399号',
  138. city: '保定',
  139. category: '景区',
  140. latitude: 38.87346,
  141. longitude: 115.47486,
  142. image: '/static/default_attraction.jpg'
  143. },
  144. {
  145. id: 997,
  146. name: '保定野三坡景区',
  147. pinyin: 'baodingyesanpo',
  148. description: '国家AAAAA级景区,以奇峰、怪石、峡谷、溶洞和清泉著称',
  149. address: '河北省保定市涞水县野三坡镇',
  150. city: '保定',
  151. category: '景区',
  152. latitude: 39.46082,
  153. longitude: 115.61501,
  154. image: '/static/default_attraction.jpg'
  155. }
  156. ]
  157. }
  158. },
  159. onLoad(options) {
  160. // 检查是否有景点参数传入
  161. if (options.lat && options.lng && options.name) {
  162. this.latitude = parseFloat(options.lat);
  163. this.longitude = parseFloat(options.lng);
  164. this.targetSpotName = decodeURIComponent(options.name);
  165. this.scale = 15; // 放大一点
  166. console.log(`接收到景点定位参数: ${this.targetSpotName}, 坐标: ${this.latitude}, ${this.longitude}`);
  167. // 显示提示
  168. uni.showToast({
  169. title: `定位到: ${this.targetSpotName}`,
  170. icon: 'none',
  171. duration: 2000
  172. });
  173. }
  174. // 初始化地图
  175. this.initMap();
  176. // 查询景点数据并显示加载状态
  177. uni.showLoading({
  178. title: '加载景点数据...'
  179. });
  180. // 先加载默认保定景点
  181. this.setDefaultBaodingSpots();
  182. // 再查询API获取所有景点
  183. this.getJingdian();
  184. },
  185. methods: {
  186. // 设置默认保定景点
  187. setDefaultBaodingSpots() {
  188. // 预先设置默认的保定景点
  189. this.jingdian = [...this.defaultBaodingSpots];
  190. // 设置地图中心为第一个景点
  191. this.latitude = this.defaultBaodingSpots[0].latitude;
  192. this.longitude = this.defaultBaodingSpots[0].longitude;
  193. // 创建地图标记
  194. this.createMarkersFromJingdian();
  195. },
  196. // 查询景点数据
  197. getJingdian() {
  198. // 调用景点查询API
  199. findalls().then((res) => {
  200. console.log('API返回原始数据:', JSON.stringify(res));
  201. // 隐藏加载状态
  202. uni.hideLoading();
  203. if (res && res.code === 200) {
  204. // 保存API返回的原始数据
  205. this.apiResponse = res;
  206. // 确保获取到正确的数据位置
  207. let data = null;
  208. // 检查各种可能的数据位置
  209. if (res.obj) {
  210. data = res.obj;
  211. console.log('数据在res.obj中, 长度:', data.length);
  212. } else if (res.data && res.data.obj) {
  213. data = res.data.obj;
  214. console.log('数据在res.data.obj中, 长度:', data.length);
  215. } else if (res.data) {
  216. data = res.data;
  217. console.log('数据在res.data中, 长度:', data.length);
  218. } else {
  219. console.log('尝试解析完整响应');
  220. data = res;
  221. }
  222. let apiJingdian = [];
  223. // 如果数据是数组,则直接使用
  224. if (Array.isArray(data)) {
  225. apiJingdian = data;
  226. console.log('直接使用数组数据, 长度:', apiJingdian.length);
  227. } else {
  228. // 尝试找到数据中的数组
  229. for (const key in data) {
  230. if (Array.isArray(data[key]) && data[key].length > 0) {
  231. apiJingdian = data[key];
  232. console.log(`找到数组数据在${key}字段中, 长度:`, apiJingdian.length);
  233. break;
  234. }
  235. }
  236. }
  237. if (apiJingdian && apiJingdian.length > 0) {
  238. // 处理后端返回的景点数据
  239. const processedApiData = apiJingdian.map(item => {
  240. // 确保每个景点都有有效的坐标
  241. if (!item.latitude || !item.longitude) {
  242. console.log(`景点${item.name || '未命名'}没有坐标,尝试解析其他字段`);
  243. // 尝试从其他可能的字段中获取坐标
  244. if (item.latitudeStr && item.longitudeStr) {
  245. item.latitude = parseFloat(item.latitudeStr);
  246. item.longitude = parseFloat(item.longitudeStr);
  247. console.log(`从latitudeStr/longitudeStr字段获取到坐标: ${item.latitude}, ${item.longitude}`);
  248. } else if (item.lat && item.lng) {
  249. item.latitude = parseFloat(item.lat);
  250. item.longitude = parseFloat(item.lng);
  251. console.log(`从lat/lng字段获取到坐标: ${item.latitude}, ${item.longitude}`);
  252. }
  253. }
  254. // 确保坐标为数字类型
  255. if (item.latitude) item.latitude = Number(item.latitude);
  256. if (item.longitude) item.longitude = Number(item.longitude);
  257. return item;
  258. }).filter(item => {
  259. // 过滤掉没有有效坐标的景点
  260. const hasValidCoords = item.latitude && item.longitude &&
  261. !isNaN(Number(item.latitude)) && !isNaN(Number(item.longitude));
  262. if (!hasValidCoords) {
  263. console.log(`过滤掉无效坐标的景点: ${item.name || '未命名'}`);
  264. }
  265. return hasValidCoords;
  266. });
  267. console.log(`处理后的有效API景点数据: ${processedApiData.length}个`);
  268. // 合并数据(同时保持默认景点)
  269. this.jingdian = [...this.defaultBaodingSpots, ...processedApiData];
  270. console.log('合并后总景点数据:', this.jingdian.length);
  271. if (this.jingdian.length > 0) {
  272. console.log('第一条数据:', JSON.stringify(this.jingdian[0]));
  273. }
  274. uni.showToast({
  275. title: `成功加载${this.jingdian.length}个景点`,
  276. icon: 'success'
  277. });
  278. // 从景点数据创建地图标记
  279. this.createMarkersFromJingdian();
  280. // 检查是否需要选中目标景点
  281. if (this.targetSpotName) {
  282. this.selectTargetSpot();
  283. }
  284. // 调整地图视野以包含所有景点
  285. this.adjustMapViewToShowAllSpots();
  286. } else {
  287. console.log('API未返回有效景点数据,继续使用默认景点');
  288. uni.showToast({
  289. title: '使用默认景点数据',
  290. icon: 'none'
  291. });
  292. // 即使使用默认景点,也尝试选中目标景点
  293. if (this.targetSpotName) {
  294. this.selectTargetSpot();
  295. }
  296. }
  297. } else {
  298. console.error('API返回错误:', res);
  299. uni.showToast({
  300. title: res && res.message ? res.message : '查询失败,使用默认景点',
  301. icon: 'none'
  302. });
  303. }
  304. }).catch((error) => {
  305. uni.hideLoading();
  306. console.error('查询失败:', error);
  307. uni.showToast({
  308. title: '加载API景点失败,使用默认景点',
  309. icon: 'none'
  310. });
  311. });
  312. },
  313. // 调整地图视野以包含所有景点
  314. adjustMapViewToShowAllSpots() {
  315. if (!this.jingdian || this.jingdian.length === 0) return;
  316. console.log('调整地图视野以显示所有景点');
  317. // 保持当前的缩放级别,不要自动缩小
  318. // this.scale = 10; // 移除这行,不自动改变缩放级别
  319. // 找出所有景点的地理中心
  320. let latSum = 0;
  321. let lngSum = 0;
  322. let validSpots = 0;
  323. this.jingdian.forEach(spot => {
  324. if (spot.latitude && spot.longitude) {
  325. latSum += Number(spot.latitude);
  326. lngSum += Number(spot.longitude);
  327. validSpots++;
  328. }
  329. });
  330. if (validSpots > 0) {
  331. const centerLat = latSum / validSpots;
  332. const centerLng = lngSum / validSpots;
  333. console.log(`设置地图中心点为所有景点的地理中心: ${centerLat}, ${centerLng}`);
  334. this.latitude = centerLat;
  335. this.longitude = centerLng;
  336. }
  337. },
  338. // 从景点数据创建标记
  339. createMarkersFromJingdian() {
  340. // 检查景点数据
  341. if (!this.jingdian || this.jingdian.length === 0) {
  342. console.log('景点数据为空');
  343. uni.showToast({
  344. title: '景点数据为空',
  345. icon: 'none'
  346. });
  347. return;
  348. }
  349. console.log('开始创建标记,景点数据长度:', this.jingdian.length);
  350. // 先清空当前标记
  351. let currentMarkers = [...this.markers];
  352. // 将景点数据转换为标记数据
  353. const poiMarkers = this.jingdian.filter(poi => {
  354. if (!poi) {
  355. console.log('发现无效的景点数据(null或undefined)');
  356. return false;
  357. }
  358. const hasCoords = poi.latitude && poi.longitude;
  359. if (!hasCoords) {
  360. console.log(`景点${poi.name || '未命名'}没有有效的坐标`);
  361. return false;
  362. }
  363. return true;
  364. }).map((poi, index) => {
  365. try {
  366. const lat = Number(poi.latitude);
  367. const lng = Number(poi.longitude);
  368. console.log(`处理景点 ID=${poi.id}, 名称=${poi.name}, 坐标: ${lat}, ${lng}`);
  369. // 检查坐标是否为NaN
  370. if (isNaN(lat) || isNaN(lng)) {
  371. console.log(`景点${poi.name}的坐标转换为数字后不有效`);
  372. return null;
  373. }
  374. // 获取标记图标
  375. const iconType = poi.category || poi.type || 'attraction';
  376. const iconPath = this.getMarkerIcon(iconType);
  377. return {
  378. id: poi.id || index + 1,
  379. latitude: lat,
  380. longitude: lng,
  381. title: poi.name || poi.title,
  382. iconPath: iconPath,
  383. width: 36, // 增大标记尺寸以提高可见度
  384. height: 36,
  385. callout: {
  386. content: poi.name || poi.title,
  387. color: '#333333',
  388. fontSize: 12,
  389. borderRadius: 4,
  390. padding: 5,
  391. display: 'BYCLICK'
  392. }
  393. };
  394. } catch (error) {
  395. console.error(`处理景点${poi.name || '未命名'}时出错:`, error);
  396. return null;
  397. }
  398. }).filter(marker => marker !== null); // 过滤掉无效的标记
  399. console.log(`生成了${poiMarkers.length}个景点标记`);
  400. if (poiMarkers.length > 0) {
  401. console.log('示例标记:', JSON.stringify(poiMarkers[0]));
  402. } else {
  403. console.log('没有有效的景点标记生成');
  404. // 添加一个测试标记
  405. this.addTestMarker();
  406. return;
  407. }
  408. // 构建已选择的标记
  409. const selectedMarkers = this.selectedLocations.map((location, index) => {
  410. return {
  411. id: 1000 + index,
  412. latitude: Number(location.latitude),
  413. longitude: Number(location.longitude),
  414. title: location.name,
  415. iconPath: '/static/marker_selected.png', // 使用绝对路径
  416. width: 40,
  417. height: 40,
  418. label: {
  419. content: (index + 1).toString(),
  420. color: '#FFFFFF',
  421. fontSize: 14,
  422. textAlign: 'center',
  423. anchorX: 0,
  424. anchorY: -10
  425. }
  426. };
  427. });
  428. // 合并景点标记和已选择的标记
  429. this.markers = [...poiMarkers, ...selectedMarkers];
  430. console.log(`总共有${this.markers.length}个标记点`);
  431. // 延迟执行检查标记
  432. setTimeout(() => {
  433. this.checkMarkersVisibility();
  434. }, 1000);
  435. },
  436. // 检查标记可见性
  437. checkMarkersVisibility() {
  438. console.log('检查标记可见性');
  439. if (this.markers.length > 0) {
  440. console.log('标记数量:', this.markers.length);
  441. console.log('地图中心坐标:', this.latitude, this.longitude);
  442. // 如果没有可见的标记,尝试将地图中心移到第一个标记
  443. const firstMarker = this.markers[0];
  444. if (firstMarker && firstMarker.latitude && firstMarker.longitude) {
  445. console.log('移动地图到第一个标记:', firstMarker.latitude, firstMarker.longitude);
  446. this.latitude = firstMarker.latitude;
  447. this.longitude = firstMarker.longitude;
  448. this.scale = 14; // 调整缩放级别
  449. }
  450. }
  451. },
  452. // 初始化地图
  453. initMap() {
  454. // 获取当前位置
  455. uni.getLocation({
  456. type: 'gcj02',
  457. success: (res) => {
  458. this.latitude = res.latitude;
  459. this.longitude = res.longitude;
  460. },
  461. fail: () => {
  462. // 定位失败时使用默认坐标
  463. uni.showToast({
  464. title: '获取位置信息失败,使用默认位置',
  465. icon: 'none'
  466. });
  467. }
  468. });
  469. },
  470. // 根据POI类型获取不同的图标
  471. getMarkerIcon(type) {
  472. // 获取当前运行平台
  473. const systemInfo = uni.getSystemInfoSync();
  474. const platform = systemInfo.platform;
  475. console.log('当前运行平台:', platform);
  476. // 由于微信小程序的限制,使用特定格式的图标URL
  477. const baseUrl = '/static/';
  478. let iconName;
  479. switch(type) {
  480. case '景区':
  481. case '著名景点':
  482. case '历史遗迹':
  483. case 'attraction':
  484. iconName = 'marker_attraction.png';
  485. break;
  486. case '酒店':
  487. case '住宿':
  488. case 'hotel':
  489. iconName = 'marker_hotel.png';
  490. break;
  491. case '美食':
  492. case '餐厅':
  493. case 'restaurant':
  494. iconName = 'marker_restaurant.png';
  495. break;
  496. case '购物':
  497. case '商场':
  498. case 'shopping':
  499. iconName = 'marker_shopping.png';
  500. break;
  501. default:
  502. iconName = 'marker_default.png';
  503. }
  504. const iconPath = baseUrl + iconName;
  505. console.log(`选择的图标路径: ${iconPath}, 类型: ${type}`);
  506. return iconPath;
  507. },
  508. // 搜索位置
  509. searchLocation() {
  510. if (!this.searchKeyword.trim()) {
  511. return;
  512. }
  513. // 使用API数据进行搜索
  514. if (!this.jingdian || this.jingdian.length === 0) {
  515. uni.showToast({
  516. title: '景点数据未加载',
  517. icon: 'none'
  518. });
  519. return;
  520. }
  521. const result = this.jingdian.find(poi =>
  522. (poi.name || '').includes(this.searchKeyword) ||
  523. (poi.address || '').includes(this.searchKeyword) ||
  524. (poi.description || '').includes(this.searchKeyword)
  525. );
  526. if (result && result.latitude && result.longitude) {
  527. this.latitude = Number(result.latitude);
  528. this.longitude = Number(result.longitude);
  529. this.scale = 15;
  530. uni.showToast({
  531. title: `找到: ${result.name}`,
  532. icon: 'none'
  533. });
  534. } else {
  535. uni.showToast({
  536. title: '未找到相关位置',
  537. icon: 'none'
  538. });
  539. }
  540. },
  541. // 点击标记
  542. onMarkerTap(e) {
  543. const markerId = e.markerId;
  544. console.log('点击了标记:', markerId);
  545. // 处理选中的已添加地点标记
  546. if (markerId >= 1000) {
  547. const index = markerId - 1000;
  548. if (index >= 0 && index < this.selectedLocations.length) {
  549. this.currentIndex = index;
  550. return;
  551. }
  552. }
  553. // 使用API数据查找POI
  554. const poi = this.jingdian.find(item => item.id === markerId);
  555. if (poi) {
  556. // 构建景点详情内容
  557. let content = '';
  558. if (poi.description) {
  559. content += `描述: ${poi.description}\n`;
  560. }
  561. if (poi.address) {
  562. content += `地址: ${poi.address}\n`;
  563. }
  564. if (poi.category) {
  565. content += `类型: ${poi.category}\n`;
  566. }
  567. if (poi.city) {
  568. content += `城市: ${poi.city}\n`;
  569. }
  570. content += '是否添加到行程?';
  571. uni.showModal({
  572. title: poi.name,
  573. content: content,
  574. confirmText: '添加',
  575. cancelText: '取消',
  576. success: (res) => {
  577. if (res.confirm) {
  578. // 标准化POI数据格式
  579. const location = {
  580. id: poi.id,
  581. name: poi.name,
  582. latitude: Number(poi.latitude),
  583. longitude: Number(poi.longitude),
  584. address: poi.address || poi.city || '',
  585. type: poi.category || 'attraction',
  586. description: poi.description || ''
  587. };
  588. this.addLocation(location);
  589. }
  590. }
  591. });
  592. }
  593. },
  594. // 添加地点到行程
  595. addLocation(location) {
  596. // 检查是否已经添加过
  597. const exists = this.selectedLocations.some(item => item.id === location.id);
  598. if (exists) {
  599. uni.showToast({
  600. title: '该地点已在行程中',
  601. icon: 'none'
  602. });
  603. return;
  604. }
  605. // 确保经纬度是数值类型
  606. const standardizedLocation = {
  607. ...location,
  608. latitude: Number(location.latitude),
  609. longitude: Number(location.longitude)
  610. };
  611. console.log('添加标准化后的地点:', standardizedLocation);
  612. this.selectedLocations.push(standardizedLocation);
  613. this.currentIndex = this.selectedLocations.length - 1;
  614. this.updateSelectedMarkers();
  615. uni.showToast({
  616. title: '已添加到行程',
  617. icon: 'success'
  618. });
  619. },
  620. // 移除地点
  621. removeLocation(index) {
  622. this.selectedLocations.splice(index, 1);
  623. if (this.currentIndex >= this.selectedLocations.length) {
  624. this.currentIndex = this.selectedLocations.length - 1;
  625. }
  626. // 更新地图标记
  627. this.updateSelectedMarkers();
  628. },
  629. // 选择地点
  630. selectLocation(index) {
  631. this.currentIndex = index;
  632. const location = this.selectedLocations[index];
  633. // 移动地图到选中的位置
  634. this.latitude = location.latitude;
  635. this.longitude = location.longitude;
  636. },
  637. // 清空所有地点
  638. clearAllLocations() {
  639. if (this.selectedLocations.length === 0) return;
  640. uni.showModal({
  641. title: '确认清空',
  642. content: '确定要清空所有已添加的地点吗?',
  643. success: (res) => {
  644. if (res.confirm) {
  645. this.selectedLocations = [];
  646. this.currentIndex = -1;
  647. this.updateSelectedMarkers();
  648. }
  649. }
  650. });
  651. },
  652. // 保存行程
  653. saveTrip() {
  654. if (this.selectedLocations.length === 0) {
  655. uni.showToast({
  656. title: '请至少添加一个地点',
  657. icon: 'none'
  658. });
  659. return;
  660. }
  661. // 将选中的位置保存到本地存储中
  662. try {
  663. uni.setStorageSync('selectedLocations', JSON.stringify(this.selectedLocations));
  664. // 直接跳转到计划详情页面,不再显示弹窗
  665. uni.navigateTo({
  666. url: '/pages/custom-trip/plan-detail',
  667. success: () => {
  668. console.log('成功跳转到计划详情页');
  669. },
  670. fail: (err) => {
  671. console.error('跳转到计划详情页失败:', err);
  672. uni.showModal({
  673. title: '跳转失败',
  674. content: JSON.stringify(err),
  675. showCancel: false
  676. });
  677. }
  678. });
  679. } catch (e) {
  680. console.error('保存位置数据失败:', e);
  681. uni.showToast({
  682. title: '操作失败,请重试',
  683. icon: 'none'
  684. });
  685. }
  686. },
  687. // 返回上一页
  688. goBack() {
  689. uni.navigateBack();
  690. },
  691. // 更新选中的标记
  692. updateSelectedMarkers() {
  693. // 重新生成地图标记
  694. this.createMarkersFromJingdian();
  695. // 更新路线连线
  696. this.updatePolylines();
  697. },
  698. // 更新路线连线
  699. updatePolylines() {
  700. if (this.selectedLocations.length < 2) {
  701. this.polylines = [];
  702. this.markers = this.markers.filter(marker => marker.id < 2000 || marker.id >= 3000); // 移除距离标记
  703. return;
  704. }
  705. // 提取所有选中位置的坐标点
  706. const points = this.selectedLocations.map(location => {
  707. return {
  708. latitude: Number(location.latitude),
  709. longitude: Number(location.longitude)
  710. };
  711. });
  712. // 创建路线
  713. this.polylines = [{
  714. points: points,
  715. color: '#1aad19',
  716. width: 4,
  717. dottedLine: false,
  718. arrowLine: true,
  719. borderColor: '#ffffff',
  720. borderWidth: 1
  721. }];
  722. // 创建距离标记
  723. let distanceMarkers = [];
  724. for (let i = 0; i < points.length - 1; i++) {
  725. const startPoint = points[i];
  726. const endPoint = points[i + 1];
  727. // 计算两点之间的距离(单位:米)
  728. const distance = this.calculateDistance(
  729. startPoint.latitude,
  730. startPoint.longitude,
  731. endPoint.latitude,
  732. endPoint.longitude
  733. );
  734. // 显示格式:小于1公里显示米,大于等于1公里显示公里
  735. const distanceText = distance < 1000
  736. ? `${Math.round(distance)}米`
  737. : `${(distance / 1000).toFixed(1)}公里`;
  738. // 计算中点位置
  739. const midPoint = {
  740. latitude: (startPoint.latitude + endPoint.latitude) / 2,
  741. longitude: (startPoint.longitude + endPoint.longitude) / 2
  742. };
  743. // 创建距离标记
  744. distanceMarkers.push({
  745. id: 2000 + i, // 距离标记ID从2000开始
  746. latitude: midPoint.latitude,
  747. longitude: midPoint.longitude,
  748. iconPath: '/static/marker_default.png', // 使用默认图标
  749. width: 0, // 设置为0使图标不可见
  750. height: 0, // 设置为0使图标不可见
  751. callout: {
  752. content: distanceText,
  753. color: '#333333',
  754. fontSize: 12,
  755. borderRadius: 4,
  756. borderWidth: 1,
  757. borderColor: '#1aad19',
  758. bgColor: '#ffffff',
  759. padding: 5,
  760. display: 'ALWAYS'
  761. }
  762. });
  763. }
  764. // 更新所有标记,保留原有标记并添加距离标记
  765. // 先过滤掉旧的距离标记
  766. const otherMarkers = this.markers.filter(marker => marker.id < 2000 || marker.id >= 3000);
  767. this.markers = [...otherMarkers, ...distanceMarkers];
  768. },
  769. // 计算两点之间的距离(使用Haversine公式)
  770. calculateDistance(lat1, lon1, lat2, lon2) {
  771. const R = 6371000; // 地球半径(米)
  772. const dLat = this.deg2rad(lat2 - lat1);
  773. const dLon = this.deg2rad(lon2 - lon1);
  774. const a =
  775. Math.sin(dLat/2) * Math.sin(dLat/2) +
  776. Math.cos(this.deg2rad(lat1)) * Math.cos(this.deg2rad(lat2)) *
  777. Math.sin(dLon/2) * Math.sin(dLon/2);
  778. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  779. return R * c; // 距离(米)
  780. },
  781. // 角度转弧度
  782. deg2rad(deg) {
  783. return deg * (Math.PI/180);
  784. },
  785. // 选择目标景点(从热门目的地跳转来的)
  786. selectTargetSpot() {
  787. if (!this.targetSpotName || this.jingdian.length === 0) {
  788. return;
  789. }
  790. // 在景点数据中查找匹配的景点
  791. const targetSpot = this.jingdian.find(spot =>
  792. spot.name === this.targetSpotName ||
  793. spot.name.includes(this.targetSpotName) ||
  794. (this.targetSpotName.includes(spot.name) && spot.name.length > 2)
  795. );
  796. if (targetSpot) {
  797. // 模拟点击该景点的marker
  798. setTimeout(() => {
  799. const e = { markerId: targetSpot.id };
  800. this.onMarkerTap(e);
  801. }, 1000);
  802. } else {
  803. console.log(`未找到匹配的目标景点: ${this.targetSpotName}`);
  804. }
  805. },
  806. // 地图加载完成后的处理
  807. onMapLoaded() {
  808. console.log('地图加载完成');
  809. // 检查当前标记数据
  810. if (this.markers && this.markers.length > 0) {
  811. console.log('当前地图标记数量:', this.markers.length);
  812. console.log('第一个标记示例:', JSON.stringify(this.markers[0]));
  813. } else {
  814. console.log('当前无地图标记');
  815. }
  816. // 尝试重新加载标记
  817. this.refreshMarkers();
  818. },
  819. // 刷新地图标记
  820. refreshMarkers() {
  821. // 确保所有坐标都是有效的数字格式
  822. const validMarkers = this.markers.map(marker => {
  823. // 深拷贝,避免直接修改原对象
  824. const newMarker = {...marker};
  825. // 确保经纬度是有效数字
  826. if (typeof newMarker.latitude === 'string') {
  827. newMarker.latitude = parseFloat(newMarker.latitude);
  828. }
  829. if (typeof newMarker.longitude === 'string') {
  830. newMarker.longitude = parseFloat(newMarker.longitude);
  831. }
  832. // 检查是否有效
  833. if (isNaN(newMarker.latitude) || isNaN(newMarker.longitude)) {
  834. console.log(`忽略无效标记: ID=${newMarker.id}, 标题=${newMarker.title}`);
  835. return null;
  836. }
  837. // 将标记的宽高稍微增大,提高可见度
  838. newMarker.width = 36;
  839. newMarker.height = 36;
  840. return newMarker;
  841. }).filter(m => m !== null);
  842. console.log(`刷新后有效标记数量: ${validMarkers.length}`);
  843. if (validMarkers.length > 0) {
  844. console.log('刷新后第一个标记:', JSON.stringify(validMarkers[0]));
  845. }
  846. // 更新标记
  847. this.markers = validMarkers;
  848. // 如果标记刷新后仍然为空,使用默认坐标
  849. if (validMarkers.length === 0) {
  850. console.log('没有有效标记,添加一个默认测试标记');
  851. this.addTestMarker();
  852. }
  853. },
  854. // 添加测试标记
  855. addTestMarker() {
  856. console.log('添加测试标记');
  857. // 创建一个简单的红色点标记(不使用图片)
  858. const testMarker = {
  859. id: 9999,
  860. latitude: 38.8743,
  861. longitude: 115.4646,
  862. title: '测试标记',
  863. width: 20,
  864. height: 20,
  865. iconPath: '/static/marker_default.png',
  866. callout: {
  867. content: '测试标记',
  868. color: '#333333',
  869. fontSize: 12,
  870. borderRadius: 4,
  871. padding: 5,
  872. display: 'ALWAYS'
  873. }
  874. };
  875. this.markers = [testMarker];
  876. console.log('添加了测试标记:', JSON.stringify(testMarker));
  877. // 更新includePoints以显示测试标记
  878. this.includePoints = [
  879. {
  880. latitude: testMarker.latitude,
  881. longitude: testMarker.longitude
  882. }
  883. ];
  884. },
  885. // 添加测试标记并显示
  886. addTestMarkerAndShow() {
  887. console.log('手动添加测试标记');
  888. // 保存当前的缩放级别
  889. const currentScale = this.scale;
  890. console.log('当前缩放级别:', currentScale);
  891. this.addTestMarker();
  892. // 移动到标记位置,但保持当前缩放级别
  893. setTimeout(() => {
  894. if (this.markers.length > 0) {
  895. const marker = this.markers[0];
  896. this.latitude = marker.latitude;
  897. this.longitude = marker.longitude;
  898. // 保持原来的缩放级别不变
  899. this.scale = currentScale;
  900. uni.showToast({
  901. title: '已添加测试标记',
  902. icon: 'success'
  903. });
  904. }
  905. }, 300);
  906. },
  907. // 显示标记信息
  908. showMarkersInfo() {
  909. if (this.markers.length > 0) {
  910. const info = {
  911. 总标记数: this.markers.length,
  912. 第一个标记: this.markers[0]
  913. };
  914. uni.showModal({
  915. title: '标记信息',
  916. content: JSON.stringify(info, null, 2),
  917. showCancel: false
  918. });
  919. console.log('当前标记信息:', info);
  920. } else {
  921. uni.showToast({
  922. title: '当前没有标记',
  923. icon: 'none'
  924. });
  925. }
  926. },
  927. // 重新加载标记
  928. reloadMarkers() {
  929. console.log('重新加载标记');
  930. // 清空当前标记
  931. this.markers = [];
  932. // 重新加载
  933. this.createMarkersFromJingdian();
  934. uni.showToast({
  935. title: `已重载${this.markers.length}个标记`,
  936. icon: 'success'
  937. });
  938. },
  939. // 显示所有景点(手动方法)
  940. showAllSpots() {
  941. // 询问用户是否调整视图以显示所有景点
  942. uni.showModal({
  943. title: '显示所有景点',
  944. content: '调整地图视图以显示所有景点?这会改变地图缩放级别。',
  945. success: (res) => {
  946. if (res.confirm) {
  947. // 用户确认后才调整地图
  948. // 设置一个较小的缩放级别,以显示更多景点
  949. this.scale = 9;
  950. // 更新include-points以包含所有景点
  951. this.includePoints = this.markers.map(marker => ({
  952. latitude: marker.latitude,
  953. longitude: marker.longitude
  954. }));
  955. // 激活显示所有景点的模式
  956. this.showAllMarkers = true;
  957. // 移动到所有景点的中心
  958. if (this.markers.length > 0) {
  959. let latSum = 0, lngSum = 0;
  960. this.markers.forEach(marker => {
  961. latSum += marker.latitude;
  962. lngSum += marker.longitude;
  963. });
  964. this.latitude = latSum / this.markers.length;
  965. this.longitude = lngSum / this.markers.length;
  966. uni.showToast({
  967. title: '已显示所有景点',
  968. icon: 'success'
  969. });
  970. }
  971. }
  972. }
  973. });
  974. },
  975. // 重置地图视图
  976. resetView() {
  977. this.showAllMarkers = false;
  978. this.scale = 14; // 恢复默认缩放级别
  979. // 如果有默认景点,移动到第一个默认景点
  980. if (this.defaultBaodingSpots.length > 0) {
  981. this.latitude = this.defaultBaodingSpots[0].latitude;
  982. this.longitude = this.defaultBaodingSpots[0].longitude;
  983. }
  984. uni.showToast({
  985. title: '已重置地图视图',
  986. icon: 'success'
  987. });
  988. }
  989. }
  990. }
  991. </script>
  992. <style>
  993. .map-container {
  994. display: flex;
  995. flex-direction: column;
  996. height: 100vh;
  997. background-color: #f8f9fa;
  998. }
  999. /* 顶部导航栏 */
  1000. .navbar {
  1001. display: flex;
  1002. align-items: center;
  1003. justify-content: space-between;
  1004. padding: 50rpx 30rpx 20rpx;
  1005. background-color: #ffffff;
  1006. position: relative;
  1007. z-index: 10;
  1008. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  1009. }
  1010. .back-btn {
  1011. display: flex;
  1012. align-items: center;
  1013. padding: 10rpx 20rpx;
  1014. border-radius: 30rpx;
  1015. background-color: rgba(0, 0, 0, 0.05);
  1016. transition: all 0.2s ease;
  1017. }
  1018. .back-btn:active {
  1019. background-color: rgba(0, 0, 0, 0.1);
  1020. }
  1021. .back-icon {
  1022. font-size: 32rpx;
  1023. color: #333;
  1024. font-weight: bold;
  1025. margin-right: 5rpx;
  1026. }
  1027. .back-text {
  1028. font-size: 28rpx;
  1029. color: #333;
  1030. }
  1031. .title {
  1032. font-size: 32rpx;
  1033. font-weight: bold;
  1034. color: #333;
  1035. position: absolute;
  1036. left: 50%;
  1037. transform: translateX(-50%);
  1038. }
  1039. /* 地图视图 */
  1040. .map-view {
  1041. flex: 1;
  1042. position: relative;
  1043. overflow: hidden;
  1044. }
  1045. .map {
  1046. width: 100%;
  1047. height: 100%;
  1048. }
  1049. /* 搜索框 */
  1050. .search-box {
  1051. position: absolute;
  1052. top: 20rpx;
  1053. left: 30rpx;
  1054. right: 30rpx;
  1055. height: 80rpx;
  1056. background-color: #fff;
  1057. border-radius: 40rpx;
  1058. display: flex;
  1059. align-items: center;
  1060. padding: 0 30rpx;
  1061. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  1062. z-index: 100;
  1063. }
  1064. .search-icon {
  1065. font-size: 36rpx;
  1066. margin-right: 20rpx;
  1067. color: #999;
  1068. }
  1069. .search-input {
  1070. flex: 1;
  1071. height: 80rpx;
  1072. font-size: 28rpx;
  1073. }
  1074. /* 测试按钮 */
  1075. .debug-panel {
  1076. position: absolute;
  1077. top: 20rpx;
  1078. right: 30rpx;
  1079. display: flex;
  1080. flex-direction: column;
  1081. gap: 10rpx;
  1082. z-index: 100;
  1083. }
  1084. .debug-btn {
  1085. padding: 10rpx 15rpx;
  1086. border-radius: 10rpx;
  1087. background-color: rgba(255, 255, 255, 0.8);
  1088. box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.1);
  1089. transition: all 0.2s ease;
  1090. text-align: center;
  1091. }
  1092. .debug-btn:active {
  1093. background-color: rgba(0, 0, 0, 0.1);
  1094. }
  1095. .debug-btn text {
  1096. font-size: 28rpx;
  1097. color: #333;
  1098. }
  1099. /* 底部面板 */
  1100. .bottom-panel {
  1101. position: absolute;
  1102. bottom: 0;
  1103. left: 0;
  1104. right: 0;
  1105. background-color: #fff;
  1106. border-radius: 30rpx 30rpx 0 0;
  1107. padding: 30rpx;
  1108. box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
  1109. z-index: 100;
  1110. display: flex;
  1111. flex-direction: column;
  1112. max-height: 60vh;
  1113. }
  1114. .panel-header {
  1115. display: flex;
  1116. justify-content: space-between;
  1117. align-items: center;
  1118. margin-bottom: 20rpx;
  1119. }
  1120. .panel-title {
  1121. font-size: 32rpx;
  1122. font-weight: bold;
  1123. color: #333;
  1124. }
  1125. .panel-subtitle {
  1126. font-size: 24rpx;
  1127. color: #999;
  1128. }
  1129. /* 地点列表 */
  1130. .location-list {
  1131. max-height: 40vh;
  1132. margin-bottom: 20rpx;
  1133. }
  1134. .location-item {
  1135. display: flex;
  1136. align-items: center;
  1137. padding: 20rpx;
  1138. margin-bottom: 10rpx;
  1139. background-color: #f8f9fa;
  1140. border-radius: 10rpx;
  1141. position: relative;
  1142. }
  1143. .location-item.active {
  1144. background-color: #e6f7ff;
  1145. }
  1146. .location-index {
  1147. width: 40rpx;
  1148. height: 40rpx;
  1149. border-radius: 50%;
  1150. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  1151. color: #fff;
  1152. display: flex;
  1153. justify-content: center;
  1154. align-items: center;
  1155. font-size: 24rpx;
  1156. font-weight: bold;
  1157. margin-right: 15rpx;
  1158. }
  1159. .location-info {
  1160. flex: 1;
  1161. }
  1162. .location-name {
  1163. font-size: 28rpx;
  1164. font-weight: bold;
  1165. color: #333;
  1166. margin-bottom: 5rpx;
  1167. }
  1168. .location-address {
  1169. font-size: 24rpx;
  1170. color: #999;
  1171. }
  1172. .location-actions {
  1173. display: flex;
  1174. }
  1175. .action-btn {
  1176. width: 60rpx;
  1177. height: 60rpx;
  1178. display: flex;
  1179. justify-content: center;
  1180. align-items: center;
  1181. }
  1182. .delete-btn {
  1183. color: #ff4d4f;
  1184. }
  1185. .action-icon {
  1186. font-size: 36rpx;
  1187. }
  1188. /* 空状态 */
  1189. .empty-state {
  1190. display: flex;
  1191. flex-direction: column;
  1192. align-items: center;
  1193. justify-content: center;
  1194. padding: 40rpx 0;
  1195. }
  1196. .empty-icon {
  1197. font-size: 60rpx;
  1198. margin-bottom: 20rpx;
  1199. }
  1200. .empty-text {
  1201. font-size: 28rpx;
  1202. color: #999;
  1203. }
  1204. /* 底部按钮 */
  1205. .action-buttons {
  1206. display: flex;
  1207. justify-content: space-between;
  1208. margin-top: 20rpx;
  1209. }
  1210. .action-btn {
  1211. height: 80rpx;
  1212. border-radius: 40rpx;
  1213. display: flex;
  1214. justify-content: center;
  1215. align-items: center;
  1216. font-size: 28rpx;
  1217. transition: all 0.3s ease;
  1218. }
  1219. .secondary {
  1220. width: 30%;
  1221. background-color: #f0f0f0;
  1222. color: #666;
  1223. }
  1224. .secondary:active {
  1225. background-color: #e0e0e0;
  1226. }
  1227. .primary {
  1228. width: 65%;
  1229. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  1230. color: #fff;
  1231. box-shadow: 0 4rpx 15rpx rgba(79, 172, 254, 0.4);
  1232. }
  1233. .primary:active {
  1234. transform: translateY(2rpx);
  1235. box-shadow: 0 2rpx 8rpx rgba(79, 172, 254, 0.3);
  1236. }
  1237. .disabled {
  1238. opacity: 0.6;
  1239. }
  1240. </style>