Skip to content

天地图

提示 非组件,请安装相应依赖后,直接复制粘贴代码并调整修改部分代码使用

目前整合的功能

  • 地图控件
  • 图层控制器和(批量)点标记
  • 点标记弹窗框和信息窗
  • geojson图层加载
  • 定位
  • poi位置搜索

源代码

tea-map

vue
<template>
  <!--  <input id="input" placeholder="请输入地址搜索" />-->
  <div id="container">
    <TeaMapController
      :map="map"
      :list="[
        { title: '图层示例1', onShow: onShow1 },
        {
          title: '图层示例2',
          children: [{ title: '图层示例2-1', onShow: onShow2 }],
        },
      ]"
    ></TeaMapController>
  </div>
</template>

<script setup lang="ts">
import { TeaDialog } from "@/components/dialog/vue3/TeaDialog.ts";
import { h, onMounted, ref } from "vue";
import TeaMapController from "./TeaMapController.vue";

let T = (window as any).T;
let map = ref(null);

// 初始化地图
const initMap = () => {
  // 请在开发前更换天地图tk
  const tk = "";
  let mapTD = new T.Map("container");
  mapTD.centerAndZoom(new T.LngLat(121.631155, 29.736966), 5); // 传参中心点经纬度,以及放大程度
  // 卫星图
  let satelliteLayer = new T.TileLayer(
    `http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles` +
      `&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
  );
  // 路网
  let roadNetLayer = new T.TileLayer(
    `http://t4.tianditu.com/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=${tk}`,
  );
  mapTD.addLayer(satelliteLayer);
  mapTD.addLayer(roadNetLayer);
  map.value = mapTD;
  // 在此处使用后面的set方法
};

// 添加地图控件
const setMapController = () => {
  //添加比例尺控件
  map.value.addControl(new T.Control.Zoom());
  // 其他控件参考http://lbs.tianditu.gov.cn/api/js4.0/examples.html
};

// 添加图层控件
const onShow1 = () => {
  return new Promise((resolve) => {
    // 此处可以调用后端接口获取点位
    let layer = [];
    const marker = setMarker({
      lng: 120.631155,
      lat: 28.736966,
    });
    layer.push(marker);
    resolve(layer);
  });
};
const onShow2 = () => {
  return new Promise((resolve) => {
    // 此处可以调用后端接口获取点位
    let layer = [];
    const marker = setMarker({
      lng: 121.631155,
      lat: 29.736966,
    });
    layer.push(marker);
    resolve(layer);
  });
};

// 创建单个标记点
const setMarker = (e: { lng: number; lat: number }) => {
  if (map.value && e) {
    const position = new T.LngLat(e.lng, e.lat); // Marker经纬度
    let marker = new T.Marker(position);
    setDialog(marker);
    setInfoWindow(position);
    return marker;
  } else {
    console.log("地图未加载或传参错误");
  }
};

// marker添加信息窗体
const setInfoWindow = (position: any) => {
  let content = ["这里是信息弹窗"];
  // 创建 infoWindow 实例
  let infoWindow = new T.InfoWindow(content.join("<br>"), {
    offset: new T.Point(0, -30),
  });
  infoWindow.setLngLat(position);
  // 打开信息窗体
  // map.value.addOverLay(infoWindow);
};

// marker添加弹出框
const setDialog = (marker: any) => {
  marker.addEventListener("click", () => {
    TeaDialog({
      props: {},
      content: h("div", {}, "弹出框内容"),
    });
  });
};

// 添加GeoJson图层
// 其他格式的矢量文件必须先转化为geojson
const setGeoJson = () => {
  if (T && map.value) {
    d3.json("/chongqing.json", function (data: any) {
      let overlay = new T.D3Overlay(
        (sel: any, transform: any) => {
          let upd = sel.selectAll("path.geojson").data(data.features);
          upd
            .enter()
            .append("path")
            .attr("class", "geojson")
            .attr("stroke", "white")
            .attr("fill", "red")
            .attr("fill-opacity", "0.5");
        },
        (sel: any, transform: any) => {
          sel.selectAll("path.geojson").each(function (d, i) {
            d3.select(this).attr("d", transform.pathFromGeojson);
          });
        },
      );
      map.value.addOverLay(overlay);
      overlay.bringToBack();
    });
  }
};

