index.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <template>
  2. <div class="map-layout">
  3. <div class="search">
  4. <el-input
  5. v-model="keyword"
  6. :placeholder="`请输入${searchName}搜索`"
  7. class="input-with-select"
  8. @keydown.enter="searchHandler"
  9. >
  10. <template #prepend>
  11. <el-select v-model="searchType" style="width: 100px">
  12. <el-option
  13. :label="type.label"
  14. :value="type.value"
  15. v-for="type in searchTypes"
  16. />
  17. </el-select>
  18. </template>
  19. </el-input>
  20. <div
  21. class="latlng-result"
  22. v-if="searchType === 'latlng' && (mark || mark === null)"
  23. :class="{ success: mark }"
  24. >
  25. <template v-if="mark">
  26. <h3>经纬度定位成功</h3>
  27. <p>经度:{{ mark.lng }}</p>
  28. <p>纬度:{{ mark.lat }}</p>
  29. </template>
  30. <template v-else>
  31. <h3>经纬度定位失败</h3>
  32. <p>请输入正确的经纬度格式</p>
  33. <p>纬度,经度 (例如23.11766,113.28122)</p>
  34. <p>纬度范围:-90到90</p>
  35. <p>经度范围:-180到180</p>
  36. </template>
  37. </div>
  38. <div class="name-result" v-if="searchType === 'name' && options.length">
  39. <div
  40. class="address-item operate"
  41. v-for="option in options"
  42. :class="{ active: activeOption === option }"
  43. @click="clickOption(option)"
  44. >
  45. <span>{{ option.name }}</span>
  46. {{ option.address }}
  47. </div>
  48. </div>
  49. </div>
  50. <div class="map" ref="mapEle"></div>
  51. <div class="tiles-select" v-if="tileGroups.length > 1">
  52. <el-select v-model="groupIndex" style="width: 140px">
  53. <el-option :label="group.name" :value="ndx" v-for="(group, ndx) in tileGroups" />
  54. </el-select>
  55. </div>
  56. </div>
  57. </template>
  58. <script lang="ts" setup>
  59. import { computed, ref, shallowRef, watch, watchEffect } from "vue";
  60. import {
  61. getCurrentLatlng,
  62. LatLng,
  63. latlngStrTransform,
  64. useLMap,
  65. useOnlyMarker,
  66. useSetLTileLayers,
  67. } from "./useLeaflet";
  68. import { BasemapInfo, SearchResultItem, SelectMapImageProps } from "../index";
  69. import html2canvas from "html2canvas";
  70. import {
  71. ElInput,
  72. ElSelect,
  73. ElOption,
  74. ElDropdown,
  75. ElDropdownMenu,
  76. ElDropdownItem,
  77. } from "element-plus";
  78. const props = defineProps<SelectMapImageProps>();
  79. const mapEle = shallowRef<HTMLDivElement>();
  80. const lMap = useLMap(mapEle);
  81. const setTileLayers = useSetLTileLayers(lMap);
  82. const groupIndex = ref(props.activeGroupIndex || 0);
  83. watchEffect(
  84. () => {
  85. if (groupIndex.value > props.tileGroups.length - 1) {
  86. groupIndex.value = 0;
  87. }
  88. },
  89. { flush: "sync" }
  90. );
  91. const tiles = computed(() => props.tileGroups[groupIndex.value].tiles);
  92. watchEffect(() => setTileLayers(tiles.value));
  93. const searchTypes = [
  94. { label: "经纬度", value: "latlng" },
  95. { label: "地址", value: "name" },
  96. ];
  97. const searchType = ref(searchTypes[0].value as "latlng" | "name");
  98. const searchName = computed(
  99. () => searchTypes.find((item) => item.value === searchType.value)!.label
  100. );
  101. const keyword = ref("");
  102. const mark = useOnlyMarker(lMap, ref<LatLng | null>());
  103. watch(searchType, () => {
  104. mark.value = undefined;
  105. keyword.value = "";
  106. });
  107. const options = ref<SearchResultItem[]>([]);
  108. const activeOption = ref<SearchResultItem>();
  109. const clickOption = (item: SearchResultItem) => {
  110. activeOption.value = item;
  111. mark.value = item.latlng;
  112. console.log(item);
  113. };
  114. let token = 0;
  115. const searchHandler = async () => {
  116. const currentToken = ++token;
  117. if (searchType.value === "latlng") {
  118. mark.value = latlngStrTransform(keyword.value);
  119. return;
  120. }
  121. options.value = [];
  122. activeOption.value = undefined;
  123. if (!keyword.value) return;
  124. const result = await props.search(keyword.value, props.tileGroups[groupIndex.value].id);
  125. if (currentToken === token) {
  126. options.value = result;
  127. }
  128. };
  129. getCurrentLatlng().then((latlng) => {
  130. if (!keyword.value && mark.value === undefined && searchType.value === "latlng") {
  131. keyword.value = `${latlng.lat},${latlng.lng}`;
  132. searchHandler();
  133. }
  134. });
  135. const submit = async (): Promise<BasemapInfo> => {
  136. if (!lMap.value) {
  137. throw "地图未初始化";
  138. }
  139. const bound = lMap.value.getBounds();
  140. const southWest = bound.getSouthWest(); // 西南角
  141. const northEast = bound.getNorthEast(); // 东北角
  142. const width = lMap.value.distance(
  143. [southWest.lng, northEast.lat],
  144. [northEast.lng, northEast.lat]
  145. );
  146. const height = lMap.value.distance(
  147. [northEast.lng, southWest.lat],
  148. [northEast.lng, northEast.lat]
  149. );
  150. let blob: Blob;
  151. try {
  152. const canvas = await html2canvas(mapEle.value!, {
  153. useCORS: true, // 允许跨域图片(瓦片需支持 CORS)
  154. allowTaint: true, // 允许污染 Canvas(慎用)
  155. scale: 2, // 提高分辨率
  156. logging: false, // 关闭日志
  157. });
  158. blob = await new Promise<Blob>((resolve, reject) => {
  159. canvas.toBlob((blob) => {
  160. blob ? resolve(blob) : reject("截图失败");
  161. });
  162. });
  163. } catch (e) {
  164. throw "截图失败";
  165. }
  166. return {
  167. blob,
  168. size: { width, height },
  169. };
  170. };
  171. defineExpose({ submit });
  172. </script>
  173. <style lang="scss" scoped>
  174. .map-layout {
  175. display: flex;
  176. }
  177. .search {
  178. margin-right: 24px;
  179. flex: 1;
  180. }
  181. .map {
  182. flex: 0 0 auto;
  183. height: 600px;
  184. width: 800px;
  185. position: relative;
  186. }
  187. .tiles-select {
  188. position: absolute;
  189. z-index: 9999;
  190. right: 30px;
  191. top: 70px;
  192. }
  193. .tiles-select-icon {
  194. filter: drop-shadow(0 0 3px rgba(0, 0, 0, 1));
  195. // box-shadow: 0 0 3px #000;
  196. }
  197. .latlng-result {
  198. margin-top: 16px;
  199. border-radius: 2px;
  200. border: 1px solid #d9d9d9;
  201. display: flex;
  202. align-items: center;
  203. flex-direction: column;
  204. justify-content: center;
  205. padding: 16px;
  206. p {
  207. font-size: 14px;
  208. color: #a7a7a7;
  209. line-height: 22px;
  210. min-width: 144px;
  211. text-align: left;
  212. }
  213. h3 {
  214. color: #f56c6c;
  215. font-weight: bold;
  216. font-size: 14px;
  217. margin-bottom: 8px;
  218. }
  219. &.success h3 {
  220. color: #67c23a;
  221. }
  222. }
  223. .name-result {
  224. margin-top: 10px;
  225. border-radius: 2px;
  226. max-height: 560px;
  227. overflow-y: auto;
  228. .address-item {
  229. padding: 16px 0px;
  230. display: flex;
  231. justify-content: center;
  232. flex-direction: column;
  233. color: rgba(0, 0, 0, 0.45);
  234. font-size: 14px;
  235. line-height: 22px;
  236. span {
  237. font-size: 14px;
  238. color: #000;
  239. margin-bottom: 4px;
  240. }
  241. cursor: pointer;
  242. &.active {
  243. background: var(--el-color-primary-light-9);
  244. }
  245. &:not(:last-child) {
  246. padding-bottom: 6px;
  247. border-bottom: 1px solid #d9d9d9;
  248. }
  249. }
  250. }
  251. .input-with-select {
  252. --el-fill-color-light: #ffffff;
  253. }
  254. </style>
  255. <style lang="scss">
  256. .leaflet-container {
  257. background: #fff;
  258. }
  259. </style>