index.vue 7.3 KB

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