// 定位
// 注意:定位会导致setGeoJson()渲染出现bug,建议二者不要一起使用
const setLocation = () => {
  if (map.value) {
    let lo = new T.Geolocation();
    let fn = function (e: any) {
      map.value.centerAndZoom(e.lnglat, 15);
    };
    lo.getCurrentPosition(fn);
  }
};

// poi搜索地点 todo 需要配合下拉框
const setSearch = () => {
  const input = document.getElementById("input") as HTMLInputElement;
  const search = () => {
    // 创建搜索对象
    let localSearch = new T.LocalSearch(map.value, {
      pageCapacity: 10, //每页显示的数量
      onSearchComplete: function (res: any) {
        console.log(res);
      },
    });
    localSearch.search(input.value);
  };
  function debounce(fn: any, wait: any) {
    let timer: any = null;
    return function () {
      if (timer !== null) {
        clearTimeout(timer);
      }
      timer = setTimeout(fn, wait);
    };
  }
  input.addEventListener("input", debounce(search, 1000));
};

onMounted(() => {
  initMap();
});
</script>

<style scoped>
#container {
  height: 500px;
  width: 500px;
}

/* 去除水印 */
:deep(.tdt-control-copyright) {
  display: none !important;
}
:deep(.tdt-control) {
  display: none !important;
}
</style>

tea-map-controller

vue
<template>
  <div class="map-controller" v-if="map && layerMap">
    <div v-for="(item, index) in listWithId" :key="index">
      <el-popover
        popper-class="!bg-black/[.5] !shadow-none"
        placement="left"
        trigger="click"
        :disabled="!item.children"
      >
        <template #reference>
          <div
            class="map-controller-item"
            :class="{ 'map-controller-item-active': activeComputed(item.id) }"
            @click="onSelect(!activeComputed(item.id), item.id)"
          >
            <div v-if="item.icon">
              <img :src="item.icon" class="map-controller-icon" alt="" />
            </div>
            <div>
              {{ item.title }}
            </div>
          </div>
        </template>
        <div v-if="item.children" class="map-controller-item-next">
          <el-checkbox
            v-if="isCheckOut && !isSingle"
            v-model="layerMap.get(item.id).checkOut"
            label="全选"
            size="large"
            @change="
              (val: boolean) => {
                onSelectAll(val, item.children!);
              }
            "
          />
          <el-checkbox
            v-for="(itemA, indexA) in item.children"
            v-model="layerMap.get(itemA.id).show"
            :key="indexA"
            :label="itemA.title"
            size="large"
            @change="
              (val: boolean) => {
                onSelect(val, itemA.id);
              }
            "
            :checked="itemA.isShow"
          />
        </div>
      </el-popover>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
import { ElLoading } from "element-plus";
import type { PropType } from "@vue/runtime-core";

interface listItemType {
  icon?: string;
  title: string;
  isShow?: boolean;
  onShow?: Function;
  children?: Array<listItemType>;
}

interface listItemWithIdType extends listItemType {
  id: symbol;
  children?: Array<listItemWithIdType>;
}

const props = defineProps({
  map: {
    type: [Object, null] as PropType<Object | null>,
    required: true,
  },
  /**
   * [
   *  {
   *    icon: string, // 图标
   *    title: string, // 图层名
   *    isShow: boolean, // 是否默认选中
   *    onShow: Function<Promise>, // 传入一个返回值是promise的方法,resolve的值是layer数组,用于加载数据
   *    children: Array // 用于渲染二级菜单,配置了children的对象isShow和onShow属性不会生效
   *  }
   * ]
   */
  list: {
    type: Object as PropType<Array<listItemType>>,
    default: () => [],
  },
  // 是否开启单选模式
  isSingle: {
    type: Boolean,
    default: false,
  },
  // 是否开启全选选项
  isCheckOut: {
    type: Boolean,
    default: true,
  },
  // 是否在显示图层前重新调用接口加载数据
  isReload: {
    type: Boolean,
    default: false,
  },
});

