Sfoglia il codice sorgente

feat: 地图功能对接

bill 1 settimana fa
parent
commit
5300741d8b

+ 3 - 52
src/example/components/slide/actions.ts

@@ -1,7 +1,7 @@
 import { DrawItem, shapeNames, ShapeType } from "@/index";
 import { defaultStyle } from "@/core/components/image/index";
 import { v4 as uuid } from "uuid";
-import { getMapInfo } from "../../dialog/basemap/index";
+import { getMapInfo, SelectMapImageProps } from "../../dialog/basemap/index";
 import { selectAI } from "../../dialog/ai";
 import { drawPlatformResource } from "../../platform/platform-draw";
 import { selectFile } from "@/utils/dom";
@@ -192,42 +192,8 @@ export const getMapImageItem = (url: string, size: Size) => ({
   mat: [1, 0, 0, 1, 0, 0],
 });
 
-export const selectMap = async () => {
-  const info = await getMapInfo({
-    activeGroupIndex: 1,
-    tileGroups: [
-      {
-        name: '高德影像地图',
-        tiles: [
-          {
-            minimumLevel: 1,
-            maximumLevel: 18,
-            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=6&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
-          },
-          {
-            minimumLevel: 1,
-            maximumLevel: 18,
-            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
-          },
-        ]
-      },
-      {
-        name: '高德矢量地图',
-        tiles: [
-          {
-            minimumLevel: 1,
-            maximumLevel: 18,
-            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
-          },
-          {
-            minimumLevel: 1,
-            maximumLevel: 18,
-            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
-          },
-        ]
-      },
-    ]
-  });
+export const selectMap = async (args: SelectMapImageProps) => {
+  const info = await getMapInfo(args);
   const url = await window.platform.uploadResourse(
     new File([info.blob], "map.png")
   );
@@ -235,20 +201,6 @@ export const selectMap = async () => {
   return { url, info };
 };
 
-export const dbImage: MenuItem = {
-  value: uuid(),
-  icon: "",
-  name: "底图",
-  children: [
-    {
-      value: uuid(),
-      icon: "",
-      name: "高德地图",
-      handler: selectMap
-    },
-  ],
-};
-
 export const showThree = ref(false);
 export const test: MenuItem = {
   value: uuid(),
@@ -264,7 +216,6 @@ export const test: MenuItem = {
         showThree.value = !showThree.value;
       },
     },
-    ...dbImage.children!,
     {
       value: uuid(),
       icon: "",

+ 7 - 3
src/example/dialog/basemap/index.ts

@@ -2,15 +2,19 @@ import { markRaw, reactive } from "vue";
 import selectMapImage from "./leaflet/index.vue";
 import { Size } from "@/utils/math";
 import { Tile } from "./leaflet/useLeaflet";
+import { LatLng } from "leaflet";
 
+export type SearchResultItem = { address: string, latlng: LatLng }
+export type SearchAPI = (keyword: string) => Promise<SearchResultItem[]>
 export type BasemapInfo = { blob: Blob; size: Size };
 export type TileGroup = {
   tiles: Tile[];
   name: string;
 };
 export type SelectMapImageProps = {
-    tileGroups: TileGroup[]
-    activeGroupIndex?: number;
+  tileGroups: TileGroup[]
+  activeGroupIndex?: number;
+  search: SearchAPI
 }
 
 type Props = {
@@ -31,7 +35,7 @@ export const props = reactive({
 export const getMapInfo = (args: SelectMapImageProps) =>
   new Promise<BasemapInfo>((resolve, reject) => {
     props.content = markRaw(selectMapImage);
-    props.title = "选择地图底图";
+    props.title = "选择地图位置";
     props.args = args
     props.visiable = true;
     props.submit = (info: BasemapInfo) => {

+ 126 - 38
src/example/dialog/basemap/leaflet/index.vue

@@ -2,10 +2,10 @@
   <div class="map-layout">
     <div class="search">
       <el-input
-        :model-value="keyword[searchType]"
-        @update:model-value="(val) => (keyword[searchType] = val)"
+        v-model="keyword"
         :placeholder="`请输入${searchName}`"
         class="input-with-select"
+        @keydown.enter="searchHandler"
       >
         <template #prepend>
           <el-select v-model="searchType" style="width: 100px">
@@ -18,13 +18,37 @@
         </template>
       </el-input>
 
-      <div class="result" v-if="mark || mark === null">
+      <div
+        class="latlng-result"
+        v-if="searchType === 'latlng' && (mark || mark === null)"
+        :class="{ success: mark }"
+      >
         <template v-if="mark">
           <h3>经纬度定位成功</h3>
-          <p>精度</p>
+          <p>经度:{{ mark.lat }}</p>
+          <p>纬度:{{ mark.lng }}</p>
+        </template>
+        <template v-else>
+          <h3>经纬度定位失败</h3>
+          <p>请输入正确的经纬度格式</p>
+          <p>纬度,经度 (例如23.11766,113.28122)</p>
+          <p>纬度范围:-90到90</p>
+          <p>经度范围:-180到180</p>
         </template>
       </div>
+
+      <div class="name-result" v-if="searchType === 'name' && options.length">
+        <div
+          class="address-item operate"
+          v-for="option in options"
+          :class="{ active: activeOption === option }"
+          @click="clickOption(option)"
+        >
+          {{ option.address }}
+        </div>
+      </div>
     </div>
+
     <div class="map" ref="mapEle">
       <div class="tiles-select">
         <el-dropdown placement="bottom-end">
@@ -47,8 +71,15 @@
 
 <script lang="ts" setup>
 import { computed, ref, shallowRef, watch, watchEffect } from "vue";
-import { isValidLatLng, useLMap, useSetLTileLayers } from "./useLeaflet";
-import { BasemapInfo, SelectMapImageProps } from "../index";
+import {
+  getCurrentLatlng,
+  LatLng,
+  latlngStrTransform,
+  useLMap,
+  useOnlyMarker,
+  useSetLTileLayers,
+} from "./useLeaflet";
+import { BasemapInfo, SearchResultItem, SelectMapImageProps } from "../index";
 import html2canvas from "html2canvas";
 import {
   ElInput,
@@ -64,18 +95,7 @@ const props = defineProps<SelectMapImageProps>();
 const mapEle = shallowRef<HTMLDivElement>();
 const lMap = useLMap(mapEle);
 const setTileLayers = useSetLTileLayers(lMap);
-const groupIndex = ref(0);
-const tiles = computed(() => props.tileGroups[groupIndex.value].tiles);
-
-watchEffect(
-  () => {
-    if (props.activeGroupIndex) {
-      groupIndex.value = props.activeGroupIndex;
-    }
-  },
-  { flush: "sync" }
-);
-
+const groupIndex = ref(props.activeGroupIndex || 0);
 watchEffect(
   () => {
     if (groupIndex.value > props.tileGroups.length - 1) {
@@ -84,10 +104,8 @@ watchEffect(
   },
   { flush: "sync" }
 );
-
-watchEffect(() => {
-  setTileLayers(tiles.value);
-});
+const tiles = computed(() => props.tileGroups[groupIndex.value].tiles);
+watchEffect(() => setTileLayers(tiles.value));
 
 const searchTypes = [
   { label: "经纬度", value: "latlng" },
@@ -97,24 +115,43 @@ const searchType = ref(searchTypes[0].value as "latlng" | "name");
 const searchName = computed(
   () => searchTypes.find((item) => item.value === searchType.value)!.label
 );
-const keyword = ref({
-  latlng: "",
-  name: "",
+const keyword = ref("");
+const mark = useOnlyMarker(lMap, ref<LatLng | null>());
+watch(searchType, () => {
+  mark.value = undefined;
+  keyword.value = "";
 });
-const mark = ref<{ lat: number; lng: number } | null>();
 
-const search = (type: "latlng" | "name", keyword: string) => {
-  mark.value = undefined;
-  if (type === "latlng") {
-    const [lng, lat] = keyword.split(",").map((s) => Number(s.trim()));
-    if (!isValidLatLng(lat, lng)) {
-      mark.value = null;
-    } else {
-      mark.value = { lat, lng };
-    }
+const options = ref<SearchResultItem[]>([]);
+const activeOption = ref<SearchResultItem>();
+const clickOption = (item: SearchResultItem) => {
+  activeOption.value = item;
+  mark.value = item.latlng;
+};
+let token = 0;
+const searchHandler = async () => {
+  const currentToken = ++token;
+  if (searchType.value === "latlng") {
+    mark.value = latlngStrTransform(keyword.value);
+    return;
+  }
+  options.value = [];
+  activeOption.value = undefined;
+
+  if (!keyword.value) return;
+
+  const result = await props.search(keyword.value);
+  if (currentToken === token) {
+    options.value = result;
   }
 };
-watch([searchType, keyword], ([type, keyword]) => search(type, keyword[type]));
+
+getCurrentLatlng().then((latlng) => {
+  if (!keyword.value && mark.value === undefined && searchType.value === "latlng") {
+    keyword.value = `${latlng.lat},${latlng.lng}`;
+    searchHandler();
+  }
+});
 
 const submit = async (): Promise<BasemapInfo> => {
   if (!lMap.value) {
@@ -180,8 +217,59 @@ defineExpose({ submit });
 .tiles-select {
   position: absolute;
   z-index: 9999;
-  right: 20px;
-  top: 20px;
+  right: 0px;
+  top: 0px;
+}
+
+.latlng-result {
+  margin-top: 16px;
+  border-radius: 2px;
+  border: 1px solid #d9d9d9;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  justify-content: center;
+  padding: 16px;
+
+  p {
+    font-size: 14px;
+    color: #a7a7a7;
+    line-height: 22px;
+    min-width: 144px;
+    text-align: left;
+  }
+
+  h3 {
+    color: #f56c6c;
+    font-weight: bold;
+    font-size: 14px;
+    margin-bottom: 8px;
+  }
+
+  &.success h3 {
+    color: #67c23a;
+  }
+}
+
+.name-result {
+  margin-top: 10px;
+  border: 1px solid #d9d9d9;
+  border-radius: 2px;
+
+  .address-item {
+    padding: 10px 6px;
+    margin: 0 10px;
+    cursor: pointer;
+
+    &.active {
+      background: var(--el-color-primary-light-9);
+    }
+
+    &:not(:last-child) {
+      padding-bottom: 6px;
+      border-bottom: 1px solid #d9d9d9;
+    }
+  }
 }
 </style>
 

+ 26 - 0
src/example/dialog/basemap/leaflet/mock.ts

@@ -0,0 +1,26 @@
+import { SearchResultItem } from "..";
+import { latlngStrTransform } from "./useLeaflet";
+
+export const gdSearch = (val: string): Promise<SearchResultItem[]> => {
+  const key = "3bddec1685d461c2271a6099cde02fd2";
+  const url = `https://restapi.amap.com/v3/geocode/geo?address=${encodeURIComponent(
+    val
+  )}&key=${key}`;
+
+  return fetch(url)
+    .then((res) => res.json())
+    .then((res) => {
+      if (res.info !== "OK") {
+        throw res.info;
+      }
+      console.log(res);
+      const items = res.geocodes
+        .map((item: any) => ({
+          id: item.location,
+          address: item.formatted_address,
+          latlng: latlngStrTransform(item.location.split(',').reverse().join(','))
+        }))
+        .slice(0, 10);
+      return items;
+    });
+};

+ 71 - 15
src/example/dialog/basemap/leaflet/useLeaflet.ts

@@ -1,13 +1,13 @@
 import { mergeFuns } from "@/utils/shared";
-import { map, Map, tileLayer, } from "leaflet";
-import 'leaflet/dist/leaflet.css'
+import { map, Map, tileLayer, marker } from "leaflet";
+import "leaflet/dist/leaflet.css";
 import { onUnmounted, Ref, shallowRef, watch, watchEffect } from "vue";
 
 export const useLMap = (domRef: Ref<HTMLDivElement | undefined>) => {
   const lMap = shallowRef<Map>();
   const init = (dom: HTMLDivElement) => {
     const mapi = map(dom, {
-      center: [22.364093, 113.600356],
+      center: [39.90923, 116.397428],
       zoom: 18,
       preferCanvas: true,
       attributionControl: false,
@@ -68,29 +68,85 @@ export const useSetLTileLayers = (lMap: Ref<Map | undefined>) => {
   };
   let _clear: (() => void) | null = null;
 
-  onUnmounted(clear)
+  onUnmounted(clear);
 
   return (tiles: Tile[]) => {
     clear();
     _clear = watch(
       lMap,
       (lMap, _, onCleanup) => {
-        lMap && tiles && onCleanup(initTileLayers(lMap, tiles).onDestory)
+        lMap && tiles && onCleanup(initTileLayers(lMap, tiles).onDestory);
       },
       { immediate: true }
     );
   };
 };
 
-export function isValidLatLng(lat: number, lng: number) {
+export const useOnlyMarker = (
+  lMap: Ref<Map | undefined>,
+  latlng: Ref<LatLng | null | undefined>
+) => {
+  const init = (lMap: Map, latlng: LatLng) => {
+    const mark = marker(latlng);
+    lMap.panTo(latlng);
+    // lMap.addLayer(mark);
+    return () => mark.remove();
+  };
+  watchEffect((onCleanup) => {
+    latlng.value && lMap.value && onCleanup(init(lMap.value, latlng.value));
+  });
+
+  return latlng;
+};
+
+export type LatLng = {
+  lat: number;
+  lng: number;
+};
+
+export function isValidLatLng(latlng: LatLng) {
   return (
-    typeof lat === 'number' && 
-    typeof lng === 'number' &&
-    !isNaN(lat) && 
-    !isNaN(lng) &&
-    lat >= -90 && 
-    lat <= 90 &&
-    lng >= -180 && 
-    lng <= 180
+    typeof latlng.lat === "number" &&
+    typeof latlng.lng === "number" &&
+    !isNaN(latlng.lat) &&
+    !isNaN(latlng.lng) &&
+    latlng.lat >= -90 &&
+    latlng.lat <= 90 &&
+    latlng.lng >= -180 &&
+    latlng.lng <= 180
   );
-}
+}
+
+export const latlngStrTransform = (latlng: string) => {
+  const [lat, lng] = latlng.split(",").map((s) => Number(s.trim()));
+  if (!isValidLatLng({ lat, lng })) {
+    return null;
+  } else {
+    return { lat, lng };
+  }
+};
+
+export const getCurrentLatlng = async () => {
+  if (!("geolocation" in navigator)) {
+    throw "浏览器不支持地理位置功能";
+  }
+  return new Promise<LatLng>((resolve, reject) => {
+    navigator.geolocation.getCurrentPosition(
+      (position) => {
+        // 成功回调
+        const lat = position.coords.latitude; // 纬度
+        const lng = position.coords.longitude; // 经度
+        resolve({ lat, lng })
+      },
+      (error) => {
+        reject(error.message)
+      },
+      {
+        // 可选参数
+        enableHighAccuracy: true, // 高精度模式(耗电)
+        timeout: 10000, // 超时时间(毫秒)
+        maximumAge: 10000, // 缓存位置的最大年龄(毫秒)
+      }
+    );
+  });
+};

+ 0 - 1
src/example/fuse/enter-mix.ts

@@ -24,7 +24,6 @@ const login = (isBack = true) => {
     return;
   }
 
-  console.log(import.meta.env.VITE_LOGIN_VIEW);
   if (import.meta.env.VITE_LOGIN_VIEW) {
     const p: any = { ...params.value };
     delete p.token;

+ 24 - 3
src/example/fuse/enter.ts

@@ -5,6 +5,7 @@ import { token, params, urlUpdateQuery, urlGetQuery } from "../env";
 import { genLoading } from "../loadding";
 import { tempStrFill } from "@/utils/shared";
 import { ElMessage } from "element-plus";
+import { gdSearch } from "../dialog/basemap/leaflet/mock";
 
 const SCENE_TYPE = {
   fuse: "fuse",
@@ -120,8 +121,8 @@ const after = async (fet: Promise<Response>) => {
   const res = await fet.then((res) => res.json());
   if (res.code === 8034) {
     setTimeout(() => {
-      history.back()
-    }, 1000)
+      history.back();
+    }, 1000);
     throw `${res.message},即将退出`;
   }
 
@@ -318,10 +319,28 @@ const getTableTemp = () => {
   if (!title) {
     title = "默认标题";
   }
-  console.log(table, title)
   return { table, title };
 };
 
+const getTileGroups = async () => {
+  const data = await get(`fusion/notAuth/getMapConfig`, {});
+  return data.map((item: any) => {
+    const mitem = JSON.parse(item.mapUrl);
+    return {
+      name: item.name,
+      tiles: mitem.map((tileItem: any) => ({
+        url: tileItem.tempUrl,
+        minimumLevel: tileItem.minimumLevel || 1,
+        maximumLevel: tileItem.maximumLevel || 18,
+      })),
+    };
+  });
+};
+
+const searchAddress = (keyword: string) => {
+  return gdSearch(keyword)
+}
+
 window.platform = {
   login,
   resourceURLS,
@@ -334,6 +353,8 @@ window.platform = {
   uploadResourse,
   getTabulationId,
   getTableTemp,
+  getTileGroups,
+  searchAddress
 };
 
 /* @vite-ignore */

+ 16 - 14
src/example/fuse/views/tabulation/slide.vue

@@ -24,6 +24,8 @@ import { v4 as uuid } from "uuid";
 import SlideIcons from "@/example/components/slide/slide-icons.vue";
 import { iconGroups } from "@/example/constant.ts";
 import { Size } from "@/utils/math.ts";
+import { loading } from "@/example/loadding.ts";
+import { TileGroup } from "@/example/dialog/basemap/index.ts";
 
 const emit = defineEmits<{
   (e: "updateMapImage", v: { url: string; size: Size }): void;
@@ -42,24 +44,24 @@ const paper = reactive({
 const drawMenu = copy(drawMenuRaw);
 drawMenu.children!.shift();
 
+let tileGroups: TileGroup[];
+loading(window.platform.getTileGroups()).then((data: any) => (tileGroups = data));
+
 const menus = reactive([
+  paper,
   {
     value: uuid(),
-    icon: "",
-    name: "底图",
-    children: [
-      {
-        value: uuid(),
-        icon: "",
-        name: "高德地图",
-        handler: async () => {
-          const result = await selectMap();
-          emit("updateMapImage", { url: result.url, size: result.info.size });
-        },
-      },
-    ],
+    icon: "drawing",
+    name: "地图",
+    handler: async () => {
+      const result = await selectMap({
+        search: window.platform.searchAddress,
+        activeGroupIndex: 0,
+        tileGroups,
+      });
+      emit("updateMapImage", { url: result.url, size: result.info.size });
+    },
   },
-  paper,
   drawMenu,
   {
     icon: "legend",