【VUE3】移动端:使用腾讯地图显示区域统计与放大显示资产点位

移动端:使用腾讯地图显示区域统计与放大显示资产点位

整合步骤总览

步骤 内容 涉及文件
步骤一 配置腾讯地图能力并注册页面 src/manifest.json(file:///e:/project/mobile-ui/src/manifest.json)、src/pages.json(file:///e:/project/mobile-ui/src/pages.json)
步骤二 准备资产接口与字典 src/services/asset.js(file:///e:/project/mobile-ui/src/services/asset.js)、src/utils/useDict.js(file:///e:/project/mobile-ui/src/utils/useDict.js)
步骤三 实现地图页面(聚合 / 详情双模式) src/pages/index/index-map.vue(file:///e:/project/mobile-ui/src/pages/index/index-map.vue)

整体逻辑:地图缩小时(缩放级别小于 15,聚合模式)请求「按村委统计资产数量」接口,在村委中心点显示数量气泡;地图放大时(缩放级别大于等于 15,详情模式)请求「资产列表」接口,按资产坐标显示具体点位。点击聚合气泡可放大进入详情模式,点击资产点弹出信息气泡。

步骤一:配置腾讯地图能力并注册页面

  1. 在 manifest.json 中配置腾讯地图 key:uni-app 的 <map> 组件在 H5 端依赖腾讯地图,key 位于 sdkConfigs.maps.tencent.key。(本文档已对该 key 做脱敏处理,实际使用时请填写自己的腾讯地图 key)
  2. 在 pages.json 中注册地图页面:路径为 pages/index/index-map,标题为「地图找地」。

完整文件:src/manifest.json

json 复制代码
{
    "name" : "智汇资产管理系统",
    "appid" : "__UNI__21D1B47",
    "description" : "",
    "versionName" : "1.0.0",
    "versionCode" : "100",
    "transformPx" : false,
    /* 5+App特有相关 */
    "app-plus" : {
        "usingComponents" : true,
        "nvueStyleCompiler" : "uni-app",
        "compilerVersion" : 3,
        "splashscreen" : {
            "alwaysShowBeforeRender" : true,
            "waiting" : true,
            "autoclose" : true,
            "delay" : 0
        },
        /* 模块配置 */
        "modules" : {},
        /* 应用发布信息 */
        "distribute" : {
            /* android打包配置 */
            "android" : {
                "permissions" : [
                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                    "<uses-feature android:name=\"android.hardware.camera\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                ]
            },
            /* ios打包配置 */
            "ios" : {},
            /* SDK配置 */
            "sdkConfigs" : {}
        }
    },
    /* 快应用特有相关 */
    "quickapp" : {},
    /* 小程序特有相关 */
    "mp-weixin" : {
        "appid" : "wx9ae1187d0b4dfxxx",
        "setting" : {
            "urlCheck" : false,
            "minified" : true,
            "postcss" : false
        },
        "styleIsolation" : "shared",
        "usingComponents" : true,
        "requiredPrivateInfos" : [
            "getLocation",
            "chooseAddress",
            "chooseLocation",
            "onLocationChange",
            "startLocationUpdate",
            "startLocationUpdateBackground",
            "choosePoi"
        ],
        "permission" : {
            "scope.userLocation" : {
                "desc" : "获取资产位置"
            }
        },
        "lazyCodeLoading" : "requiredComponents"
    },
    "mp-alipay" : {
        "usingComponents" : true
    },
    "mp-baidu" : {
        "usingComponents" : true
    },
    "mp-toutiao" : {
        "usingComponents" : true
    },
    "uniStatistics" : {
        "enable" : false
    },
    "vueVersion" : "3",
    "h5" : {
        "title" : "智汇资产管理系统",
        "template" : "index.html",
        "router" : {
            "mode" : "history",
            "base" : "/fsgdzc-h5/"
        },
        "devServer" : {
            "https" : false
        },
        "sdkConfigs" : {
            "maps" : {
                "tencent" : {
                    "key" : "I6ABZ-*****-*****-*****-*****-C2BWD"
                }
            }
        }
    }
}

(pages.json 为全局路由配置,体量较大且大部分与地图无关,此处不再整篇附上;其与该页面相关的注册为:path 取 pages/index/index-map,style.navigationBarTitleText 取「地图找地」。完整文件见 pages.json(file:///e:/project/mobile-ui/src/pages.json)。)

步骤二:准备资产接口与字典

  1. services/asset.js:详情模式使用 listInfo(资产列表,支持按村委、类型、租金、面积、中心点与距离筛选);聚合模式使用 assetCountByVillage(按村委统计资产数量)。
  2. utils/useDict.js:加载字典数据(资产类型 asset_type、村委 village_committee 等),用于筛选下拉选项与资产类型名称展示。

相关接口代码:src/services/asset.js(仅保留用到的接口)

javascript 复制代码
import { http } from '@/utils/http'

// 查询资产列表(详情模式使用:按中心点、距离及筛选项拉取资产点位)
export function listInfo(data) {
  return http({
    url: 'mpwx/asset/list',
    method: 'post',
    data
  })
}

// 按村委统计资产数量(聚合模式使用:缩小地图时显示各村委资产总数)
export function assetCountByVillage(data) {
  return http({
    url: 'mpwx/asset/assetCountByVillage',
    method: 'post',
    data
  })
}

完整文件:src/utils/useDict.js

javascript 复制代码
import { ref, onMounted } from 'vue';
import { getDicts } from "@/services/dict";

export function useDict(dictTypes) {
  const dictsData = ref({});

  const loadDicts = async () => {
    try {
      for (const type of dictTypes) {
        const res = await getDicts(type);
        dictsData.value[type] = res.data || []
      }
    } catch (e) {
      console.error('字典加载失败', e);
    }
  };

  const getDictLabel = (type, value) => {
    const item = dictsData.value[type]?.find(item => item.value === value);
    return item?.label || '';
  };

  const getDictClass = (type, value) => {
    const item = dictsData.value[type]?.find(item => item.value === value);
    return item?.listClass || 'default';
  };

  onMounted(loadDicts);

  return {
    dictsData,
    getDictLabel,
    getDictClass,
    loadDicts // 提供手动刷新方法
  };
}

步骤三:实现地图页面(index-map.vue)

页面核心机制:

  1. 地图渲染 :使用 uni-app 的 <map> 组件,绑定 markers、中心经纬度、scale,并监听 markertap(点击标记)、tap(点击空白处)、regionchange(视野变化)。
  2. 两种模式与阈值:以常量 DETAIL_ZOOM_LEVEL(默认 15)为界。缩放级别小于 15 为聚合模式,大于等于 15 为详情模式。
  3. 聚合模式(缩小,显示区域统计) :
    • 调用 assetCountByVillage 拉取各村委的资产数量;
    • 标记位置取「村委中心点」(页面内置各村委中心点表,用村委名称前两个字匹配),标记背景使用不同颜色的图片;
    • 通过 callout 气泡常显「村委名(数量)」,形成区域资产统计展示。
  4. 详情模式(放大,显示资产点位) :
    • 调用 listInfo,参数包含中心点经纬度与距离(默认 1000 米),以及村委、类型、租金、面积等筛选;
    • 按资产的 locationCoordinates(格式为「经度,纬度」)生成 marker;
    • 点击资产点弹出气泡,展示资产名称、类型、面积、地址,点击名称跳转资产详情页。
  5. 缩放级别检测与模式切换 :
    • 小程序端通过 mapContext.getScale 定时检查缩放级别(startScaleCheck),H5 端通过右上角加/减按钮(zoomIn / zoomOut)配合 watch(scale) 触发;
    • 级别跨越阈值时切换模式:进入详情模式清空聚合标记并拉取资产点;进入聚合模式清空资产点并拉取统计。
  6. 视野变化处理:regionchange 的 end 事件做约 600 毫秒防抖;详情模式下若中心点移动超过阈值则重新拉取资产点。
  7. 筛选与初始化:顶部提供类型、区域、租金、面积筛选,变更后按当前模式刷新数据;页面挂载后初始化地图上下文、获取定位,并延迟加载数据等待地图渲染完成。
  8. 加载与提示:加载中显示 loading,数据加载完成用 toast 提示数量。

完整文件:src/pages/index/index-map.vue

vue 复制代码
<template>
	<view class="page home">
		<view class="filter-head blueModel">
			<view class="filter-item" style="flex: 1">
				<wd-picker :columns="assetsColumns" v-model="assetsValue" @confirm="assetsValueConfirm" use-default-slot :z-index="99" root-portal>
					<view class="flex">
						<wd-text color="#1570FD" size="28rpx" :text="assetsText" :lines="1" />
						<wd-icon name="fill-arrow-down" size="20px"></wd-icon>
					</view>
				</wd-picker>
			</view>
			<view class="filter-item" style="flex: 1">
				<wd-picker :columns="villageColumns" v-model="villageValue" @confirm="villageValueConfirm" use-default-slot :z-index="99" root-portal>
					<view class="flex">
						<wd-text color="#1570FD" size="28rpx" :text="villageText" :lines="1" />
						<wd-icon name="fill-arrow-down" size="20px"></wd-icon>
					</view>
				</wd-picker>
			</view>
			<view class="filter-item" style="flex: 1">
				<view class="flex">
					<wd-text color="#1570FD" size="28rpx" :text="rentText" :lines="1" @click="(rentShow = true), closeBubblePopup()" />
					<wd-icon name="fill-arrow-down" size="20px"></wd-icon>
				</view>
			</view>
			<!-- 租金筛选使用uni-app原生弹窗 -->
			<view v-if="rentShow" class="custom-overlay">
				<view class="wrapper popboxs" @click.stop>
					<view class="block" style="width: 100%">
						<!-- 添加关闭按钮 -->
						<view class="close-btn" @click="rentShow = false">
							<wd-icon name="close" size="20px" color="#999"></wd-icon>
						</view>
						<view>
							租金
							<wd-text color="#1570FD" size="28rpx" :text="'¥' + rentValue[0] + '-'" />
							<wd-text color="#1570FD" size="28rpx" :text="rentValue[1]" />
						</view>
						<wd-slider hide-label v-model="rentValue" type="range" :min="0" :max="20000" />
						<view class="tag-container">
							<wd-tag
								type="primary"
								plain
								v-for="(item, index) in rentColumns"
								:key="index"
								:mark="isTagSelected(item.value)"
								@click="rentClick(item.value, index)"
								custom-class="rent-tag"
							>
								{{ item.name }}
							</wd-tag>
						</view>

						<view
							class="sticky-bottom-bar"
							style="box-sizing: border-box; padding-left: 42rpx; padding-right: 42rpx; padding-top: 42rpx; display: flex; gap: 20rpx; background: rgb(255, 255, 255)"
						>
							<view style="flex: 1; padding-bottom: 10px">
								<wd-button block type="info" :round="false" @tap="reset">重置</wd-button>
							</view>
							<view style="flex: 1; padding-bottom: 10px">
								<wd-button block custom-class="blue-btn" :round="false" @tap="confirmRent">确定</wd-button>
							</view>
						</view>
					</view>
				</view>
			</view>
			<view class="filter-item" style="flex: 1">
				<view class="flex">
					<wd-text color="#1570FD" size="28rpx" :text="areaText" :lines="1" @click="(areaShow = true), closeBubblePopup()" />
					<wd-icon name="fill-arrow-down" size="20px"></wd-icon>
				</view>
			</view>
			<!-- 面积筛选使用wot-design-uni弹窗 -->
			<view v-if="areaShow" class="custom-overlay">
				<view class="wrapper popboxs" @click.stop>
					<view class="block" style="width: 100%">
						<!-- 添加关闭按钮 -->
						<view class="close-btn" @click="areaShow = false">
							<wd-icon name="close" size="20px" color="#999"></wd-icon>
						</view>
						<view>
							面积
							<wd-text color="#1570FD" size="28rpx" :text="areaValue[0] + '-'" />
							<wd-text color="#1570FD" size="28rpx" :text="areaValue[1]" />
						</view>
						<wd-slider hide-label v-model="areaValue" type="range" :min="0" :max="200" />
						<view class="tag-container">
							<wd-tag
								type="primary"
								plain
								v-for="(item, index) in areaColumns"
								:key="index"
								:mark="isTagSelectedArea(item.value)"
								@click="areaClick(item.value, index)"
								custom-class="rent-tag"
							>
								{{ item.name }}
							</wd-tag>
						</view>

						<view
							class="sticky-bottom-bar"
							style="box-sizing: border-box; padding-left: 42rpx; padding-right: 42rpx; padding-top: 42rpx; display: flex; gap: 20rpx; background: rgb(255, 255, 255)"
						>
							<view style="flex: 1; padding-bottom: 10px">
								<wd-button block type="info" :round="false" @tap="resetArea">重置</wd-button>
							</view>
							<view style="flex: 1; padding-bottom: 10px">
								<wd-button block custom-class="blue-btn" :round="false" @tap="confirmArea">确定</wd-button>
							</view>
						</view>
					</view>
				</view>
			</view>
		</view>
		<view class="map-container">
			<map
				id="myMap"
				:markers="displayMarkers"
				@markertap="onMarkerTap"
				@tap="onMapTap"
				@regionchange="onRegionChange"
				:longitude="centerLongitude"
				:latitude="centerLatitude"
				:scale="scale"
				:show-location="true"
				style="width: 100%; height: 100%"
				:enable-zoom="true"
				:enable-scroll="true"
				:show-scale="true"
			></map>

      <!-- 缩放控制按钮 -->
      <!-- #ifdef H5 -->
      <view class="zoom-controls">
        <view class="zoom-btn" @click="zoomIn">
          <text>+</text>
        </view>
        <view class="zoom-btn" @click="zoomOut">
          <text>-</text>
        </view>
      </view>
      <!-- #endif -->

			<!-- 地图缩放级别显示 -->
			<view class="scale-info" v-if="showScaleInfo">级别: {{ currentScale }} {{ isDetailMode ? '(详情)' : '(聚合)' }}</view>

			<!-- 手动刷新按钮 -->
			<view class="refresh-btn" @click="manualRefresh">
				<wd-icon name="refresh" size="20px" color="#1890ff"></wd-icon>
			</view>

			<!-- 加载状态 -->
			<view class="loading-indicator" v-if="isLoading">加载中...</view>
		</view>

		<!-- 气泡式弹窗 -->
		<view v-if="showBubblePopup" class="bubble-popup">
			<view class="bubble-content">
				<!-- 气泡箭头(指向下方) -->
				<view class="bubble-arrow"></view>

				<!-- 资产名称,可点击跳转 -->
				<view class="asset-name-line">
					<text class="info-label">资产名称:</text>
					<text class="asset-name-link" @tap="gotoAssetDetail(currentAsset.assetCode)">
						{{ currentAsset.assetName }}
					</text>
				</view>

				<view class="asset-info">
					<text class="info-label">资产类型:</text>
					<text class="info-value">{{ currentAsset.assetType }}</text>
				</view>

				<view class="asset-info">
					<text class="info-label">资产面积:</text>
					<text class="info-value">{{ currentAsset.area }}{{ currentAsset.unit }}</text>
				</view>

				<view class="asset-info">
					<text class="info-label">资产地址:</text>
					<text class="info-value">{{ currentAsset.address }}</text>
				</view>
			</view>
		</view>

		<wd-toast />
		<wd-message-box />
	</view>
</template>

<script setup>
import { ref, watch, onMounted, nextTick, computed, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { useToast, useMessage } from 'wot-design-uni';
import { assetCountByVillage, listInfo } from '@/services/asset';
import { useDict } from '@/utils/useDict';

// 下拉框值
// 资产类型
const assetType = ref('');
const assetsText = ref('类型');
const assetsValue = ref('');
const assetsColumns = ref([]);
const assetsValueConfirm = (e) => {
	assetType.value = e.value;
	assetsText.value = e.selectedItems.label;
	refreshMapData();
};
const villageText = ref('区域');
const villageValue = ref('');
const villageColumns = ref([]);
const villageValueConfirm = (e) => {
	villageText.value = e.selectedItems.label;
	refreshMapData();
};

// 租金下拉框开始
const rentShow = ref(false);
const rentText = ref('租金');
const rentColumns = ref([
	{
		name: '¥1000以下',
		value: [0, 1000]
	},
	{
		name: '¥1000-1500',
		value: [1000, 1500]
	},
	{
		name: '¥1500-2000',
		value: [1500, 2000]
	},
	{
		name: '¥2000-3000',
		value: [2000, 3000]
	},
	{
		name: '¥3000-5000',
		value: [3000, 5000]
	},
	{
		name: '¥5000-8000',
		value: [5000, 8000]
	},
	{
		name: '¥8000-13000',
		value: [8000, 13000]
	},
	{
		name: '¥13000-20000',
		value: [13000, 20000]
	},
	{
		name: '¥20000以上',
		value: [20000, 200000]
	}
]);
const rentChooseValue = ref(); //选中自定义的租金范围
const rentValue = ref([0, 20000]);
const confirmRent = () => {
	rentShow.value = false;
	refreshMapData();
};

const isTagSelected = (value) => {
	if (!rentChooseValue.value) return false;
	return JSON.stringify(value) === JSON.stringify(rentChooseValue.value);
};

const rentClick = (value, index) => {
	// 如果再次点击已选中的标签,则取消选择
	if (JSON.stringify(rentChooseValue.value) === JSON.stringify(value)) {
		rentChooseValue.value = null;
		// 同时更新slider值为默认范围
		rentValue.value = [0, 20000];
	} else {
		rentChooseValue.value = value;
		// 更新slider值为标签对应的范围
		rentValue.value = value;
	}
};

const reset = () => {
	rentValue.value = [0, 20000];
	rentChooseValue.value = null;
	refreshMapData();
};
// 租金下拉框结束

// 面积下拉框开始
const areaShow = ref(false);
const areaText = ref('面积');
const areaColumns = ref([
	{
		name: '≤20m²/亩',
		value: [0, 20]
	},
	{
		name: '20-40m²/亩',
		value: [20, 40]
	},
	{
		name: '40-60m²/亩',
		value: [40, 60]
	},
	{
		name: '60-90m²/亩',
		value: [60, 90]
	},
	{
		name: '90-120m²/亩',
		value: [90, 120]
	},
	{
		name: '120-150m²/亩',
		value: [120, 150]
	},
	{
		name: '150-180m²/亩',
		value: [150, 180]
	},
	{
		name: '≥180m²/亩',
		value: [180, 200]
	}
]);
const areaChooseValue = ref(); //选中自定义的面积范围
const areaValue = ref([0, 200]);
const confirmArea = () => {
	areaShow.value = false;
	refreshMapData();
};

const isTagSelectedArea = (value) => {
	if (!areaChooseValue.value) return false;
	return JSON.stringify(value) === JSON.stringify(areaChooseValue.value);
};

const areaClick = (value, index) => {
	// 如果再次点击已选中的标签,则取消选择
	if (JSON.stringify(areaChooseValue.value) === JSON.stringify(value)) {
		areaChooseValue.value = null;
		// 同时更新slider值为默认范围
		areaValue.value = [0, 200];
	} else {
		areaChooseValue.value = value;
		// 更新slider值为标签对应的范围
		areaValue.value = value;
	}
};

const resetArea = () => {
	rentValue.value = [0, 20000];
	areaChooseValue.value = null;
	refreshMapData();
};
// 面积下拉框结束

// 地图相关变量
const showBubblePopup = ref(false);
const currentAsset = ref({});
const currentMarker = ref(null);
const isLoading = ref(false);

const toast = useToast();
const message = useMessage();
const centerLatitude = ref(23.260015);
const centerLongitude = ref(113.018526);
const scale = ref(13); // 初始缩放级别
const currentScale = ref(13); // 当前缩放级别
const showScaleInfo = ref(false);

const staticUrl = import.meta.env.VITE_APP_STATIC_URL || '/static';

// 模式相关
const isDetailMode = ref(false); // 是否为详情模式
const DETAIL_ZOOM_LEVEL = 15; // 进入详情模式的缩放级别阈值

// 标记数据
const mapMarkers = ref([]); // 详细资产点位标记
const clusterMarkers = ref([]); // 聚合统计标记(包含图片标记和数字标记)
const textMarkers = ref([]); // 单独的数字标记

// 计算要显示的标记
const displayMarkers = computed(() => {
	if (isDetailMode.value) {
		return mapMarkers.value;
	} else {
		// 聚合模式下同时显示图片标记和数字标记
		return [...clusterMarkers.value, ...textMarkers.value];
	}
});

// 地图上下文实例
let mapContext = null;

// 定时器相关
let scaleCheckTimer = null;
let lastScaleCheckTime = 0;
const SCALE_CHECK_INTERVAL = 2000; // 1000ms检查一次
let isCheckingScale = false;

// 初始化地图上下文
const initMapContext = () => {
	// #ifdef MP-WEIXIN
	try {
		mapContext = uni.createMapContext('myMap');
		console.log('地图上下文初始化成功');
	} catch (error) {
		console.error('初始化地图上下文失败:', error);
	}
	// #endif

	// #ifdef H5
	// H5环境下可能需要使用其他方式获取地图实例
	console.log('H5环境地图初始化');
	// #endif
};

// 获取地图缩放级别
const getMapScale = () => {
	return new Promise((resolve) => {
		if (!mapContext) {
			resolve(currentScale.value);
			return;
		}

		// #ifdef MP-WEIXIN
		mapContext.getScale({
			success: (res) => {
				console.log('获取缩放级别成功:', res.scale);
				resolve(res.scale);
			},
			fail: (err) => {
				console.warn('获取缩放级别失败:', err);
				resolve(currentScale.value);
			}
		});
		// #endif

		// #ifdef H5
		// H5环境直接返回当前scale值
		resolve(currentScale.value);
		// #endif
	});
};

// 定期检查缩放级别
const startScaleCheck = () => {
	if (scaleCheckTimer) {
		clearInterval(scaleCheckTimer);
	}

	scaleCheckTimer = setInterval(async () => {
		if (isCheckingScale) return;

		const now = Date.now();
		// 限制检查频率
		if (now - lastScaleCheckTime < SCALE_CHECK_INTERVAL) {
			return;
		}

		isCheckingScale = true;
		lastScaleCheckTime = now;

		try {
			const newScale = await getMapScale();

			// 只有当缩放级别确实发生变化时才处理
			if (Math.abs(newScale - currentScale.value) >= 0.5) {
				console.log(`缩放级别变化: ${currentScale.value} -> ${newScale}`);
				currentScale.value = newScale;
				// scale.value = newScale;

				// 触发缩放级别变化处理
				handleScaleChange(newScale);
			}
		} catch (error) {
			console.error('检查缩放级别失败:', error);
		} finally {
			isCheckingScale = false;
		}
	}, SCALE_CHECK_INTERVAL);
};

// 处理缩放级别变化
const handleScaleChange = (newScale) => {
	// 判断是否进入详情模式
	const shouldBeDetailMode = newScale >= DETAIL_ZOOM_LEVEL;

	if (shouldBeDetailMode !== isDetailMode.value) {
		console.log(`模式切换: ${isDetailMode.value ? '聚合' : '详情'} -> ${shouldBeDetailMode ? '详情' : '聚合'}`);
		isDetailMode.value = shouldBeDetailMode;

		if (shouldBeDetailMode) {
			// 进入详情模式,清除聚合标记,加载详细资产
			clusterMarkers.value = [];
			textMarkers.value = [];
			getAssetList();
		} else {
			// 进入聚合模式,清除详细标记,加载聚合统计
			mapMarkers.value = [];
			getClusterData();
		}
	}

	// 显示缩放信息
	showScaleInfo.value = true;
	setTimeout(() => {
		showScaleInfo.value = false;
	}, 2000);
};

// 地图区域变化事件
let regionChangeTimer = null;
let isRequesting = false;
const onRegionChange = (e) => {
  if (e.type === 'end') {
    // 清除之前的定时器
    if (regionChangeTimer) clearTimeout(regionChangeTimer);

    // 设置新的定时器,防抖处理
    regionChangeTimer = setTimeout(async () => {
      if (isRequesting) return;

      try {
        const newScale = await getMapScale();
        const scaleChanged = Math.abs(newScale - currentScale.value) >= 0.5;

        if (scaleChanged) {
          currentScale.value = newScale;
          // scale.value = newScale;
          await handleScaleChange(newScale);
          closeBubblePopup();
        }

        // 只有在缩放级别没有变化,且是详情模式时才更新中心点并重新获取数据
        if (!scaleChanged && isDetailMode.value) {
          if (mapContext) {
            mapContext.getCenterLocation({
              success: (res) => {
                // 检查中心点变化是否超过阈值
                const latDiff = Math.abs(res.latitude - centerLatitude.value);
                const lngDiff = Math.abs(res.longitude - centerLongitude.value);

                if (latDiff > 0.001 || lngDiff > 0.001) {
                  centerLongitude.value = res.longitude;
                  centerLatitude.value = res.latitude;
                  getAssetList();
                }
              }
            });
          }
        }
      } catch (error) {
        console.error('处理区域变化失败:', error);
      }
    }, 600); // 600ms防抖时间
  }
};

// 手动刷新
const manualRefresh = () => {
	console.log('手动刷新地图数据');
	getMapScale().then((newScale) => {
		currentScale.value = newScale;
		scale.value = newScale;
		handleScaleChange(newScale);
	});
};

// 放大
const zoomIn = () => {
	if (scale.value < 20) {
		scale.value += 1;
		currentScale.value = scale.value;
		handleScaleChange(scale.value);
	}
};

// 缩小
const zoomOut = () => {
	if (scale.value > 3) {
		scale.value -= 1;
		currentScale.value = scale.value;
		handleScaleChange(scale.value);
	}
};

// 监听 scale 变化(通过按钮控制)
watch(
	() => scale.value,
	(newScale) => {
		if (Math.abs(newScale - currentScale.value) >= 1) {
			currentScale.value = newScale;
			handleScaleChange(newScale);
		}
	}
);

// 刷新地图数据
const refreshMapData = () => {
	if (isDetailMode.value) {
		getAssetList();
	} else {
		getClusterData();
	}
};

// 初始化标记
const initMarker = () => {
	mapMarkers.value = [];

	if (!details.value || details.value.length === 0) {
		console.log('没有资产数据');
		return;
	}

	details.value.forEach((item, index) => {
		let latlng = item.locationCoordinates;
		if (!latlng) return;

		let targetLatitude = Number(latlng.split(',')[1]);
		let targetLongitude = Number(latlng.split(',')[0]);

		// 构建标记对象
		let marker = {
			id: index + 1,
			latitude: targetLatitude,
			longitude: targetLongitude,
			customData: {
				assetId: item.id || item.assetId,
				assetName: item.assetName,
				assetCode: item.assetCode,
				assetType: getDictLabel('asset_type', item.assetType),
				area: item.area,
				unit: item.unit,
				address: item.assetAddress,
				fullData: item,
				markerId: index + 1,
				latitude: targetLatitude,
				longitude: targetLongitude
			}
		};

		// 判断平台,为不同平台设置不同的标记
		// #ifdef H5
		marker.iconPath = staticUrl + '/images/location-marker.png';
		marker.width = 28;
		marker.height = 57;
		marker.callout = { display: 'BYCLICK' };
		// #endif

		// #ifdef MP-WEIXIN
		marker.callout = { display: 'BYCLICK' };
		// #endif

		mapMarkers.value.push(marker);
	});

	console.log('初始化了', mapMarkers.value.length, '个详细资产标记');
};

// 生成聚合点图标(使用预留图片)
const generateClusterIcon = (village) => {
	// 根据数量范围选择不同的图片
	let imageName = 'asset-blue.png'; // 默认小于10的图片

	//需要根据资产数量设置颜色的话可以修改这里
	if (village % 3 === 0) {
		imageName = 'asset-blue.png';
	} else if (village % 3 === 1) {
		imageName = 'asset-green.png';
	} else if (village % 3 === 2) {
		imageName = 'asset-orange.png';
	} else {
		imageName = 'asset-red.png'; // 30以上
	}

	// 返回图片路径
	return `${staticUrl}/images/${imageName}`;
};

// 初始化聚合标记
const initClusterMarkers = (clusters) => {
	clusterMarkers.value = [];
	textMarkers.value = [];

	if (!clusters || clusters.length === 0) {
		console.log('没有聚合数据');
		return;
	}

  const mapData = ref([
        {
          name: '三江村委',
          center: [23.231835,113.026861]
        },{
          name: '三溪村委',
          center: [23.231187,112.987291]
        },{
          name: '华㘵村委',
          center: [23.267152,113.040755]
        },{
          name: '乐平村委',
          center: [23.260015,113.018526]
        },{
          name: '源潭村委',
          center: [23.26953,112.979652]
        },{
          name: '新旗村委',
          center: [23.281252,113.019258]
        },{
          name: '范湖村委',
          center: [23.299372,112.998074]
        },{
          name: '南联村委',
          center: [23.330682,113.000471]
        },{
          name: '大岗村委',
          center: [23.304003,112.938363]
        },{
          name: '竹山村委',
          center: [23.283047,112.95597]
        },{
          name: '念德村委',
          center: [23.272531,112.932292]
        },{
          name: '南丰劳教所',
          center: [23.291387,112.906008]
        },{
          name: '保安村委',
          center: [23.276728,112.893139]
        },{
          name: '湖岗村委',
          center: [23.258563,112.869062]
        },{
          name: '黄塘村委',
          center: [23.309776,112.883705]
        }
      ]);
	// 为每个聚合点创建两个标记:图片标记和数字标记
	clusters.forEach((cluster, index) => {
    //村委的坐标使用上面mapData的中心点
    const village = mapData.value.find(item => cluster.name.indexOf(item.name.substring(0, 2)) !== -1);
		// 获取聚合点图片路径和大小
		const iconSize = getClusterSize(cluster.count);
		const iconPath = generateClusterIcon(cluster.village);

		// 1. 创建图片标记(背景)
		// const imageMarkerId = `cluster_image_${index + 1}`;
		const imageMarkerId = index + 1;
		let imageMarker = {
			id: imageMarkerId,
			latitude: village?village.center[0]:cluster.longitude,
			longitude: village?village.center[1]:cluster.longitude,
			// title: cluster.name + '(' + cluster.count + ')',
			iconPath: iconPath,
			width: iconSize,
			height: iconSize,
			callout: {
				content: cluster.name + '(' + cluster.count + ')',
				// color: '#333',
				// fontSize: 14,
				// bgColor: '#fff',
				// padding: 8,
				display: 'ALWAYS'
			},
			// zIndex: 99, // 图片标记的层级较低
			customData: {
				...cluster,
				isCluster: true,
				markerId: imageMarkerId,
				isImageMarker: true // 标记这是图片标记
			}
		};

		//原本是打算数字直接显示在图片上面的,存在位置问题并且显示不到村委名称,代码先注释掉
		// 2. 创建数字标记(文字,显示在图片上面)
		// const textMarkerId = `cluster_text_${index + 1}`;
		// let offset = 0.02; // 默认值
		//
		// // #ifdef H5
		// offset = 0.002;
		// // #endif
		//
		// // #ifdef MP-WEIXIN
		// offset = -0.002;
		// // #endif
		//
		// let textMarker = {
		//   id: textMarkerId,
		//   latitude: cluster.latitude-offset,
		//   longitude: cluster.longitude,
		//   iconPath: iconPath,
		//   width: 0.1,
		//   height: 0.1,
		//   zIndex: 100,
		//   callout: {
		//     content: cluster.count.toString(),
		//     color: '#FFFFFF', // 文字颜色
		//     fontSize: getFontSize(cluster.count),
		//
		//     // 关键:完全透明的气泡背景
		//     bgColor: 'rgba(0,0,0,0)', // 完全透明背景
		//     borderWidth: 0, // 无边框
		//     borderColor: 'rgba(0,0,0,0)', // 透明边框
		//     borderRadius: 0, // 无圆角
		//
		//     // 位置调整 - 让文字显示在图标正上方
		//     anchorX: 0.5, // X轴居中 (0=左, 0.5=中, 1=右)
		//     // anchorY: 0, // Y轴在图标顶部 (0=上, 1=下)
		//
		//     // 偏移量调整 - 微调文字位置
		//     offset: [0, 0], // 向上偏移图标高度的30%
		//
		//     // 其他样式设置
		//     padding: 0, // 无内边距
		//     textAlign: 'center', // 文字居中
		//     display: 'ALWAYS', // 始终显示
		//
		//     // 注意:某些版本可能需要添加以下属性
		//     boxShadow: 'none', // 无阴影
		//     borderStyle: 'solid'
		//   },
		//   customData: {
		//     ...cluster,
		//     isCluster: true,
		//     markerId: textMarkerId
		//   }
		// }

		// textMarkers.value.push(textMarker);
		clusterMarkers.value.push(imageMarker);
	});

	console.log('初始化了', clusterMarkers.value.length, '个图片聚合标记和', textMarkers.value.length, '个文字聚合标记');
};

// 根据数量获取字体大小(无论村委有多少个资产,都显示一样的大小先,有这样的需求再修改这里)
const getFontSize = (count) => {
	// if (count < 10) return 14;
	// if (count < 20) return 16;
	// if (count < 30) return 18;
	return 14; // 30以上
};

// 根据数量获取聚合点大小
const getClusterSize = (count) => {
	// if (count < 10) return 30;
	// if (count < 20) return 40;
	// if (count < 30) return 50;
	return 30; // 30以上
};

// marker点击事件
let lastTapTime = 0;
const onMarkerTap = async (e) => {
	const now = Date.now();
	if (now - lastTapTime < 300) return;
	lastTapTime = now;

	const markerId = e.detail.markerId;
	// 如果是聚合模式,需要处理图片标记和文字标记
	if (!isDetailMode.value) {
		// 查找点击的标记
		let marker = null;

		// 先查找图片标记
		marker = clusterMarkers.value.find((m) => m.id === markerId);
		if (!marker) {
			// 如果不是图片标记,查找文字标记
			marker = textMarkers.value.find((m) => m.id === markerId);
		}

		if (marker && marker.customData) {
			showBubblePopup.value = false;
			await nextTick();

			centerLongitude.value = marker.customData.longitude;
			centerLatitude.value = marker.customData.latitude;

			if (marker.customData.isCluster) {
				console.log('点击聚合点,放大地图');
				// 放大到详情模式级别
				scale.value = DETAIL_ZOOM_LEVEL;
				currentScale.value = DETAIL_ZOOM_LEVEL;
				isDetailMode.value = true;
				clusterMarkers.value = [];
				textMarkers.value = [];
				getAssetList();
			}
		}
	} else {
		// 详情模式下的处理
		const marker = mapMarkers.value.find((m) => m.id === markerId);
		if (marker && marker.customData) {
			showBubblePopup.value = false;
			await nextTick();

			centerLongitude.value = marker.customData.longitude;
			centerLatitude.value = marker.customData.latitude;

			setTimeout(() => {
				currentAsset.value = marker.customData;
				currentMarker.value = marker;
				showBubblePopup.value = true;
			}, 150);
		}
	}
};

// 地图空白处点击事件
const onMapTap = (e) => {
	if (!e.detail.markerId) {
		closeBubblePopup();
	}
};

// 跳转到资产详情
const gotoAssetDetail = (assetCode) => {
	if (!assetCode) return;
	uni.navigateTo({
		url: `/pages/index/detail?assetCode=${assetCode}`,
		success: () => {
			showBubblePopup.value = false;
		}
	});
};

// 关闭气泡弹窗
const closeBubblePopup = () => {
	showBubblePopup.value = false;
	currentMarker.value = null;
};

// 获取资产列表
const details = ref({});
const getAssetList = async () => {

  // 如果正在请求,则跳过
  if (isRequesting) {
    console.log('已有请求在进行中,跳过');
    return;
  }
  isRequesting = true;
	isLoading.value = true;

	const params = {
		pageNum: 1,
		pageSize: 10000,
		villageCommittee: villageValue.value ? [String(villageValue.value)] : [],
		assetType: assetType.value ? [String(assetType.value)] : [],
		unitPriceStart: rentValue.value[0]===0?null:rentValue.value[0],
		unitPriceEnd: rentValue.value[1]===20000?null:rentValue.value[1],
		areaStart: areaValue.value[0]===0?null:areaValue.value[0],
		areaEnd: areaValue.value[1]===200?null:areaValue.value[1],
		centerLat: centerLatitude.value,
		centerLng: centerLongitude.value,
		// distance: currentScale.value>=15&&currentScale.value<16?1000:currentScale.value>=16&&currentScale.value<17?500:100,
		distance: 1000
	};

	console.log('获取详细资产数据,参数:', params);

	try {
		// 尝试调用真实接口
		const res = await listInfo(params);

		if (res.code === 200) {
			details.value = res.rows;
			console.log('获取到', details.value.length, '条资产数据');
			initMarker();
			toast.success(`加载了${details.value.length}个资产点`);
		} else {
			toast.error(res.msg || '获取数据失败');
			// 使用模拟数据
			// mockAssetData();
		}
	} catch (error) {
		console.error('获取资产数据失败:', error);
		// 使用模拟数据
		// mockAssetData();
	} finally {
		isLoading.value = false;
    // 请求完成后释放锁
    setTimeout(() => {
      isRequesting = false;
    }, 300);
	}
};

// 获取聚合统计数据
const getClusterData = async () => {
	isLoading.value = true;
	console.log('获取聚合统计数据');
	const params = {
		pageNum: 1,
		pageSize: 10000,
		villageCommittee: villageValue.value ? [String(villageValue.value)] : [],
		assetType: assetType.value ? [String(assetType.value)] : [],
		unitPriceStart: rentValue.value[0]===0?null:rentValue.value[0],
		unitPriceEnd: rentValue.value[1]===20000?null:rentValue.value[1],
		areaStart: areaValue.value[0]===0?null:areaValue.value[0],
		areaEnd: areaValue.value[1]===200?null:areaValue.value[1],
	};
	console.log('获取聚合统计数据,参数:', params);

	try {
		// 尝试调用真实接口
		const res = await assetCountByVillage(params);

		if (res.code === 200) {
			const data = res.data;
			console.log('获取到', data.length, '条村委数据');
			const clusters = [];
			for (let i = 0; i < data.length; i++) {
				if (data[i].location) {
					data[i].location = data[i].location.split(',').map(Number);
          clusters.push({
            id: i + 1,
            latitude: data[i].location[1],
            longitude: data[i].location[0],
            count: data[i].countValue,
            village: data[i].villageCommittee,
            name: data[i].villageName || '-'
          });
				}
			}
			initClusterMarkers(clusters);
			toast.success(`加载了${data.length}个村委的资产点`);
		} else {
			toast.error(res.msg || '获取数据失败');
		}
	} catch (error) {
		console.error('获取资产数据失败:', error);
	} finally {
		isLoading.value = false;
	}
};

// 获取位置
const getLocation = () => {
	uni.getLocation({
		type: 'gcj02',
		success: (res) => {
			console.log('获取位置成功:', res);
			toast.success('获取当前位置成功');
			centerLatitude.value = res.latitude;
			centerLongitude.value = res.longitude;
		},
		fail: (err) => {
			console.log('获取位置失败:', err);
			if (err.errMsg.includes('auth deny')) {
				message
					.alert({
						title: '授权提示',
						msg: '需要获取您的位置权限才能使用地图功能,请在设置中开启',
						zIndex: 9999,
						closeOnClickModal: false
					})
					.then(() => {
						uni.openSetting({
							success: (settingRes) => {
								console.log('权限设置结果:', settingRes.authSetting);
							}
						});
					});
			} else {
				toast.error('获取位置失败');
			}
		}
	});
};

onLoad((options) => {
	console.log('页面参数:', options);
});

onMounted(() => {
	// 初始化地图上下文
	initMapContext();

	// 根据初始缩放级别确定模式
	isDetailMode.value = currentScale.value >= DETAIL_ZOOM_LEVEL;

	// 获取位置
	getLocation();

	// 延迟加载数据,确保地图已经渲染
	setTimeout(() => {
		if (isDetailMode.value) {
			getAssetList();
		} else {
			getClusterData();
		}

		// 开始定期检查缩放级别
		startScaleCheck();

		// 初始显示缩放信息
		showScaleInfo.value = true;
		setTimeout(() => {
			showScaleInfo.value = false;
		}, 3000);
	}, 1000);
});

// 页面显示时重新开始检查
onShow(() => {
	if (!scaleCheckTimer) {
		startScaleCheck();
	}
});

// 页面卸载时清理
onUnmounted(() => {
	if (scaleCheckTimer) {
		clearInterval(scaleCheckTimer);
		scaleCheckTimer = null;
	}
});

// 获取字典数据
const { dictsData, getDictLabel, getDictClass } = useDict(['asset_type', 'village_committee', 'contract_status']);

// 监听dictsData变化
watch(
	() => dictsData.value,
	(newVal) => {
		// 确保有目标字典数据(避免初始空对象触发)
		if (newVal.asset_type) {
			let lists = newVal.asset_type;
			assetsColumns.value = lists.map((item) => ({
				label: item.label,
				value: item.value
			}));
		}
		if (newVal.village_committee) {
			villageColumns.value = newVal.village_committee.map((item) => ({
				label: item.label,
				value: item.value
			}));
		}
	},
	{ deep: true }
);
</script>

<style lang="scss">
@use '~@/assets/styles/common.scss';
@use '~@/assets/styles/fixedAssets.scss';

.page.home {
	position: relative;
	width: 100vw;
	height: 100vh;
	overflow: hidden;
}

.map-container {
	width: 100%;
	height: 100%;
	position: relative;
}
</style>

<style lang="scss" scoped>
// 缩放控制按钮
.zoom-controls {
	position: absolute;
	right: 20rpx;
	bottom: 150rpx;
	display: flex;
	flex-direction: column;
	gap: 10rpx;
	z-index: 100;

	.zoom-btn {
		width: 80rpx;
		height: 80rpx;
		background: white;
		border-radius: 50%;
		display: flex;
		align-items: center;
		justify-content: center;
		box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
		font-size: 36rpx;
		font-weight: bold;
		color: #333;

		&:active {
			background: #f5f5f5;
			transform: scale(0.95);
		}
	}
}

// 手动刷新按钮
.refresh-btn {
	position: absolute;
	right: 20rpx;
	bottom: 250rpx;
	width: 80rpx;
	height: 80rpx;
	background: white;
	border-radius: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
	z-index: 100;

	&:active {
		background: #f5f5f5;
		transform: scale(0.95);
	}
}

// 地图缩放级别信息样式
.scale-info {
	position: absolute;
	top: 20rpx;
	right: 20rpx;
	background: rgba(0, 0, 0, 0.7);
	color: white;
	padding: 10rpx 20rpx;
	border-radius: 20rpx;
	font-size: 24rpx;
	z-index: 100;
	pointer-events: none;
	backdrop-filter: blur(4rpx);
}

// 加载状态指示器
.loading-indicator {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	background: rgba(0, 0, 0, 0.7);
	color: white;
	padding: 20rpx 40rpx;
	border-radius: 10rpx;
	font-size: 28rpx;
	z-index: 100;
	pointer-events: none;
}

// 气泡式弹窗样式 - 固定在屏幕上方居中
/* #ifdef H5 */
.bubble-popup {
	position: fixed;
	z-index: 9999;
	width: 280px;
	left: 50%;
	top: 50%; // H5 环境下使用 50%
	transform: translateX(-50%);
	pointer-events: auto;
	animation: fadeIn 0.3s ease;
}
/* #endif */

/* #ifdef MP-WEIXIN */
.bubble-popup {
	position: fixed;
	z-index: 9999;
	width: 280px;
	left: 50%;
	top: 32%; // 小程序环境下使用 32%
	transform: translateX(-50%);
	pointer-events: auto;
	animation: fadeIn 0.3s ease;
}
/* #endif */

.bubble-content {
	background: #fff;
	border-radius: 12px;
	box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
	padding: 16px;
	position: relative;
	border: 1px solid #e4e7ed;
}

// 气泡箭头 - 指向下方(指向地图中心的图标)
.bubble-arrow {
	position: absolute;
	bottom: -8px;
	left: 50%;
	transform: translateX(-50%);
	width: 0;
	height: 0;
	border-left: 8px solid transparent;
	border-right: 8px solid transparent;
	border-top: 8px solid #fff;

	&::before {
		content: '';
		position: absolute;
		top: -9px;
		left: -8px;
		width: 0;
		height: 0;
		border-left: 8px solid transparent;
		border-right: 8px transparent;
		border-top: 8px solid #e4e7ed;
		z-index: -1;
	}
}

// 资产名称行样式
.asset-name-line {
	margin-bottom: 12px;
	display: flex;
	align-items: flex-start;
	font-size: 14px;
	line-height: 1.4;
}

.asset-name-link {
	color: #1890ff;
	font-weight: bold;
	flex: 1;
	text-decoration: underline;
	cursor: pointer;
	word-break: break-all;

	&:active {
		opacity: 0.8;
	}
}

.asset-info {
	margin-bottom: 8px;
	display: flex;
	align-items: flex-start;
	font-size: 14px;
	line-height: 1.4;
}

.info-label {
	color: #606266;
	min-width: 60px;
	flex-shrink: 0;
}

.info-value {
	color: #303133;
	flex: 1;
	word-break: break-all;
}

@keyframes fadeIn {
	from {
		opacity: 0;
		transform: translateX(-50%) translateY(-10px);
	}
	to {
		opacity: 1;
		transform: translateX(-50%) translateY(0);
	}
}

// 适配不同屏幕尺寸
@media (max-width: 768px) {
	/* #ifdef H5 */
	.bubble-popup {
		width: 250px;
		top: 35%;
	}
	/* #endif */

	/* #ifdef MP-WEIXIN */
	.bubble-popup {
		width: 250px;
		top: 32%;
	}
	/* #endif */

	.asset-info {
		font-size: 13px;
	}

	.info-label {
		min-width: 55px;
	}

	.scale-info {
		font-size: 20rpx;
		padding: 8rpx 16rpx;
	}

	.zoom-controls {
		right: 10rpx;
		bottom: 120rpx;

		.zoom-btn {
			width: 70rpx;
			height: 70rpx;
			font-size: 30rpx;
		}
	}

	.refresh-btn {
		right: 10rpx;
		bottom: 210rpx;
		width: 70rpx;
		height: 70rpx;
	}

	.loading-indicator {
		font-size: 24rpx;
		padding: 15rpx 30rpx;
	}
}

// 适配小屏幕手机
@media (max-width: 375px) {
	.bubble-popup {
		width: 90%;
		max-width: 250px;
	}
}

.custom-overlay {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	background-color: rgba(0, 0, 0, 0.5);
	display: flex;
	align-items: center;
	justify-content: center;
	z-index: 999;
}

.tag-container {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 12rpx;
	padding: 20rpx 10rpx;
	width: 100%;
}

.rent-tag {
	margin: 0 !important;
	flex: 0 0 auto;
	// 设置固定宽度,使所有标签大小一致
	min-width: 170rpx;
	text-align: center;
	// 限制最大宽度,防止文本过长撑开标签
	max-width: 200rpx;
	// 字体大小统一
	font-size: 24rpx;
	// 圆角统一
	border-radius: 8rpx;
	// 内边距统一
	padding: 0 12rpx;
}

// 自定义mark状态下的样式
:deep(.wd-tag--primary.wd-tag--mark) {
	background-color: #1570fd !important;
	border-color: #1570fd !important;
	color: white !important;
}

:deep(.wd-tag.is-primary.is-mark) {
	border-radius: 6px !important;
	padding: 0 4px !important;
	background: #1570fd !important;
	color: white !important;
}

.close-btn {
	float: right;

	&:active {
		opacity: 0.7;
	}
}

/* 聚合标记数字样式 */
::v-deep .map-marker-label {
	text-align: center;
	font-weight: bold;
	text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
	pointer-events: none; /* 确保文字标记不会干扰点击事件 */
}

/* 隐藏腾讯地图的定位控件 */
:deep(.refresh-btn) {
	display: none !important;
}
</style>

常见调整位置速查

需求 调整位置
聚合 / 详情切换阈值 index-map.vue 的 DETAIL_ZOOM_LEVEL(默认 15)
初始中心点与缩放级别 index-map.vue 的 centerLatitude、centerLongitude、scale(默认 13)
村委中心点坐标 index-map.vue 的 initClusterMarkers 内置村委中心点表
聚合点颜色与大小 index-map.vue 的 generateClusterIcon、getClusterSize
详情模式拉取范围 index-map.vue 的 getAssetList 中 distance(默认 1000 米)
地图 key src/manifest.json 的 sdkConfigs.maps.tencent.key
标记图片地址 环境变量 VITE_APP_STATIC_URL 拼接 /images/ 下的图片
相关推荐
a努力。1 小时前
LangChain Agent消息链路全解析
java·前端·javascript
今年下半年1 小时前
【前端】ant-design-vue 表格「行、列都动态」的处理方案
前端·javascript·vue.js
默_笙2 小时前
🚕 缺料就出门买:给 RAG 装上"信息够不够"的判断力
前端·javascript
志尊宝3 小时前
Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂
前端·javascript·vue.js·vue·前端开发
wangyadong3173 小时前
# el-table 复选框无法选中问题记录
前端·javascript·vue.js
Hilaku4 小时前
为什么前端应该主动去学数据库?
前端·javascript·程序员
朱 欢 庆4 小时前
el-table 表格组件二次封装
javascript·vue.js·elementui
怕浪猫6 小时前
分享一个做视频的skill,这条白板视频,每一笔都是代码画的
前端·javascript·面试
OpsEye10 小时前
当企业不再只关心能不能调用模型,而是关心值不值得调用
javascript·ai编程