const layerMap = ref(new Map());
const listWithId = ref<listItemWithIdType[]>([]);

const activeComputed = (id: symbol) => {
  return layerMap.value.get(id)?.show;
};

watch(
  () => props.list,
  () => {
    const processItem = (item: listItemType): listItemWithIdType => {
      const result: listItemWithIdType = {
        id: Symbol(),
        ...item,
        children: undefined, // 先初始化为undefined
      };
      if (item.children) {
        result.children = item.children.map((child) => processItem(child));
      }
      return result;
    };
    listWithId.value = props.list.map((item) => processItem(item));
    for (let index in listWithId.value) {
      // 有二级菜单
      if (listWithId.value[index].children) {
        layerMap.value.set(listWithId.value[index].id, {
          options: listWithId.value[index],
          checkOut: false,
        });
        listWithId.value[index].children!.forEach(
          (item: listItemWithIdType) => {
            layerMap.value.set(item.id, {
              options: item,
              layer: null,
              show: false,
              isChild: true,
              father: listWithId.value[index].id,
            });
            item.isShow && onSelect(true, item.id);
          },
        );
      } else {
        layerMap.value.set(listWithId.value[index].id, {
          options: listWithId.value[index],
          layer: null,
          show: false,
        });
        listWithId.value[index].isShow &&
          onSelect(true, listWithId.value[index].id);
      }
    }
  },
);

const onSelect = async (val: boolean, id: symbol) => {
  const addLayer = (layer) => {
    if (Array.isArray(layer)) {
      layer.forEach((item) => {
        props.map?.addOverLay(item);
      });
    } else {
      props.map?.addOverLay(layer);
    }
  };
  const removeLayer = (layer) => {
    if (Array.isArray(layer)) {
      layer.forEach((item) => {
        props.map?.removeOverLay(item);
      });
    } else {
      props.map?.removeOverLay(layer);
    }
  };
  let obj = layerMap.value.get(id);
  if (!obj || obj.options.children) return;
  // 关闭图层
  if (!val) {
    obj.show = false;
    if (obj.isChild) {
      const fa = layerMap.value.get(obj.father);
      if (fa) fa.checkOut = false;
    }
    obj.layer && removeLayer(obj.layer);
    return;
  }

  // 打开图层
  obj.show = true;
  // 是否需要重新加载数据
  let reloadFlag = !obj.layer || props.isReload;
  if (reloadFlag) {
    const loadingInstance = ElLoading.service();
    if (obj.layer && props.isReload) {
      removeLayer(obj.layer);
    }
    obj.layer = await obj.options.onShow();
    addLayer(obj.layer);
    loadingInstance.close();
  } else {
    addLayer(obj.layer);
  }
  // 单选模式
  if (props.isSingle) {
    layerMap.value.forEach((value, key) => {
      if (key !== id && value.show && value.layer) {
        value.show = false;
        removeLayer(value.layer);
      }
    });
  }
};

const onSelectAll = (val: boolean, children: Array<listItemWithIdType>) => {
  children.forEach((item: listItemWithIdType) => {
    onSelect(val, item.id);
  });
};
</script>

<style scoped>
.map-controller {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 10px;
  position: absolute;
  bottom: 20px;
  right: 20px;
  z-index: 999;
}
.map-controller-item {
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  padding: 10px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  user-select: none;
}
.map-controller-item-active {
  background-color: #25599f;
}
.map-controller-icon {
  height: 15px;
  width: 15px;
}
.map-controller-item-next {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
</style>