search.vue 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <template>
  2. <div class="search-container">
  3. <div class="search-box">
  4. <input
  5. v-model="searchText"
  6. type="text"
  7. placeholder="搜索地名或经纬度,例如:北京 或 116.4074, 39.9042"
  8. @keyup.enter="search"
  9. @input="handleInput"
  10. />
  11. <button @click="search">搜索</button>
  12. </div>
  13. <div class="search-results" v-if="showResults && searchResults.length > 0">
  14. <div
  15. v-for="(result, index) in searchResults"
  16. :key="index"
  17. class="search-result-item"
  18. @click="selectResult(result)"
  19. >
  20. {{ result.name }}
  21. </div>
  22. </div>
  23. </div>
  24. </template>
  25. <script setup>
  26. import { ref, onUnmounted } from 'vue';
  27. import { PROXY_TIANDITU_API, TIANDITU_KEY } from "../../config/server_config.js";
  28. const props = defineProps({
  29. viewer: {
  30. type: Object,
  31. required: true
  32. }
  33. });
  34. const searchText = ref('');
  35. const showResults = ref(false);
  36. const searchResults = ref([]);
  37. const isSearching = ref(false);
  38. // 天地图 API 密钥来自外部配置 supermap.config.js
  39. // 处理输入
  40. const handleInput = async () => {
  41. if (searchText.value.trim()) {
  42. try {
  43. isSearching.value = true;
  44. // 调用天地图API获取搜索结果
  45. const results = await getLocationByTianditu(searchText.value.trim());
  46. searchResults.value = results;
  47. showResults.value = true;
  48. } catch (error) {
  49. console.error('搜索失败:', error);
  50. searchResults.value = [];
  51. showResults.value = false;
  52. } finally {
  53. isSearching.value = false;
  54. }
  55. } else {
  56. searchResults.value = [];
  57. showResults.value = false;
  58. }
  59. };
  60. // 搜索
  61. const search = async () => {
  62. if (searchText.value.trim()) {
  63. try {
  64. // 先尝试解析经纬度
  65. const latLng = parseLatLng(searchText.value.trim());
  66. if (latLng) {
  67. flyToLocation(latLng);
  68. showResults.value = false;
  69. return;
  70. }
  71. // 如果不是经纬度,调用天地图API获取位置信息
  72. const location = await getLonLatByTianditu(searchText.value.trim());
  73. if (location) {
  74. flyToLocation(location);
  75. showResults.value = false;
  76. }
  77. } catch (error) {
  78. console.error('搜索失败:', error);
  79. alert('搜索失败: ' + error.message);
  80. }
  81. }
  82. };
  83. // 解析经纬度字符串
  84. // 支持格式: "116.4074, 39.9042" 或 "116.4074 39.9042"
  85. const parseLatLng = (text) => {
  86. // 匹配经纬度格式:数字, 数字 或 数字 数字
  87. const regex = /^\s*(-?\d+\.?\d*)\s*[,,\s]\s*(-?\d+\.?\d*)\s*$/;
  88. const match = text.match(regex);
  89. if (match) {
  90. const lng = parseFloat(match[1]);
  91. const lat = parseFloat(match[2]);
  92. // 验证经纬度范围
  93. if (lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90) {
  94. return { name: `${lng}, ${lat}`, lng: lng, lat: lat, height: 3000 };
  95. }
  96. }
  97. return null;
  98. };
  99. // 选择搜索结果
  100. const selectResult = (result) => {
  101. searchText.value = result.name;
  102. flyToLocation(result);
  103. showResults.value = false;
  104. };
  105. // 天地图地理编码 API
  106. const getLonLatByTianditu = async (keyword) => {
  107. const url = `${PROXY_TIANDITU_API}/geocoder?ds={"keyWord":"${encodeURIComponent(keyword)}"}&tk=${TIANDITU_KEY}`;
  108. const res = await fetch(url);
  109. const data = await res.json();
  110. // 天地图返回成功
  111. if (data.status === "0" && data.location) {
  112. const lon = parseFloat(data.location.lon);
  113. const lat = parseFloat(data.location.lat);
  114. return { name: keyword, lng: lon, lat: lat, height: 3000 };
  115. } else {
  116. throw new Error("未找到该地点");
  117. }
  118. };
  119. // 天地图搜索 API
  120. const getLocationByTianditu = async (keyword) => {
  121. // 由于天地图的搜索API可能需要不同的接口,这里使用地理编码API作为示例
  122. // 实际项目中可以根据天地图API文档使用专门的搜索接口
  123. try {
  124. const location = await getLonLatByTianditu(keyword);
  125. return [location];
  126. } catch (error) {
  127. return [];
  128. }
  129. };
  130. // 飞行到指定位置
  131. const flyToLocation = (location) => {
  132. if (props.viewer) {
  133. // 设置一个更高的高度,确保能够看到周围区域
  134. const height = 200000; // 100公里高度
  135. props.viewer.camera.flyTo({
  136. destination: Cesium.Cartesian3.fromDegrees(
  137. location.lng,
  138. location.lat,
  139. height
  140. ),
  141. orientation: {
  142. heading: Cesium.Math.toRadians(0),
  143. pitch: Cesium.Math.toRadians(-90), // 俯仰角-45度
  144. roll: 0.0
  145. },
  146. duration: 1.8 // 飞行时间
  147. });
  148. }
  149. };
  150. // 点击外部关闭搜索结果
  151. const handleClickOutside = (event) => {
  152. const searchContainer = document.querySelector('.search-container');
  153. if (searchContainer && !searchContainer.contains(event.target)) {
  154. showResults.value = false;
  155. }
  156. };
  157. // 监听点击事件
  158. window.addEventListener('click', handleClickOutside);
  159. // 组件销毁时移除事件监听
  160. onUnmounted(() => {
  161. window.removeEventListener('click', handleClickOutside);
  162. });
  163. </script>
  164. <style scoped>
  165. .search-container {
  166. position: absolute;
  167. top: 20px;
  168. left: 20px;
  169. z-index: 1000;
  170. width: 300px;
  171. }
  172. .search-box {
  173. display: flex;
  174. background: white;
  175. border-radius: 4px;
  176. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  177. overflow: hidden;
  178. }
  179. .search-box input {
  180. flex: 1;
  181. padding: 10px 15px;
  182. border: none;
  183. outline: none;
  184. font-size: 14px;
  185. }
  186. .search-box button {
  187. padding: 0 20px;
  188. background: #409eff;
  189. color: white;
  190. border: none;
  191. cursor: pointer;
  192. font-size: 14px;
  193. }
  194. .search-box button:hover {
  195. background: #66b1ff;
  196. }
  197. .search-results {
  198. position: absolute;
  199. top: 100%;
  200. left: 0;
  201. right: 0;
  202. background: white;
  203. border-radius: 0 0 4px 4px;
  204. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  205. max-height: 200px;
  206. overflow-y: auto;
  207. margin-top: 2px;
  208. }
  209. .search-result-item {
  210. padding: 10px 15px;
  211. cursor: pointer;
  212. font-size: 14px;
  213. }
  214. .search-result-item:hover {
  215. background: #f5f7fa;
  216. }
  217. </style>