navigation.ts 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. // navigation.ts:手机端导航组件
  2. // 功能描述:实现符合大厂UI规范的导航界面,包含两种状态切换:底部状态和顶部状态
  3. // 集成高德地图API,实现地图展示、控件添加、输入提示和IP定位功能
  4. import {Component, ElementRef, ViewChild, AfterViewInit, signal, output, ChangeDetectorRef, Inject} from '@angular/core';
  5. import {CommonModule} from '@angular/common';
  6. import {FormsModule} from '@angular/forms';
  7. // 声明AMap全局变量以提供TypeScript支持
  8. declare const AMap: any;
  9. /**
  10. * NavigationComponent:手机端导航组件
  11. * 功能描述:
  12. * - 实现符合大厂UI规范的导航界面,支持底部和顶部两种输入框状态切换
  13. * - 集成高德地图API,提供地图展示、搜索提示、路线规划等功能
  14. * - 支持IP定位、HTML5定位,实现精确的位置服务
  15. * - 优化搜索体验,包含历史记录、距离计算、搜索结果排序等功能
  16. */
  17. @Component({
  18. selector: 'app-navigation',
  19. standalone: true,
  20. imports: [CommonModule, FormsModule],
  21. templateUrl: './navigation.html',
  22. styleUrl: './navigation.scss'
  23. })
  24. export class Navigation implements AfterViewInit {
  25. /** 控制输入框位置状态:true表示在顶部,false表示在底部 */
  26. isInputBoxAtTop = signal(false);
  27. /** 搜索关键词双向绑定 */
  28. searchKeyword = '';
  29. /** 搜索提示结果数组 */
  30. searchSuggestions: any[] = [];
  31. /** 历史记录数据,存储用户最近搜索的地址 */
  32. historyItems = [
  33. '北京市朝阳区',
  34. '上海市浦东新区',
  35. '广州市天河区',
  36. '深圳市南山区',
  37. '杭州市西湖区'
  38. ];
  39. /** 获取顶部输入框容器元素引用 */
  40. @ViewChild('topInputContainer', {static: false}) topInputContainer!: ElementRef;
  41. /** 获取底部输入框容器元素引用 */
  42. @ViewChild('bottomInputContainer', {static: false}) bottomInputContainer!: ElementRef;
  43. /** 获取历史记录面板元素引用 */
  44. @ViewChild('historyPanel', {static: false}) historyPanel!: ElementRef;
  45. /** 获取顶部输入框元素引用 */
  46. @ViewChild('topInput', {static: false}) topInput!: ElementRef;
  47. /** 高德地图实例 */
  48. private map: any;
  49. /** 高德地图输入提示服务实例 */
  50. private autoComplete: any;
  51. /** 高德地图定位服务实例 */
  52. private geolocation: any;
  53. /** 当前城市名称 */
  54. private currentCity: string = '';
  55. /** 搜索防抖定时器 */
  56. private debounceTimer: any;
  57. /** 存储地图上的定位标记点 */
  58. private locationMarker: any = null;
  59. /** 控制路线按钮是否显示 */
  60. showRouteButton = false;
  61. /** 控制路线规划面板是否显示 */
  62. showRoutePanel = false;
  63. /** 路线规划类型:driving(驾车), walking(步行), bus(公交) */
  64. routeType = 'driving';
  65. /** 路线规划结果数据 */
  66. routeResult: any = null;
  67. /** 当前选择的目标位置信息 */
  68. currentTarget: any = null;
  69. /** 当前位置坐标 - 设置默认值为北京坐标 */
  70. private currentLocation: any = [116.397428, 39.90923]; // 默认北京天安门坐标
  71. /**
  72. * 构造函数
  73. * @param cdr ChangeDetectorRef服务,用于手动触发变更检测
  74. */
  75. constructor(
  76. private cdr: ChangeDetectorRef,
  77. @Inject('AMAP_LOCATION_CONFIG') private config: any
  78. ) {
  79. // 在构造函数中初始化防抖定时器
  80. this.debounceTimer = null;
  81. }
  82. /**
  83. * 生命周期钩子:视图初始化完成后调用
  84. * 功能:初始化高德地图和相关组件
  85. */
  86. ngAfterViewInit() {
  87. // 加载高德地图API
  88. this.loadAMapAPI();
  89. }
  90. /**
  91. * 加载高德地图API
  92. */
  93. private loadAMapAPI(): void {
  94. try {
  95. // 配置安全密钥
  96. (window as any)._AMapSecurityConfig = {
  97. securityJsCode: this.config.securityJsCode || ''
  98. };
  99. // 动态加载高德地图API脚本
  100. const script = document.createElement('script');
  101. script.src = `https://webapi.amap.com/maps?v=2.0&key=${this.config.key}&plugin=AMap.CitySearch,AMap.ToolBar,AMap.Scale,AMap.Geolocation,AMap.AutoComplete,AMap.Driving,AMap.Walking,AMap.Transfer`;
  102. script.onload = () => {
  103. console.log('高德地图API加载成功');
  104. // API加载成功后初始化地图
  105. this.initMap();
  106. };
  107. script.onerror = (error) => {
  108. console.error('高德地图API加载失败:', error);
  109. };
  110. document.head.appendChild(script);
  111. } catch (error) {
  112. console.error('加载高德地图API时出错:', error);
  113. }
  114. }
  115. /**
  116. * 初始化高德地图
  117. * 功能:创建地图实例,设置默认中心点和缩放级别,并初始化地图控件和定位功能
  118. */
  119. private initMap(): void {
  120. // 创建地图实例
  121. this.map = new AMap.Map('amap-container', {
  122. zoom: 11, // 默认缩放级别
  123. center: [116.397428, 39.90923] // 默认中心点(北京天安门)
  124. });
  125. // 添加地图控件(缩放、比例尺、定位等)
  126. this.addMapControls();
  127. // 初始化IP定位,获取用户当前城市信息
  128. this.initIPLocation();
  129. }
  130. /**
  131. * 添加地图控件
  132. * 功能:添加缩放控件、比例尺控件和定位控件到地图上
  133. * 每个控件都经过位置优化,避免与界面元素重叠
  134. */
  135. private addMapControls(): void {
  136. // 添加缩放控件
  137. AMap.plugin(['AMap.ToolBar'], () => {
  138. const toolBar = new AMap.ToolBar({
  139. offset: [20, 150], // 调整位置避免与输入框重叠
  140. position: 'RB'
  141. });
  142. this.map.addControl(toolBar);
  143. });
  144. // 添加比例尺控件
  145. AMap.plugin(['AMap.Scale'], () => {
  146. const scale = new AMap.Scale({
  147. offset: [20, 90], // 左下角位置
  148. position: 'LB'
  149. });
  150. this.map.addControl(scale);
  151. });
  152. // 添加定位控件(仅用于获取精确坐标,城市信息通过CitySearch获取)
  153. AMap.plugin(['AMap.Geolocation'], () => {
  154. this.geolocation = new AMap.Geolocation({
  155. enableHighAccuracy: true,
  156. showMarker: true,
  157. maximumAge: 1000000000,
  158. timeout: 10000,
  159. offset: [20, 90], // 调整位置避免与输入框重叠
  160. position: 'RB',
  161. zoomToAccuracy: true
  162. });
  163. this.map.addControl(this.geolocation);
  164. // 获取当前位置
  165. this.geolocation.getCurrentPosition((status: string, result: any) => {
  166. try {
  167. if (status === 'complete' && result.position) {
  168. // 保存HTML5定位的精确坐标
  169. this.currentLocation = result.position;
  170. console.log('HTML5定位成功获取坐标:', this.currentLocation);
  171. console.log('定位精度:', result.accuracy);
  172. // 注意:城市信息主要通过AMap.CitySearch获取,这里仅获取坐标
  173. // HTML5定位主要用于获取精确位置,不依赖它获取城市信息
  174. console.log('HTML5定位结果:', result);
  175. // 如果有定位成功回调,也可以在这里触发重新计算距离
  176. if (this.searchSuggestions.length > 0 && this.searchKeyword) {
  177. console.log('定位成功后重新计算搜索结果距离...');
  178. this.onSearchKeywordChange();
  179. }
  180. } else {
  181. console.warn('HTML5定位失败,将继续使用IP定位结果:', result.message || '未知错误');
  182. }
  183. } catch (error) {
  184. console.error('处理HTML5定位结果时出错:', error);
  185. // 即使定位处理出错也不中断程序
  186. }
  187. });
  188. });
  189. }
  190. /**
  191. * 初始化IP定位功能
  192. * 功能:
  193. * - 使用AMap.CitySearch获取用户城市信息
  194. * - 标准化城市名称,去除末尾的"市"字
  195. * - 设置地图中心点为用户城市中心
  196. * - 容错处理:定位失败时默认使用南昌作为城市
  197. * - 初始化输入提示功能,确保搜索范围限制在当前城市
  198. */
  199. private initIPLocation(): void {
  200. try {
  201. // 使用高德地图的IP定位服务
  202. AMap.plugin(['AMap.CitySearch'], () => {
  203. const citySearch = new AMap.CitySearch();
  204. // 使用getLocalCity方法获取用户城市信息
  205. citySearch.getLocalCity((status: string, result: any) => {
  206. try {
  207. console.log('CitySearch.getLocalCity结果:', {status, result});
  208. if (status === 'complete' && result.info === 'OK' && result.city) {
  209. // 处理城市名称,去除末尾的"市"字
  210. let cityName = result.city;
  211. cityName = cityName.replace(/市$/, '');
  212. this.currentCity = cityName;
  213. // 保存当前城市中心坐标并设置地图中心点
  214. if (result.center && Array.isArray(result.center) && result.center.length === 2) {
  215. // 只有当center有效时才设置地图中心点
  216. this.map.setCenter(result.center);
  217. this.currentLocation = result.center;
  218. }
  219. console.log('当前城市:', result.city);
  220. console.log('标准化后的城市名:', this.currentCity);
  221. console.log('当前城市中心坐标:', this.currentLocation);
  222. console.log('IP定位城市编码:', result.citycode);
  223. // 强制使用南昌作为搜索城市,确保搜索结果只显示南昌地区
  224. console.log('强制使用南昌作为搜索城市');
  225. this.currentCity = '南昌';
  226. // 设置南昌的精确中心坐标
  227. this.currentLocation = [115.892151, 28.676493];
  228. this.map.setCenter(this.currentLocation);
  229. } else {
  230. console.warn('IP定位失败或无结果,status:', status, 'info:', result?.info || '未知');
  231. // IP定位失败时,使用南昌作为默认城市
  232. console.log('使用默认城市:南昌');
  233. this.currentCity = '南昌';
  234. // 设置南昌的中心坐标 [115.892151, 28.676493]
  235. this.currentLocation = [115.892151, 28.676493];
  236. this.map.setCenter(this.currentLocation);
  237. }
  238. console.log('最终搜索城市:', this.currentCity);
  239. console.log('最终定位坐标:', this.currentLocation);
  240. // 无论定位成功与否,都初始化输入提示
  241. this.initAutoComplete();
  242. } catch (error) {
  243. console.error('处理CitySearch结果时出错:', error);
  244. // 设置默认值并继续初始化
  245. this.currentCity = '南昌';
  246. this.currentLocation = [115.892151, 28.676493];
  247. this.map.setCenter(this.currentLocation);
  248. this.initAutoComplete();
  249. }
  250. });
  251. });
  252. } catch (error) {
  253. console.error('初始化CitySearch时出错:', error);
  254. // 设置默认值并继续初始化
  255. this.currentCity = '南昌';
  256. this.currentLocation = [115.892151, 28.676493];
  257. this.map.setCenter(this.currentLocation);
  258. this.initAutoComplete();
  259. }
  260. }
  261. /**
  262. * 初始化输入提示功能
  263. * 功能:
  264. * - 创建高德地图输入提示服务实例
  265. * - 限制搜索范围在当前城市
  266. * - 设置搜索类型为全类型POI
  267. * - 监听输入提示选择事件和结果事件
  268. * - 增强城市名称过滤,严格限制只保留南昌地区的结果
  269. * - 包含错误处理,确保即使初始化失败也有友好的错误信息
  270. */
  271. private initAutoComplete(): void {
  272. if (!this.topInput || !this.topInput.nativeElement) {
  273. console.error('输入框元素未找到,无法初始化搜索功能');
  274. return;
  275. }
  276. // 添加输入提示插件
  277. try {
  278. AMap.plugin(['AMap.AutoComplete'], () => {
  279. // 如果没有获取到当前城市,默认使用'南昌'
  280. const initCity = this.currentCity || '南昌';
  281. console.log('初始化AutoComplete使用的城市:', initCity);
  282. this.autoComplete = new AMap.AutoComplete({
  283. input: '', // 不绑定输入框,避免显示官方默认提示
  284. city: initCity, // 限制在当前城市范围内
  285. citylimit: true, // 强制限制在当前城市
  286. type: 'all', // 查询所有类型的POI
  287. output: 'all' // 返回详细信息
  288. });
  289. // 监听输入提示选中事件
  290. this.autoComplete.on('select', (data: any) => {
  291. console.log('选中的地址:', data);
  292. // 兼容不同数据格式
  293. this.searchKeyword = data.poi ? data.poi.name : data.name;
  294. this.searchSuggestions = [];
  295. // 确保选中地址时也使用正确的城市限制
  296. console.log('选中地址时使用的城市:', this.currentCity);
  297. // 可以在这里进一步验证选中的地址是否在当前城市范围内
  298. });
  299. // 监听输入提示结果事件
  300. this.autoComplete.on('complete', (data: any) => {
  301. console.log('输入提示结果:', data);
  302. // 验证结果是否包含城市信息,确保是南昌地区
  303. const suggestions = data.tips || [];
  304. console.log('原始建议数量:', suggestions.length);
  305. // 增强城市名称过滤,严格限制只保留南昌地区的结果
  306. const nanchangSuggestions = suggestions.filter((suggestion: any) => {
  307. // 检查建议项中是否包含南昌相关信息
  308. const hasNanchang =
  309. (suggestion.district && (suggestion.district.includes('南昌') || suggestion.district.includes('红谷滩') ||
  310. suggestion.district.includes('东湖') || suggestion.district.includes('西湖') ||
  311. suggestion.district.includes('青山湖') || suggestion.district.includes('青云谱'))) ||
  312. (suggestion.address && (suggestion.address.includes('南昌') ||
  313. // 对于地址中不包含城市名称但在南昌地区的结果
  314. (suggestion.poiaddress && suggestion.poiaddress.includes('南昌市')))) ||
  315. // 对于没有地区信息但确实在南昌的结果,使用距离过滤
  316. (!suggestion.district && !suggestion.address && suggestion.location);
  317. // 额外检查:排除明显不在南昌的结果
  318. const isNotInNanchang =
  319. (suggestion.district && (suggestion.district.includes('吉林') || suggestion.district.includes('通化') ||
  320. suggestion.district.includes('梅河口') || suggestion.district.includes('省外'))) ||
  321. (suggestion.address && (suggestion.address.includes('吉林') || suggestion.address.includes('通化') ||
  322. suggestion.address.includes('梅河口')));
  323. return hasNanchang && !isNotInNanchang;
  324. });
  325. console.log('南昌地区过滤后建议数量:', nanchangSuggestions.length);
  326. // 处理搜索结果,添加距离信息并排序
  327. this.enrichSuggestionsWithDistance(nanchangSuggestions);
  328. });
  329. // 监听输入提示错误事件
  330. this.autoComplete.on('error', (error: any) => {
  331. console.error('输入提示错误:', error);
  332. this.searchSuggestions = [];
  333. });
  334. });
  335. } catch (error) {
  336. console.error('初始化AutoComplete失败:', error);
  337. }
  338. }
  339. /**
  340. * 处理搜索关键词变化
  341. * 功能:
  342. * - 实现搜索防抖功能(300ms延迟),优化性能
  343. * - 清空无效关键词的搜索建议
  344. * - 构建搜索参数,确保city参数正确传递
  345. * - 设置城市编码和搜索半径,提高结果准确性
  346. * - 处理各种错误情况,提供友好的错误提示
  347. * - 调用enrichSuggestionsWithDistance方法为搜索结果添加距离信息并排序
  348. */
  349. onSearchKeywordChange(): void {
  350. console.log('\n===== 搜索关键词变化 =====');
  351. console.log('新的搜索关键词:', this.searchKeyword);
  352. console.log('当前城市:', this.currentCity);
  353. // 清除之前的定时器
  354. if (this.debounceTimer) {
  355. console.log('清除之前的搜索定时器');
  356. clearTimeout(this.debounceTimer);
  357. }
  358. // 如果关键词为空,清空建议列表
  359. if (!this.searchKeyword.trim()) {
  360. console.log('搜索关键词为空,清空建议列表');
  361. this.searchSuggestions = [];
  362. this.cdr.markForCheck();
  363. return;
  364. }
  365. // 设置300ms的防抖定时器(降低延迟提高响应速度)
  366. console.log('设置300ms防抖定时器');
  367. this.debounceTimer = setTimeout(() => {
  368. console.log('执行搜索请求...');
  369. // 如果autoComplete未初始化,尝试重新初始化
  370. if (!this.autoComplete && this.topInput && this.topInput.nativeElement) {
  371. console.log('autoComplete未初始化,尝试重新初始化');
  372. this.initAutoComplete();
  373. }
  374. if (this.autoComplete) {
  375. console.log('AutoComplete实例存在,执行搜索');
  376. try {
  377. // 构建搜索参数,确保city参数正确传递
  378. // 如果没有获取到当前城市,设置为'南昌'作为默认值
  379. const searchCity = this.currentCity || '南昌';
  380. console.log('实际使用的搜索城市:', searchCity);
  381. const searchOptions = {
  382. city: searchCity,
  383. citylimit: true,
  384. // 增加extensions参数以获取更详细的信息
  385. extensions: 'all',
  386. // 明确指定城市编码,确保只返回南昌的数据
  387. citycodes: '360100', // 南昌的城市编码
  388. // 添加区域限制,确保结果更精准
  389. location: this.currentLocation, // 使用南昌中心坐标作为搜索中心点
  390. radius: 50000, // 搜索半径(米),设置为50公里,限制搜索范围在城市内
  391. // 增加类型过滤,提高结果相关性
  392. type: 'all'
  393. }
  394. console.log('搜索参数:', {
  395. keyword: this.searchKeyword,
  396. options: searchOptions
  397. });
  398. this.autoComplete.search(this.searchKeyword, searchOptions, (status: string, result: any) => {
  399. console.log('搜索返回结果:');
  400. console.log(' 状态:', status);
  401. console.log(' 结果数据:', result);
  402. console.log(' 建议数量:', result.tips?.length || 0);
  403. if (status === 'complete') {
  404. console.log('搜索成功,开始处理建议项...');
  405. // 处理搜索结果,添加距离信息并排序
  406. const suggestions = result.tips || [];
  407. this.enrichSuggestionsWithDistance(suggestions);
  408. // 强制触发变更检测
  409. this.cdr.markForCheck();
  410. console.log('已触发变更检测');
  411. } else {
  412. // 处理各种错误情况
  413. let errorMessage = '搜索失败';
  414. if (result === 'INVALID_USER_SCODE') {
  415. errorMessage = '高德地图API密钥无效,请检查index.html中的配置';
  416. console.error('高德地图API错误:密钥无效');
  417. } else {
  418. errorMessage = result?.info || '未知错误';
  419. }
  420. console.error('输入提示失败:', errorMessage);
  421. // 显示空结果,不使用模拟数据
  422. this.searchSuggestions = [];
  423. this.cdr.markForCheck();
  424. console.log('已触发变更检测(错误状态)');
  425. // 可以在这里添加用户友好的错误提示
  426. // 注释掉alert以避免频繁弹窗
  427. // alert(`搜索出错:${errorMessage}`);
  428. }
  429. });
  430. } catch (error) {
  431. console.error('搜索执行异常:', error);
  432. this.searchSuggestions = [];
  433. this.cdr.markForCheck();
  434. console.log('已触发变更检测(异常状态)');
  435. // 注释掉alert以避免频繁弹窗
  436. // alert('搜索功能暂时不可用,请稍后再试');
  437. }
  438. } else {
  439. console.log('autoComplete仍未初始化');
  440. this.searchSuggestions = [];
  441. this.cdr.markForCheck();
  442. console.log('已触发变更检测(未初始化状态)');
  443. // 注释掉alert以避免频繁弹窗
  444. // alert('搜索功能正在初始化,请稍后再试');
  445. }
  446. }, 300);
  447. }
  448. /**
  449. * 为搜索建议添加距离信息并按距离和相似度排序
  450. * 功能:
  451. * - 支持两种坐标格式:数组格式[lng, lat]和对象格式{lng, lat}
  452. * - 对每个搜索建议项计算与当前位置的距离
  453. * - 使用Haversine公式计算球面两点间距离
  454. * - 按距离升序排序,距离相同时按相似度降序排序
  455. * - 距离过滤:严格限制只保留距离小于等于50公里的结果
  456. * - 增强城市名称过滤,确保只显示南昌地区的结果
  457. * - 强制触发变更检测,更新UI显示
  458. */
  459. private enrichSuggestionsWithDistance(suggestions: any[]): void {
  460. console.log('===== 开始处理搜索建议,数量:', suggestions?.length);
  461. if (!suggestions || suggestions.length === 0) {
  462. console.log('没有搜索建议,清空结果');
  463. this.searchSuggestions = [];
  464. return;
  465. }
  466. // 打印当前位置信息
  467. console.log('当前位置currentLocation:', this.currentLocation);
  468. console.log('当前位置类型:', typeof this.currentLocation);
  469. console.log('当前位置是否为数组:', Array.isArray(this.currentLocation));
  470. if (Array.isArray(this.currentLocation)) {
  471. console.log('当前位置数组长度:', this.currentLocation.length);
  472. console.log('坐标值:', this.currentLocation[0], this.currentLocation[1]);
  473. }
  474. // 支持两种坐标格式:数组格式 [lng, lat] 或对象格式 {lng: number, lat: number}
  475. let validLocation = false;
  476. let currentLocationPoint: number[] = [];
  477. // 检查是否为数组格式
  478. if (Array.isArray(this.currentLocation) && this.currentLocation.length === 2 && !isNaN(this.currentLocation[0]) && !isNaN(this.currentLocation[1])) {
  479. validLocation = true;
  480. currentLocationPoint = this.currentLocation;
  481. console.log('当前位置为数组格式,有效');
  482. }
  483. // 检查是否为对象格式(AMap.LngLat或其他带有lng/lat属性的对象)
  484. else if (this.currentLocation && typeof this.currentLocation === 'object' &&
  485. this.currentLocation.lng !== undefined && this.currentLocation.lat !== undefined &&
  486. !isNaN(this.currentLocation.lng) && !isNaN(this.currentLocation.lat)) {
  487. validLocation = true;
  488. currentLocationPoint = [this.currentLocation.lng, this.currentLocation.lat];
  489. console.log('当前位置为对象格式,有效');
  490. console.log('提取的坐标点:', currentLocationPoint);
  491. }
  492. // 预处理所有建议项,确保rawDistance有默认值
  493. suggestions.forEach(suggestion => {
  494. // 确保每个建议项都有rawDistance属性,初始化为Infinity
  495. if (suggestion.rawDistance === undefined || suggestion.rawDistance === null) {
  496. suggestion.rawDistance = Infinity;
  497. }
  498. });
  499. // 如果有当前位置坐标,计算每个建议项的距离
  500. if (validLocation) {
  501. console.log('开始计算每个建议项的距离...');
  502. suggestions.forEach((suggestion, index) => {
  503. console.log(`\n处理建议项${index + 1}:`, suggestion.name);
  504. console.log('建议项location:', suggestion.location);
  505. console.log('建议项location类型:', typeof suggestion.location);
  506. if (suggestion.location) {
  507. // 处理不同格式的location对象
  508. let locationPoint: number[] = [];
  509. // 检查location是否为对象并有lng/lat属性
  510. if (suggestion.location.lng !== undefined && suggestion.location.lat !== undefined) {
  511. console.log('检测到location为对象格式,提取lng/lat');
  512. locationPoint = [suggestion.location.lng, suggestion.location.lat];
  513. }
  514. // 或者是数组格式
  515. else if (Array.isArray(suggestion.location)) {
  516. console.log('检测到location为数组格式');
  517. locationPoint = suggestion.location;
  518. } else {
  519. console.warn(`建议项${index + 1}的location格式不支持:`, suggestion.location);
  520. }
  521. console.log('提取的locationPoint:', locationPoint);
  522. if (locationPoint.length === 2 && !isNaN(locationPoint[0]) && !isNaN(locationPoint[1])) {
  523. try {
  524. // 计算两点之间的距离
  525. console.log('开始计算距离...');
  526. console.log('起点坐标:', currentLocationPoint);
  527. console.log('终点坐标:', locationPoint);
  528. const distance = this.calculateDistance(currentLocationPoint, locationPoint);
  529. const formattedDistance = this.formatDistance(distance);
  530. console.log(`计算完成,距离: ${distance}米, 格式化后: ${formattedDistance}`);
  531. // 格式化距离显示
  532. suggestion.distance = formattedDistance;
  533. // 保存原始距离用于排序
  534. suggestion.rawDistance = distance;
  535. } catch (error) {
  536. console.error(`计算距离时出错:`, error);
  537. suggestion.distance = '';
  538. suggestion.rawDistance = Infinity;
  539. }
  540. } else {
  541. console.warn(`locationPoint无效,无法计算距离:`, locationPoint);
  542. suggestion.distance = '';
  543. suggestion.rawDistance = Infinity;
  544. }
  545. } else {
  546. console.warn(`建议项${index + 1}没有location属性`);
  547. suggestion.distance = '';
  548. suggestion.rawDistance = Infinity;
  549. }
  550. });
  551. } else {
  552. console.warn('当前位置信息无效,无法计算距离');
  553. // 如果没有有效的当前位置,设置默认值
  554. suggestions.forEach(suggestion => {
  555. suggestion.distance = '';
  556. suggestion.rawDistance = Infinity;
  557. });
  558. }
  559. // 按距离和相似度排序(优先按距离排序,距离相同时按相似度)
  560. console.log('\n开始排序搜索建议...');
  561. console.log('排序前的建议列表(显示部分字段):');
  562. suggestions.forEach((s, i) => console.log(` ${i + 1}: ${s.name}, 距离: ${s.rawDistance}米, 权重: ${s.weight || 0}`));
  563. suggestions.sort((a, b) => {
  564. // 距离升序
  565. if (a.rawDistance !== b.rawDistance) {
  566. return a.rawDistance - b.rawDistance;
  567. }
  568. // 相似度降序
  569. return (b.weight || 0) - (a.weight || 0);
  570. });
  571. console.log('排序后的建议列表(显示部分字段):');
  572. suggestions.forEach((s, i) => console.log(` ${i + 1}: ${s.name}, 距离: ${s.rawDistance}米, 权重: ${s.weight || 0}`));
  573. // 距离过滤:严格限制只保留距离小于等于50公里的结果
  574. // 这是一个额外的保险机制,确保只显示南昌本地结果
  575. const filteredSuggestions = suggestions.filter(suggestion => {
  576. // 严格限制:只有距离信息有效且小于等于50公里的结果才保留
  577. // 对于没有距离信息的结果,需要额外判断是否在南昌地区
  578. if (suggestion.rawDistance === Infinity) {
  579. // 如果没有距离信息,检查地址或区域信息是否包含南昌
  580. return (suggestion.district && (suggestion.district.includes('南昌') ||
  581. suggestion.district.includes('红谷滩') ||
  582. suggestion.district.includes('东湖') ||
  583. suggestion.district.includes('西湖') ||
  584. suggestion.district.includes('青山湖') ||
  585. suggestion.district.includes('青云谱'))) ||
  586. (suggestion.address && suggestion.address.includes('南昌'));
  587. }
  588. // 距离小于等于50公里的结果保留
  589. return suggestion.rawDistance <= 50000; // 50000米 = 50公里
  590. });
  591. console.log('\n距离过滤后建议数量:', filteredSuggestions.length, '(过滤前:', suggestions.length, ')');
  592. filteredSuggestions.forEach((s, i) => console.log(` ${i + 1}: ${s.name}, 距离: ${s.rawDistance}米`));
  593. // 更新建议列表
  594. this.searchSuggestions = [...filteredSuggestions];
  595. console.log('searchSuggestions已更新:', this.searchSuggestions.length, '项');
  596. // 强制触发变更检测
  597. this.cdr.markForCheck();
  598. console.log('已触发变更检测');
  599. }
  600. /**
  601. * 计算两点之间的距离(单位:米)
  602. * 功能:
  603. * - 使用Haversine公式计算球面两点间距离,适用于地球表面上的坐标计算
  604. * - 包含参数验证,确保输入坐标有效
  605. * - 将经纬度转换为弧度进行计算
  606. * - 返回两点间的直线距离(单位:米)
  607. * @param point1 第一个点的坐标,格式为[经度, 纬度]
  608. * @param point2 第二个点的坐标,格式为[经度, 纬度]
  609. * @returns 两点间距离(米)
  610. */
  611. private calculateDistance(point1: number[], point2: number[]): number {
  612. console.log('调用calculateDistance方法:');
  613. console.log(' point1:', point1);
  614. console.log(' point2:', point2);
  615. // 参数验证
  616. if (!Array.isArray(point1) || point1.length !== 2 || !Array.isArray(point2) || point2.length !== 2) {
  617. console.error('calculateDistance参数错误:', {point1, point2});
  618. return 0;
  619. }
  620. // 检查坐标值是否有效
  621. if (isNaN(point1[0]) || isNaN(point1[1]) || isNaN(point2[0]) || isNaN(point2[1])) {
  622. console.error('calculateDistance参数包含无效数值:', {point1, point2});
  623. return 0;
  624. }
  625. // 地球半径(单位:米)
  626. const R = 6371000;
  627. // 转换为弧度
  628. const lat1 = point1[1] * Math.PI / 180; // 纬度1(弧度)
  629. const lat2 = point2[1] * Math.PI / 180; // 纬度2(弧度)
  630. const latDiff = (point2[1] - point1[1]) * Math.PI / 180; // 纬度差
  631. const lngDiff = (point2[0] - point1[0]) * Math.PI / 180; // 经度差
  632. console.log(' 转换为弧度后:');
  633. console.log(' lat1:', lat1);
  634. console.log(' lat2:', lat2);
  635. console.log(' latDiff:', latDiff);
  636. console.log(' lngDiff:', lngDiff);
  637. // 计算Haversine公式的中间值
  638. const sinLat = Math.sin(latDiff / 2);
  639. const sinLng = Math.sin(lngDiff / 2);
  640. console.log(' Haversine中间值:');
  641. console.log(' sinLat:', sinLat);
  642. console.log(' sinLng:', sinLng);
  643. const a = sinLat * sinLat +
  644. Math.cos(lat1) * Math.cos(lat2) *
  645. sinLng * sinLng;
  646. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  647. const distance = R * c;
  648. console.log(' 计算结果:', distance, '米');
  649. return distance;
  650. }
  651. /**
  652. * 格式化距离显示
  653. * 功能:
  654. * - 处理无效距离值
  655. * - 小于1000米时显示为"XX米"
  656. * - 大于等于1000米时显示为"X.X公里"
  657. * @param meters 距离(米)
  658. * @returns 格式化后的距离字符串
  659. */
  660. private formatDistance(meters: number): string {
  661. console.log('调用formatDistance方法:', meters, '米');
  662. let result = '';
  663. if (isNaN(meters) || meters < 0) {
  664. console.warn('无效的距离值:', meters);
  665. result = '';
  666. } else if (meters < 1000) {
  667. result = `${Math.round(meters)}米`;
  668. } else {
  669. result = `${(meters / 1000).toFixed(1)}公里`;
  670. }
  671. console.log(' 格式化结果:', result);
  672. return result;
  673. }
  674. /**
  675. * 选择搜索建议项
  676. * 功能:
  677. * - 设置搜索关键词为选中的建议名称
  678. * - 清空搜索建议列表
  679. * - 切换输入框到底部状态
  680. * - 将地图中心移动到选中地址位置
  681. * - 添加定位标记点
  682. * - 保存当前选择的目标位置信息
  683. * - 显示路线规划按钮
  684. * @param suggestion 选中的搜索建议项
  685. */
  686. selectSuggestion(suggestion: any): void {
  687. this.searchKeyword = suggestion.name;
  688. this.searchSuggestions = [];
  689. // 1. 搜索区域触发返回按钮点击效果
  690. this.switchToBottomInput();
  691. // 2. 地图定位至所选提示项对应的位置
  692. if (suggestion.location) {
  693. this.map.setCenter(suggestion.location);
  694. this.map.setZoom(15);
  695. // 3. 在定位位置显示定位图标
  696. this.addLocationMarker(suggestion.location, suggestion.name);
  697. // 4. 保存当前选择的目标位置
  698. this.currentTarget = {
  699. name: suggestion.name,
  700. location: suggestion.location,
  701. address: suggestion.address || suggestion.name
  702. };
  703. // 5. 显示底部按钮区域
  704. this.showRouteButton = true;
  705. }
  706. }
  707. /**
  708. * 添加定位标记点
  709. * 功能:
  710. * - 如果已有标记点,先移除旧的标记点
  711. * - 创建新的标记点,设置位置、标题和自定义图标
  712. * - 调整图标锚点位置,使图标中心对准实际位置
  713. * - 添加标记点到地图
  714. * - 为标记点添加点击事件,显示信息窗口
  715. * @param location 标记点位置坐标
  716. * @param title 标记点标题
  717. */
  718. private addLocationMarker(location: any, title: string): void {
  719. // 如果已有标记点,先移除
  720. if (this.locationMarker) {
  721. this.map.remove(this.locationMarker);
  722. }
  723. // 创建新的标记点
  724. this.locationMarker = new AMap.Marker({
  725. position: location,
  726. title: title,
  727. icon: new AMap.Icon({
  728. size: new AMap.Size(40, 40),
  729. image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
  730. imageSize: new AMap.Size(40, 40)
  731. }),
  732. offset: new AMap.Pixel(-20, -40) // 调整图标锚点
  733. });
  734. // 添加标记点到地图
  735. this.map.add(this.locationMarker);
  736. // 添加标记点点击事件,显示信息窗口
  737. this.locationMarker.on('click', () => {
  738. const infoWindow = new AMap.InfoWindow({
  739. content: `<div style="padding: 10px;">${title}</div>`,
  740. offset: new AMap.Pixel(0, -30)
  741. });
  742. infoWindow.open(this.map, location);
  743. });
  744. }
  745. /**
  746. * 规划路线
  747. * 功能:
  748. * - 检查当前位置和目标位置是否有效
  749. * - 显示路线规划面板
  750. * - 自动生成路径规划结果(默认驾车)
  751. * - 提供错误处理,确保缺少必要位置信息时有友好提示
  752. */
  753. planRoute(): void {
  754. // 修改:不再依赖locationMarker,直接使用currentLocation和currentTarget
  755. if (this.currentLocation && this.currentTarget && this.currentTarget.location) {
  756. console.log('planRoute: 准备显示路线规划面板');
  757. // 显示路线规划面板
  758. this.showRoutePanel = true;
  759. // 自动生成路径规划结果(默认驾车)
  760. this.calculateRoute();
  761. } else {
  762. console.error('planRoute: 缺少必要的位置信息', {
  763. currentLocation: this.currentLocation,
  764. currentTarget: this.currentTarget
  765. });
  766. }
  767. }
  768. /**
  769. * 切换路线规划类型
  770. * 功能:
  771. * - 更新路线规划类型
  772. * - 切换类型后自动重新计算路线
  773. * @param type 路线类型:driving(驾车), walking(步行), bus(公交)
  774. */
  775. switchRouteType(type: string): void {
  776. this.routeType = type;
  777. // 切换类型后重新计算路线
  778. this.calculateRoute();
  779. }
  780. /**
  781. * 计算路线
  782. * 功能:
  783. * - 验证起点和终点位置信息是否有效
  784. * - 支持多种坐标格式的转换和标准化
  785. * - 处理起点终点过于接近的特殊情况(距离小于100米)
  786. * - 根据路线类型(驾车、步行、公交)初始化不同的路线规划服务
  787. * - 设置不同的路线规划策略(时间优先、距离优先等)
  788. * - 调用performRouteSearch执行实际的路线搜索
  789. */
  790. private calculateRoute(): void {
  791. // 修改:使用当前位置作为起点,而不是标记点位置
  792. if (!this.currentLocation || !this.currentTarget || !this.currentTarget.location) {
  793. console.error('缺少必要的位置信息,无法计算路线');
  794. return;
  795. }
  796. // 获取目标位置作为终点
  797. const endPoint = this.currentTarget.location;
  798. // 使用当前定位位置作为起点
  799. // 支持数组格式和对象格式的坐标
  800. let startPoint;
  801. if (Array.isArray(this.currentLocation) && this.currentLocation.length === 2) {
  802. // 数组格式 [lng, lat]
  803. startPoint = new AMap.LngLat(this.currentLocation[0], this.currentLocation[1]);
  804. } else if (this.currentLocation && typeof this.currentLocation === 'object' &&
  805. this.currentLocation.lng !== undefined && this.currentLocation.lat !== undefined) {
  806. // 对象格式 {lng, lat}
  807. startPoint = new AMap.LngLat(this.currentLocation.lng, this.currentLocation.lat);
  808. } else {
  809. console.error('当前位置格式无效:', this.currentLocation);
  810. // 使用默认起点(南昌中心)
  811. startPoint = new AMap.LngLat(115.892151, 28.676493);
  812. }
  813. // 检查起点和终点是否相同或过于接近(距离小于100米)
  814. // calculateDistance函数接受两个数组参数[经度, 纬度]
  815. const distanceMeters = this.calculateDistance(
  816. [startPoint.lng, startPoint.lat],
  817. [endPoint.lng, endPoint.lat]
  818. ); // 距离(米)
  819. // 精确到小数点后4位显示公里数,确保显示准确距离
  820. const distanceKm = distanceMeters / 1000;
  821. // 只有当距离真正小于100米时才使用默认起点
  822. if (distanceMeters < 100 && distanceMeters > 0) {
  823. // 设置一个默认起点,距离终点约500米
  824. startPoint = new AMap.LngLat(
  825. endPoint.lng - 0.005, // 经度减去0.005,大约向西移动500米
  826. endPoint.lat
  827. );
  828. console.log(`起点和终点过于接近(${distanceKm.toFixed(4)}公里),使用默认起点`);
  829. }
  830. console.log(`开始计算${this.routeType}路线`);
  831. console.log('起点:', startPoint);
  832. console.log('终点:', endPoint);
  833. console.log('起点终点距离:', distanceKm.toFixed(4), '公里'); // 显示更精确的距离
  834. // 只有当两点完全相同时才提示
  835. if (startPoint.lng === endPoint.lng && startPoint.lat === endPoint.lat) {
  836. console.log('起点和终点位置完全相同');
  837. }
  838. // 根据路线类型调用不同的路线规划服务
  839. AMap.plugin([
  840. 'AMap.Driving', // 驾车
  841. 'AMap.Walking', // 步行
  842. 'AMap.Transfer' // 公交
  843. ], () => {
  844. try {
  845. // 清除之前的路线结果
  846. this.clearRouteOverlays();
  847. let routeService: any;
  848. let options: any;
  849. switch (this.routeType) {
  850. case 'driving':
  851. routeService = new AMap.Driving({
  852. map: this.map,
  853. panel: 'amap-container',
  854. showTraffic: true,
  855. policy: AMap.DrivingPolicy.LEAST_TIME // 添加时间优先策略
  856. });
  857. options = {
  858. origin: startPoint,
  859. destination: endPoint
  860. };
  861. break;
  862. case 'walking':
  863. routeService = new AMap.Walking({
  864. map: this.map,
  865. panel: 'amap-container',
  866. policy: AMap.WalkingPolicy.LEAST_DISTANCE // 添加距离优先策略
  867. });
  868. options = {
  869. origin: startPoint,
  870. destination: endPoint
  871. };
  872. break;
  873. case 'bus':
  874. routeService = new AMap.Transfer({
  875. map: this.map,
  876. panel: 'amap-container',
  877. policy: AMap.TransferPolicy.LEAST_TIME, // 时间优先策略
  878. city: this.currentCity || '南昌'
  879. });
  880. options = {
  881. origin: startPoint,
  882. destination: endPoint,
  883. city: this.currentCity || '南昌'
  884. };
  885. break;
  886. default:
  887. console.error('未知的路线类型:', this.routeType);
  888. return;
  889. }
  890. this.performRouteSearch(routeService, options);
  891. } catch (error) {
  892. console.error('初始化路线规划服务失败:', error);
  893. alert('路线规划服务初始化失败,请重试');
  894. }
  895. });
  896. }
  897. /**
  898. * 执行路线搜索
  899. * 功能:
  900. * - 验证路线服务实例是否有效
  901. * - 清除之前的路线覆盖物
  902. * - 规范化起点和终点坐标格式
  903. * - 调用高德地图API执行路线搜索
  904. * - 处理搜索结果,显示路线信息
  905. * - 自动调整地图视野以显示完整路线
  906. * - 添加起点和终点标记点
  907. * - 提供错误处理和用户友好提示
  908. * @param service 路线服务实例
  909. * @param options 路线搜索选项(包含origin和destination)
  910. */
  911. private performRouteSearch(service: any, options: any): void {
  912. console.log('开始执行路线搜索:', options);
  913. // 确保service实例存在且有search方法
  914. if (!service || typeof service.search !== 'function') {
  915. console.error('路线规划服务实例无效');
  916. return;
  917. }
  918. // 清除可能存在的旧路线
  919. this.clearRouteOverlays();
  920. // 从options中提取起点和终点坐标
  921. let startPoint, endPoint;
  922. // 根据官方API,需要将起点和终点作为经纬度数组传递
  923. if (typeof options === 'object' && options.origin && options.destination) {
  924. // 确保获取到的是经纬度坐标
  925. if (Array.isArray(options.origin) && options.origin.length >= 2) {
  926. startPoint = options.origin; // 已经是经纬度数组
  927. } else if (options.origin.lng !== undefined && options.origin.lat !== undefined) {
  928. startPoint = [options.origin.lng, options.origin.lat]; // 转换为经纬度数组
  929. }
  930. if (Array.isArray(options.destination) && options.destination.length >= 2) {
  931. endPoint = options.destination; // 已经是经纬度数组
  932. } else if (options.destination.lng !== undefined && options.destination.lat !== undefined) {
  933. endPoint = [options.destination.lng, options.destination.lat]; // 转换为经纬度数组
  934. }
  935. console.log('使用经纬度数组形式参数:', { startPoint, endPoint });
  936. }
  937. // 验证起点终点是否有效
  938. if (!startPoint || !endPoint) {
  939. console.error('无效的起点或终点坐标:', { startPoint, endPoint });
  940. alert('无效的路线规划参数,请重试');
  941. return;
  942. }
  943. // 按照高德地图官方API格式调用:service.search(起点数组, 终点数组, 回调函数)
  944. service.search(startPoint, endPoint, (status: string, result: any) => {
  945. console.log('路线规划结果:', {status, result});
  946. this.routeResult = result;
  947. if (status === 'complete') {
  948. console.log('路线规划成功');
  949. // 增加结果数据的详细日志
  950. console.log('路线结果完整数据:', JSON.stringify(result, null, 2).substring(0, 500) + '...'); // 限制日志长度
  951. // 确保结果有效
  952. if (result.routes && result.routes.length > 0) {
  953. console.log('路线规划返回了', result.routes.length, '条路线');
  954. console.log('第一条路线详细信息:', {
  955. distance: result.routes[0].distance,
  956. duration: result.routes[0].duration,
  957. steps: result.routes[0].steps ? result.routes[0].steps.length : 0
  958. });
  959. // 自动调整地图视野以显示路线
  960. this.map.setFitView();
  961. // 显式地添加起点和终点标记(如果API没有自动添加)
  962. if (this.routeType === 'driving' || this.routeType === 'walking') {
  963. // 添加起点标记
  964. const startMarker = new AMap.Marker({
  965. position: options.origin,
  966. map: this.map,
  967. icon: new AMap.Icon({
  968. image: 'https://webapi.amap.com/theme/v1.3/markers/n/start.png',
  969. size: new AMap.Size(36, 36),
  970. imageSize: new AMap.Size(36, 36)
  971. })
  972. });
  973. // 添加终点标记
  974. const endMarker = new AMap.Marker({
  975. position: options.destination,
  976. map: this.map,
  977. icon: new AMap.Icon({
  978. image: 'https://webapi.amap.com/theme/v1.3/markers/n/end.png',
  979. size: new AMap.Size(36, 36),
  980. imageSize: new AMap.Size(36, 36)
  981. })
  982. });
  983. }
  984. } else {
  985. console.warn('路线规划成功但没有返回路线数据');
  986. alert('未找到合适的路线,请尝试调整起点和终点');
  987. }
  988. } else {
  989. console.error('路线规划失败:', result);
  990. alert(`路线规划失败: ${result.info || '未知错误'}`);
  991. }
  992. });
  993. }
  994. /**
  995. * 清除路线覆盖物
  996. * 功能:
  997. * - 获取地图上所有的折线覆盖物
  998. * - 检查覆盖物是否有效(包含路径点)
  999. * - 移除所有有效的路线覆盖物
  1000. */
  1001. private clearRouteOverlays(): void {
  1002. if (this.map) {
  1003. const overlays = this.map.getAllOverlays('polyline');
  1004. overlays.forEach((overlay: any) => {
  1005. if (overlay.getPath && overlay.getPath().length > 0) {
  1006. this.map.remove(overlay);
  1007. }
  1008. });
  1009. }
  1010. }
  1011. /**
  1012. * 清空路线规划
  1013. * 功能:
  1014. * - 隐藏路线规划面板
  1015. * - 重置路线规划类型为驾车
  1016. * - 清空路线规划结果数据
  1017. * - 移除地图上所有路线覆盖物
  1018. */
  1019. clearRoute(): void {
  1020. this.showRoutePanel = false;
  1021. this.routeType = 'driving';
  1022. this.routeResult = null;
  1023. // 清除地图上的路线
  1024. if (this.map) {
  1025. // 获取所有覆盖物
  1026. const overlays = this.map.getAllOverlays('polyline');
  1027. // 移除所有路线覆盖物
  1028. overlays.forEach((overlay: any) => {
  1029. if (overlay.getPath && overlay.getPath().length > 0) {
  1030. this.map.remove(overlay);
  1031. }
  1032. });
  1033. }
  1034. }
  1035. /**
  1036. * 切换输入框位置(底部到顶部)
  1037. * 功能:
  1038. * - 更新输入框位置状态为顶部
  1039. * - 使用定时器延迟聚焦输入框,确保动画完成后再聚焦
  1040. */
  1041. switchToTopInput(): void {
  1042. this.isInputBoxAtTop.set(true);
  1043. // 确保动画完成后聚焦输入框
  1044. setTimeout(() => {
  1045. if (this.topInput && this.topInput.nativeElement) {
  1046. this.topInput.nativeElement.focus();
  1047. }
  1048. }, 300);
  1049. }
  1050. /**
  1051. * 切换输入框位置(顶部到底部)
  1052. * 功能:
  1053. * - 更新输入框位置状态为底部
  1054. * - 清空搜索关键词
  1055. * - 清空搜索建议列表
  1056. * - 清空路线规划状态
  1057. * - 隐藏底部路线规划按钮
  1058. */
  1059. switchToBottomInput(): void {
  1060. this.isInputBoxAtTop.set(false);
  1061. this.searchKeyword = '';
  1062. this.searchSuggestions = [];
  1063. // 清空路线规划状态
  1064. this.clearRoute();
  1065. // 隐藏底部按钮区域
  1066. this.showRouteButton = false;
  1067. }
  1068. /**
  1069. * 点击历史记录项
  1070. * 功能:
  1071. * - 设置搜索关键词为选中的历史记录
  1072. * - 清空搜索建议列表
  1073. * - 注:可扩展添加地图移动到历史记录位置的逻辑
  1074. * @param item 选中的历史记录项
  1075. */
  1076. selectHistoryItem(item: string): void {
  1077. this.searchKeyword = item;
  1078. this.searchSuggestions = [];
  1079. // 可以在这里添加地图移动到历史记录位置的逻辑
  1080. }
  1081. /**
  1082. * 清空搜索关键词
  1083. * 功能:
  1084. * - 重置搜索关键词为空字符串
  1085. * - 清空搜索建议列表
  1086. * - 清空路线规划状态
  1087. * - 隐藏底部路线规划按钮
  1088. */
  1089. clearSearchKeyword(): void {
  1090. this.searchKeyword = '';
  1091. this.searchSuggestions = [];
  1092. // 清空路线规划状态
  1093. this.clearRoute();
  1094. // 隐藏底部按钮区域
  1095. this.showRouteButton = false;
  1096. }
  1097. /**
  1098. * 执行搜索
  1099. * 功能:
  1100. * - 验证搜索关键词和自动完成服务是否有效
  1101. * - 执行搜索操作
  1102. * - 如果搜索成功,将地图中心移动到第一个搜索结果位置
  1103. * - 将搜索词添加到历史记录(如果不存在)
  1104. * - 保持历史记录最多5项
  1105. * - 提供错误处理和用户友好提示
  1106. */
  1107. performSearch(): void {
  1108. if (this.searchKeyword.trim() && this.autoComplete) {
  1109. try {
  1110. this.autoComplete.search(this.searchKeyword, (status: string, result: any) => {
  1111. if (status === 'complete' && result.tips && result.tips.length > 0) {
  1112. const firstTip = result.tips[0];
  1113. if (firstTip.location) {
  1114. this.map.setCenter(firstTip.location);
  1115. this.map.setZoom(15);
  1116. }
  1117. // 添加到历史记录(如果不存在)
  1118. if (!this.historyItems.includes(this.searchKeyword)) {
  1119. this.historyItems.unshift(this.searchKeyword);
  1120. // 保持历史记录最多5项
  1121. if (this.historyItems.length > 5) {
  1122. this.historyItems.pop();
  1123. }
  1124. }
  1125. } else if (status === 'error') {
  1126. console.error('搜索执行错误:', result);
  1127. alert('搜索失败,请稍后再试');
  1128. }
  1129. this.searchSuggestions = [];
  1130. });
  1131. } catch (error) {
  1132. console.error('搜索执行异常:', error);
  1133. alert('搜索功能暂时不可用');
  1134. this.searchSuggestions = [];
  1135. }
  1136. }
  1137. }
  1138. /**
  1139. * 清空历史记录
  1140. * 功能:重置历史记录数组为空,清除所有搜索历史
  1141. */
  1142. clearHistory(): void {
  1143. this.historyItems = [];
  1144. }
  1145. }