Forráskód Böngészése

📦 亮乡源地图构建1

快乐的梦鱼 1 napja
szülő
commit
6db35b6d7c

+ 2 - 1
src/api/inhert/VillageApi.ts

@@ -214,10 +214,11 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
   async claimVallage(data: any) {
     return this.post('/village/village/addVillageClaim', '认领村落', data);
   }
-  async getVallageList(level?: number, status?: number) {
+  async getVallageList(level?: number, status?: number, region?: string) {
     const res = await this.get('/village/village/getList', '村落列表', {
       history_level: level,
       status,
+      region: region,
     });
     return transformArrayDataModel<VillageListItem>(VillageListItem, transformSomeToArray(res.data), `村落`, true);
   }

+ 85 - 0
src/api/light/LightVillageApi.ts

@@ -0,0 +1,85 @@
+import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import { transformSomeToArray } from '../Utils';
+
+export class VillageListItem extends DataModel<VillageListItem> {
+  constructor() {
+    super(VillageListItem, "活动列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      isLight: { clientSide: 'boolean' },
+    }
+    this._nameMapperServer = {
+      name: 'villageName',
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+    this._afterSolveServer = () => {
+      this.address = 
+        (this.province || '') + 
+        (this.city || '') + 
+        (this.district || '') + 
+        (this.township || '');
+      if (this.images && this.images && this.images.length > 0  ) {
+        this.image = this.images[0]
+      }
+      this.thumbnail = this.image;
+      this.title = this.villageName
+      this.isLight = Math.random() > 0.5;
+    }
+
+  }
+
+  id !: number;
+  province = '' as string|null;
+  city = '' as string|null;
+  district = '' as string|null;
+  township = '' as string|null;
+  address = '';
+  isLight = false;
+  isLightText = '';
+  lightValue = Math.random();
+  villageVolunteerId = null as number|null;
+  villageId !:number;
+  claimReason = '';
+  status = '';
+  statusText = '';
+  createdAt = null as Date|null;
+  updatedAt = null as Date|null;
+  deleteAt = null as Date|null;
+  image = '';
+  thumbnail = '';
+  images = [] as string[];
+  villageName = '';
+  title = '';
+  volunteerName = '';
+}
+export class LightVillageApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async getVillageList(level?: number, region?: number, status?: number, page?: number, pageSize?: number) {
+    const res = await this.get<{
+      data: any[],
+      total: number,
+    }>('/village/village/list', '乡源村落列表', {
+      history_level: level,
+      status,
+      region: region,
+      page: page,
+      pageSize: pageSize,
+    });
+    return transformArrayDataModel<VillageListItem>(VillageListItem, transformSomeToArray(res.requireData().data), `村落`, true);
+  }
+}
+
+export default new LightVillageApi();

+ 37 - 1
src/api/map/MapApi.ts

@@ -67,7 +67,43 @@ export class MapApi extends MapServerRequestModule<DataModel> {
   loadCityData() {
     return new Promise((resolve, reject) => {
       uni.request({
-        url: 'https://mn.wenlvti.net/app_static/xiangan/city-data.json',
+        url: 'https://mn.wenlvti.net/app_static/xiangyuan/data/ChinaCityData.slim.json',
+        method: 'GET',
+        success(result) {
+          if (result.statusCode === 200) {
+            resolve(result.data);
+          } else {
+            reject(new Error(`请求失败,状态码:${result.statusCode}`));
+          }
+        },
+        fail(error) {
+          reject(error);
+        }
+      })
+    });
+  }
+  loadCountryGeoJsonData() {
+    return new Promise((resolve, reject) => {
+      uni.request({
+        url: 'https://mn.wenlvti.net/app_static/xiangyuan/data/100000_full.json',
+        method: 'GET',
+        success(result) {
+          if (result.statusCode === 200) {
+            resolve(result.data);
+          } else {
+            reject(new Error(`请求失败,状态码:${result.statusCode}`));
+          }
+        },
+        fail(error) {
+          reject(error);
+        }
+      })
+    });
+  }
+  loadGeoJsonData(code: string) {
+    return new Promise((resolve, reject) => {
+      uni.request({
+        url: `https://mn.wenlvti.net/app_static/xiangyuan/data/${code}_full.json`,
         method: 'GET',
         success(result) {
           if (result.statusCode === 200) {

+ 1 - 1
src/common/components/parts/Box.vue

@@ -8,7 +8,7 @@
     :radius="30" 
     shadow="light"
   >
-    <SubTitle :title="title" :showMore="showMore" @moreClicked="emit('moreClicked')">
+    <SubTitle v-if="title" :title="title" :showMore="showMore" @moreClicked="emit('moreClicked')">
       <template #left>
         <FlexRow align="center">
           <Icon :icon="icon" :size="36" />

+ 0 - 1
src/common/components/parts/Box2LineImageRightShadow.vue

@@ -16,7 +16,6 @@
         :width="wideImage ? 250 : 150"
         :height="150"
         :radius="10"
-        round
         :flexShrink="0"
         :src="image"
         mode="aspectFill"

+ 0 - 1
src/common/components/parts/Box2LineLargeImageUserShadow.vue

@@ -24,7 +24,6 @@
       :height="300" 
       width="100%"
       :radius="20"
-      round
       :src="image" 
       mode="aspectFill" 
     />

+ 168 - 0
src/common/utils/geoJsonToWechatMap.ts

@@ -0,0 +1,168 @@
+import type { MapLocationPoint, MapPolygon, MapPolyline } from '@/types/Map';
+
+/** GeoJSON 坐标 [经度, 纬度(, 可选高度)] */
+export type GeoJsonPosition = [number, number] | [number, number, number];
+
+export type GeoJsonGeometry =
+  | { type: 'Polygon'; coordinates: GeoJsonPosition[][] }
+  | { type: 'MultiPolygon'; coordinates: GeoJsonPosition[][][] }
+  | { type: 'LineString'; coordinates: GeoJsonPosition[] }
+  | { type: 'MultiLineString'; coordinates: GeoJsonPosition[][] }
+  | { type: 'GeometryCollection'; geometries: GeoJsonGeometry[] }
+  | { type: string; coordinates?: unknown; geometries?: unknown };
+
+export interface GeoJsonFeature {
+  type: 'Feature';
+  properties?: Record<string, unknown> | null;
+  geometry: GeoJsonGeometry | null;
+}
+
+export interface GeoJsonFeatureCollection {
+  type: 'FeatureCollection';
+  features: GeoJsonFeature[];
+}
+
+export interface GeoJsonToWechatMapOptions {
+  fillColor?: string;
+  strokeColor?: string;
+  strokeWidth?: number;
+  /** LineString / MultiLineString 使用的颜色 */
+  lineColor?: string;
+  lineWidth?: number;
+  level?: MapPolygon['level'];
+  /** 仅转换满足的要素(例如按 adcode 过滤) */
+  featureFilter?: (feature: GeoJsonFeature) => boolean;
+}
+
+const defaultOptions: Required<
+  Pick<
+    GeoJsonToWechatMapOptions,
+    'fillColor' | 'strokeColor' | 'strokeWidth' | 'lineColor' | 'lineWidth' | 'level'
+  >
+> = {
+  fillColor: '#3388ff33',
+  strokeColor: '#3388ff',
+  strokeWidth: 1,
+  lineColor: '#3388ff',
+  lineWidth: 2,
+  level: 'abovelabels',
+};
+
+function lngLatToPoint(lngLat: GeoJsonPosition): MapLocationPoint {
+  return { longitude: lngLat[0], latitude: lngLat[1] };
+}
+
+/** GeoJSON 多边形环通常为闭合环;若未闭合则补全,满足 map 组件闭合多边形要求 */
+function ringToClosedPolygonPoints(ring: GeoJsonPosition[]): MapLocationPoint[] {
+  if (ring.length < 3) return [];
+  const pts = ring.map(lngLatToPoint);
+  const first = pts[0];
+  const last = pts[pts.length - 1];
+  if (first.longitude !== last.longitude || first.latitude !== last.latitude) {
+    pts.push({ latitude: first.latitude, longitude: first.longitude });
+  }
+  return pts;
+}
+
+function appendPolygonOuterRing(
+  rings: GeoJsonPosition[][] | undefined,
+  o: typeof defaultOptions,
+  polygons: MapPolygon[]
+): void {
+  if (!rings?.length) return;
+  const outer = rings[0];
+  const points = ringToClosedPolygonPoints(outer);
+  if (points.length < 4) return;
+  polygons.push({
+    points,
+    fillColor: o.fillColor,
+    strokeColor: o.strokeColor,
+    strokeWidth: o.strokeWidth,
+    level: o.level,
+  });
+}
+
+function appendLine(coords: GeoJsonPosition[] | undefined, o: typeof defaultOptions, polylines: MapPolyline[]): void {
+  if (!coords?.length) return;
+  const points = coords.map(lngLatToPoint);
+  if (points.length < 2) return;
+  polylines.push({
+    points,
+    color: o.lineColor,
+    width: o.lineWidth,
+    level: o.level,
+  });
+}
+
+function appendFromGeometry(geometry: GeoJsonGeometry | null, o: typeof defaultOptions, polygons: MapPolygon[], polylines: MapPolyline[]): void {
+  if (!geometry) return;
+  switch (geometry.type) {
+    case 'Polygon':
+      appendPolygonOuterRing(geometry.coordinates as GeoJsonPosition[][], o, polygons);
+      break;
+    case 'MultiPolygon':
+      for (const polygonCoords of (geometry.coordinates as GeoJsonPosition[][][])) {
+        appendPolygonOuterRing(polygonCoords, o, polygons);
+      }
+      break;
+    case 'LineString':
+      appendLine(geometry.coordinates as GeoJsonPosition[], o, polylines);
+      break;
+    case 'MultiLineString':
+      for (const line of (geometry.coordinates as GeoJsonPosition[][])) {
+        appendLine(line as GeoJsonPosition[], o, polylines);
+      }
+      break;
+    case 'GeometryCollection':
+      for (const g of (geometry.geometries as GeoJsonGeometry[])) {
+        appendFromGeometry(g, o, polygons, polylines);
+      }
+      break;
+    default:
+      break;
+  }
+}
+
+function mergeOptions(options?: GeoJsonToWechatMapOptions): typeof defaultOptions {
+  return {
+    ...defaultOptions,
+    ...options,
+    level: options?.level ?? defaultOptions.level,
+  };
+}
+
+/**
+ * 将 GeoJSON(FeatureCollection 或单个 Feature)转为微信小程序 map 的 polygons / polyline 数据源。
+ * - 坐标系:与数据一致(国内区划数据多为 GCJ-02,与微信 map 一致)。
+ * - GeoJSON 坐标顺序为 [longitude, latitude]。
+ * - Polygon / MultiPolygon:仅取外环填充并描边;洞内环(hole)小程序 map 无法用单 polygon 表达,当前版本忽略。
+ */
+export function geoJsonToWechatMapShapes(
+  data: unknown,
+  options?: GeoJsonToWechatMapOptions
+): { polygons: MapPolygon[]; polylines: MapPolyline[] } {
+  const o = mergeOptions(options);
+  const polygons: MapPolygon[] = [];
+  const polylines: MapPolyline[] = [];
+
+  if (!data || typeof data !== 'object') {
+    return { polygons, polylines };
+  }
+
+  const root = data as Record<string, unknown>;
+
+  if (root.type === 'FeatureCollection' && Array.isArray(root.features)) {
+    for (const f of root.features as GeoJsonFeature[]) {
+      if (options?.featureFilter && !options.featureFilter(f)) continue;
+      appendFromGeometry(f.geometry, o, polygons, polylines);
+    }
+  } else if (root.type === 'Feature') {
+    const f = root as unknown as GeoJsonFeature;
+    if (options?.featureFilter && !options.featureFilter(f)) {
+      return { polygons, polylines };
+    }
+    appendFromGeometry(f.geometry, o, polygons, polylines);
+  }
+
+  return { polygons, polylines };
+}

+ 132 - 22
src/pages/components/LightMap.vue

@@ -1,34 +1,103 @@
 <template>
-  <map 
-    id="prevMap"
-    map-id="prevMap"
-    :style="{ width: '100%', height: '400rpx', borderRadius: '10rpx', overflow: 'hidden' }"
-    :markers="mapLoader.content.value || []"
-    :scale="10"
-    :longitude="AppCofig.defaultLonLat[0]"
-    :latitude="AppCofig.defaultLonLat[1]"
-  />
+  <div class="light-map">
+    <map 
+      id="prevMap"
+      map-id="prevMap"
+      class="light-map-map"
+      :markers="mapLoader.content.value || []"
+      :polygons="polygon"
+      :polyline="polyline"
+      :enable-poi="false"
+      :scale="10"
+      :longitude="AppCofig.defaultLonLat[0]"
+      :latitude="AppCofig.defaultLonLat[1]"
+    />
+    <SimpleDropDownPicker 
+      class="light-map-region-picker" 
+      v-model:modelValue="selectedRegion" 
+      :columns="regionLoader.content.value" 
+    />
+  </div>
 </template>
 
 <script setup lang="ts">
+import { getCurrentInstance, ref, watch } from 'vue';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
-import { getCurrentInstance } from 'vue';
-import VillageApi from '@/api/inhert/VillageApi';
+import { geoJsonToWechatMapShapes } from '@/common/utils/geoJsonToWechatMap';
+import LightVillageApi from '@/api/light/LightVillageApi';
+import MapApi from '@/api/map/MapApi';
 import AppCofig from '@/common/config/AppCofig';
+import type { MapMarker, MapPolygon, MapPolyline } from '@/types/Map';
+import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
+import CommonContent from '@/api/CommonContent';
 
 const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
-const mapLoader = useSimpleDataLoader(async () => {
-  const res = (await VillageApi.getVallageList(undefined, 1)).map((p, i) => ({
-    ...p,
-    id: p.id ?? i,
-    title: p.villageName,
-    longitude: Number(p.longitude),
-    latitude: Number(p.latitude),
-    width: 30,
-    height: 30,
-    iconPath: p.thumbnail || p.image,
+const selectedRegion = ref<number>();
+
+const regionLoader = useSimpleDataLoader(async () => {
+  return (await CommonContent.getCategoryChildList(1)).map(p => ({
+    id: p.id,
+    name: p.title,
   }));
+});
+const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
+  const res = (await LightVillageApi.getVillageList(undefined, selectedRegion.value, undefined, 1, 12)).map((p, i) => {
+    const maker : MapMarker = {
+      ...p,
+      id: p.id ?? i,
+      title: p.villageName,
+      longitude: Number(p.longitude),
+      latitude: Number(p.latitude),
+      width: 30,
+      height: 30,
+      iconPath: '',
+      callout: {
+        content: p.villageName,
+        color: '#000000',
+        fontSize: 12,
+        bgColor: '#ffffff',
+        borderRadius: 5,
+      },
+    }
+
+    if (p.isLight) {
+      if (p.lightValue >= 1) {
+        maker.width = 50;
+        maker.height = 50;
+        maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/StarX.png`;
+      } else if (p.lightValue >= 0.5) {
+        maker.width = 40;
+        maker.height = 40;
+        maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/StarLarge.png`;
+      } else if (p.lightValue >= 0.25) {
+        maker.width = 30;
+        maker.height = 30;
+        maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/StarMid.png`;
+      } else{
+        maker.width = 20;
+        maker.height = 20;
+        maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/StarSmall.png`;
+      }
+    } else {
+      maker.width = 15;
+      maker.height = 15;
+      maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/StarNotLight1.png`;
+    }
+
+    return maker as MapMarker;
+  });
+
+  const countryGeoJsonData = await MapApi.loadCountryGeoJsonData();
+  const { polygons, polylines: geoPolylines } = geoJsonToWechatMapShapes(countryGeoJsonData, {
+    fillColor: '#344A41',
+    strokeColor: '#4AB58A',
+    strokeWidth: 2,
+    level: 'abovebuildings',
+  });
+  polygon.value.push(...polygons);
+  polyline.value = geoPolylines;
+
   setTimeout(() => {
     mapCtx.includePoints({
       points: res.map(p => {
@@ -44,8 +113,49 @@ const mapLoader = useSimpleDataLoader(async () => {
       padding: [20, 20, 20, 20],
     });
   }, 200);
+
   return res;
 });
 
+const polygon = ref<MapPolygon[]>([
+  {
+    fillColor: '#000',
+    level: 'abovebuildings',
+    points: [
+      { latitude: -89, longitude: 0 },
+      { latitude: 89, longitude: 0 },
+      { latitude: 89, longitude: 179.999 },
+      { latitude: -89, longitude: 179.999 },
+    ]
+  }
+]);
+const polyline = ref<MapPolyline[]>([]);
+
+watch(selectedRegion, async () => {
+  mapLoader.loadData(undefined, true);
+});
+
+</script>
+
+<style lang="scss">
+.light-map {
+  position: relative;
+  width: 100%;
+  height: 600rpx;
+  border-radius: 30rpx;
+  overflow: hidden;
+
+  .light-map-map {
+    width: 100%;
+    height: 600rpx;
+  }
 
-</script>
+  .light-map-region-picker {
+    position: absolute;
+    bottom: 20rpx;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 100;
+  }
+}
+</style>

+ 3 - 3
src/pages/home/index.vue

@@ -12,13 +12,13 @@
       <Image src="https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/ButtonTitle.png" width="430rpx" mode="widthFix" />
       <Image src="https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/ButtonSubTitle.png" width="290rpx" mode="widthFix" />
     </FlexCol>
-    <Box title="村社分布" icon="https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/icon-pin-distance.png">
-      <LightMap />
-      <Height :height="20" />
+    <LightMap />
+    <Box icon="https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/icon-pin-distance.png">
       <ProvideVar :vars="{
         GridItemIconSize: 90,
         GridItemBackgroundColor: 'transparent',
         GridItemPaddingHorizontal: 0,
+        GridItemPaddingVertical: 0,
       }">
         <Grid :borderGrid="false" :mainAxisCount="4">
           <GridItem title="匠韵薪传" icon="https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/IconInherit.png" touchable @click="goList('匠韵薪传')" />

+ 1 - 1
src/pages/index.vue

@@ -6,7 +6,7 @@
         :title="title"
         :titleScroll="false"
         backgroundColor="transparent"
-        textColor="black"
+        textColor="white"
         align="left"
       />
       <HomeIndex v-if="tabIndex === 0" @goDiscover="tabIndex = 1" />

BIN
src/static/images/StarLarge.png


BIN
src/static/images/StarMid.png


BIN
src/static/images/StarNotLight1.png


BIN
src/static/images/StarSmall.png


BIN
src/static/images/StarX.png


+ 155 - 0
src/types/Map.ts

@@ -0,0 +1,155 @@
+/** 经纬度点(GCJ-02),与微信 map 组件一致 */
+export interface MapLocationPoint {
+  latitude: number;
+  longitude: number;
+}
+
+/** marker 上气泡 callout */
+export interface MapMarkerCallout {
+  content?: string;
+  color?: string;
+  fontSize?: number;
+  borderRadius?: number;
+  borderWidth?: number;
+  borderColor?: string;
+  bgColor?: string;
+  padding?: number;
+  /** BYCLICK: 点击显示;ALWAYS: 常显 */
+  display?: 'BYCLICK' | 'ALWAYS';
+  textAlign?: 'left' | 'right' | 'center';
+  anchorX?: number;
+  anchorY?: number;
+  collision?: string;
+}
+
+/** marker 自定义气泡 customCallout(存在时忽略 callout 与 title) */
+export interface MapMarkerCustomCallout {
+  display?: 'BYCLICK' | 'ALWAYS';
+  anchorX?: number;
+  anchorY?: number;
+}
+
+/** marker 旁 label */
+export interface MapMarkerLabel {
+  content?: string;
+  color?: string;
+  fontSize?: number;
+  /** @deprecated 文档标记废弃,优先使用 anchorX / anchorY */
+  x?: number;
+  /** @deprecated 文档标记废弃,优先使用 anchorX / anchorY */
+  y?: number;
+  anchorX?: number;
+  anchorY?: number;
+  borderWidth?: number;
+  borderColor?: string;
+  borderRadius?: number;
+  bgColor?: string;
+  padding?: number;
+  textAlign?: 'left' | 'right' | 'center';
+  collision?: string;
+}
+
+/** 经纬度在标注图标的锚点,默认底边中点 */
+export interface MapMarkerAnchor {
+  /** 横向 0~1 */
+  x: number;
+  /** 竖向 0~1 */
+  y: number;
+}
+
+/**
+ * 地图标记点 marker
+ * @see https://developers.weixin.qq.com/miniprogram/dev/component/map.html
+ */
+export interface MapMarker {
+  /** 标记点 id,点击事件回调会返回此 id */
+  id: number;
+  clusterId?: number;
+  joinCluster?: boolean;
+  latitude: number;
+  longitude: number;
+  title?: string;
+  zIndex?: number;
+  iconPath: string;
+  rotate?: number;
+  /** 透明度 0~1,默认 1 */
+  alpha?: number;
+  width?: number | string;
+  height?: number | string;
+  callout?: MapMarkerCallout;
+  customCallout?: MapMarkerCustomCallout;
+  label?: MapMarkerLabel;
+  anchor?: MapMarkerAnchor;
+  /** 无障碍访问描述 */
+  'aria-label'?: string;
+  /** alone | together */
+  collisionRelation?: string;
+  /** 如 poi、marker,多类型逗号分隔,如 "poi,marker" */
+  collision?: string;
+}
+
+/** polyline 折线上文字样式 */
+export interface MapPolylineTextStyle {
+  textColor?: string;
+  strokeColor?: string;
+  fontSize?: number;
+}
+
+/** polyline 分段文本 */
+export interface MapPolylineSegmentText {
+  name?: string;
+  startIndex?: number;
+  endIndex?: number;
+}
+
+/**
+ * 路线 polyline
+ * 彩虹线:points 含 n 个点时 colorList 宜为 n-1 段颜色;不足时余段与最后一色相同
+ * @see https://developers.weixin.qq.com/miniprogram/dev/component/map.html#polyline
+ */
+export interface MapPolyline {
+  points: MapLocationPoint[];
+  /** 十六进制颜色;存在 colorList 时被忽略 */
+  color?: string;
+  colorList?: string[];
+  width?: number;
+  dottedLine?: boolean;
+  arrowLine?: boolean;
+  arrowIconPath?: string;
+  borderColor?: string;
+  borderWidth?: number;
+  /** 压盖关系,默认 abovelabels */
+  level?: 'abovelabels' | 'abovebuildings' | 'aboveroads';
+  textStyle?: MapPolylineTextStyle;
+  segmentTexts?: MapPolylineSegmentText[];
+}
+
+/**
+ * 多边形 polygon(闭合区域)
+ * @see https://developers.weixin.qq.com/miniprogram/dev/component/map.html#polygon
+ */
+export interface MapPolygon {
+  /** [0,0] 为实线;如 [10,10] 表示 10px 实线 + 10px 空白循环 */
+  dashArray?: number[];
+  points: MapLocationPoint[];
+  strokeWidth?: number;
+  strokeColor?: string;
+  fillColor?: string;
+  zIndex?: number;
+  level?: 'abovelabels' | 'abovebuildings' | 'aboveroads';
+}
+
+/**
+ * 圆形 circle
+ * @see https://developers.weixin.qq.com/miniprogram/dev/component/map.html#circle
+ */
+export interface MapCircle {
+  latitude: number;
+  longitude: number;
+  /** 描边颜色 */
+  color?: string;
+  fillColor?: string;
+  radius: number;
+  strokeWidth?: number;
+  level?: 'abovelabels' | 'abovebuildings' | 'aboveroads';
+}