|
@@ -1,1433 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="map-page">
|
|
|
- <!-- 新增加载提示 -->
|
|
|
- <div v-if="isLoading" class="loading-overlay">
|
|
|
- <div class="loading-spinner"></div>
|
|
|
- <p>地图数据加载中...</p>
|
|
|
- </div>
|
|
|
- <!-- 新增工具栏容器 -->
|
|
|
- <div class="map-toolbar">
|
|
|
- <RegionSelector
|
|
|
- class="compact-region-selector"
|
|
|
- ref="regionSelector"
|
|
|
- @region-change="handleRegionChange" />
|
|
|
-
|
|
|
- </div>
|
|
|
- <div ref="mapContainer"
|
|
|
- class="map-container"></div>
|
|
|
- <div v-if="error" class="error">{{ error }}</div>
|
|
|
-
|
|
|
- <div class="control-panel">
|
|
|
- <div class="basemap-toggle">
|
|
|
- <button @click="toggleBaseLayer" :class="{ active: isBaseLayer }">
|
|
|
- {{ isBaseLayer ? '纯净地图' : '腾讯地图' }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <label>
|
|
|
- <input type="checkbox" v-model="state.showSoilTypes" @change="toggleSoilTypeLayer" />
|
|
|
- 显示韶关市评估单元
|
|
|
- </label>
|
|
|
- <label>
|
|
|
- <input type="checkbox" v-model="state.showSurveyData" @change="toggleSurveyDataLayer" />
|
|
|
- 显示韶关市调查数据
|
|
|
- </label>
|
|
|
- <!-- 截图控制 -->
|
|
|
- <div class="export-controls">
|
|
|
- <button @click="exportMapImage" :disabled="!isMapReady">
|
|
|
- {{ isExporting ? '生成中...' : '导出截图' }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="map-legend" :class="{ active: isShowLegend }">
|
|
|
- <div class="legend-controls">
|
|
|
- <button @click="switchLegendType('cdRisk')">Cd风险</button>
|
|
|
- <button @click="switchLegendType('safetyQ')">安全指数Q</button>
|
|
|
- </div>
|
|
|
- <div class="legend-header">
|
|
|
- <h1>图例</h1>
|
|
|
- </div>
|
|
|
- <div class="legend-header">
|
|
|
- <h4>{{ currentLegendTitle }}</h4>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Cd风险等级图例 -->
|
|
|
- <div v-if="currentLegend === 'cdRisk'" class="legend-section">
|
|
|
- <div class="legend-scale">
|
|
|
- <div class="scale-item" v-for="(item, index) in cdRiskLegend" :key="index">
|
|
|
- <div class="color-box" :style="{ backgroundColor: item.color }"></div>
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 安全指数Q图例 -->
|
|
|
- <div v-if="currentLegend === 'safetyQ'" class="legend-section">
|
|
|
- <div class="legend-scale">
|
|
|
- <div class="scale-item" v-for="(item, index) in safetyQLegend" :key="index">
|
|
|
- <div class="color-box" :style="{ backgroundColor: item.color }"></div>
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="legend-source">
|
|
|
- <p>注:Q = 阈值/污染物含量</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
|
|
-import html2canvas from 'html2canvas'
|
|
|
-import RegionSelector from '@/components/RegionSelector.vue'
|
|
|
-
|
|
|
-const isExporting = ref(false)
|
|
|
-const isMapReady = ref(false)
|
|
|
-const exportSettings = reactive({
|
|
|
- quality: 0.9,
|
|
|
- showMapControls: false,
|
|
|
- showWatermark: true
|
|
|
-})
|
|
|
-const mapContainer = ref(null)
|
|
|
-const activeMarker = ref(null)
|
|
|
-const error = ref(null)
|
|
|
-let activeTempMarker = ref(null)
|
|
|
-let infoWindow = ref(null)
|
|
|
-let map = null
|
|
|
-let markersLayer = null
|
|
|
-let overlay = null
|
|
|
-const state = reactive({
|
|
|
- showOverlay: false,
|
|
|
- showSoilTypes: true,
|
|
|
- showSurveyData: true,
|
|
|
- excelData: [],
|
|
|
- lastTapTime: 0
|
|
|
-})
|
|
|
-let soilTypeLayer = null
|
|
|
-let geoJSONLayer;
|
|
|
-let currentInfoWindow = null;
|
|
|
-const surveyDataLayer = ref(null); // 保持响应式引用
|
|
|
-let multiPolygon;
|
|
|
-const districtLayers = ref(new Map()) // 存储区县图层
|
|
|
-const combinedSurveyFeatures = ref([]); // 存储原始数据
|
|
|
-const currentSurveyFilter = ref([]); // 当前选中区域
|
|
|
-const isLoading = ref(false)
|
|
|
-
|
|
|
-const categoryColors = { // 分类颜色配置
|
|
|
- '优先保护类': '#00C853', // 绿色
|
|
|
- '安全利用类': '#FFD600', // 黄色
|
|
|
- '严格管控类': '#D50000', // 红色
|
|
|
- '其他': '#CCCCCC', // 灰色
|
|
|
- '农产品样品': '#4CAF50', // 绿色
|
|
|
- '土壤样品': '#2196F3' // 蓝色
|
|
|
-};
|
|
|
-
|
|
|
-const isShowLegend = ref(true)
|
|
|
-const currentLegend = ref('cdRisk') // 默认显示Cd风险图例
|
|
|
-
|
|
|
-/// 图例配置数据
|
|
|
-const cdRiskLegend = reactive([
|
|
|
- { label: '无风险', color: '#00C853' },
|
|
|
- { label: '中低风险', color: '#FFD600' },
|
|
|
- { label: '高风险', color: '#D50000' }
|
|
|
-])
|
|
|
-
|
|
|
-const safetyQLegend = reactive([
|
|
|
- { label: 'Q < 1', color: '#00C853' },
|
|
|
- { label: '1 < Q < 5', color: '#FFD600' },
|
|
|
- { label: 'Q > 5', color: '#D50000' }
|
|
|
-])
|
|
|
-
|
|
|
-// 图例标题映射
|
|
|
-const legendTitles = {
|
|
|
- cdRisk: 'Cd污染风险等级',
|
|
|
- safetyQ: '安全生产指数Q'
|
|
|
-}
|
|
|
-
|
|
|
-const currentLegendTitle = computed(() => legendTitles[currentLegend.value])
|
|
|
-
|
|
|
-// 切换图例显示
|
|
|
-const toggleLegend = () => {
|
|
|
- isShowLegend.value = !isShowLegend.value
|
|
|
-}
|
|
|
-
|
|
|
-// 切换图例类型
|
|
|
-const switchLegendType = (type) => {
|
|
|
- currentLegend.value = type
|
|
|
-}
|
|
|
-
|
|
|
-const tMapConfig = reactive({
|
|
|
- key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
|
|
|
- geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
|
|
|
-})
|
|
|
-
|
|
|
-const isBaseLayer = ref(false)
|
|
|
-const layerVisibility = reactive({
|
|
|
- province: false,
|
|
|
- city: false,
|
|
|
- county: false
|
|
|
-})
|
|
|
-const currentZoom = ref(12)
|
|
|
-
|
|
|
-// 预加载所有GeoJSON图层
|
|
|
-const geoLayers = reactive({
|
|
|
- province: null,
|
|
|
- city: null,
|
|
|
- county: null
|
|
|
-})
|
|
|
-
|
|
|
-const initBaseLayers = async () => {
|
|
|
- try {
|
|
|
- // 按层级加载GeoJSON(需替换实际路径)
|
|
|
- isLoading.value = true;
|
|
|
- geoLayers.province = await loadAndCreateLayer('/data/省.geojson', 'province')
|
|
|
- geoLayers.city = await loadAndCreateLayer('/data/市.geojson', 'city')
|
|
|
- geoLayers.county = await loadAndCreateLayer('/data/县.geojson', 'county')
|
|
|
-
|
|
|
- // 初始化默认状态
|
|
|
- updateLayerVisibility()
|
|
|
- } catch (error) {
|
|
|
- console.error('加载地理数据失败:', error)
|
|
|
- error.value = '地理数据加载失败'
|
|
|
- } finally {
|
|
|
- isLoading.value = false;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 创建带样式的图层
|
|
|
-const loadAndCreateLayer = async (url, type) => {
|
|
|
- const geoData = await loadGeoJSON(url)
|
|
|
- return new TMap.value.vector.GeoJSONLayer({
|
|
|
- map: map,
|
|
|
- data: geoData,
|
|
|
- zIndex: 5,
|
|
|
- polygonStyle: new TMap.value.PolygonStyle({
|
|
|
- color: 'rgba(242, 241, 237, 1)',
|
|
|
- borderColor: '#000000',
|
|
|
- borderWidth: 1
|
|
|
- })
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// 智能切换核心方法
|
|
|
-const toggleBaseLayer = () => {
|
|
|
- isBaseLayer.value = !isBaseLayer.value
|
|
|
- // 新增地图样式切换逻辑
|
|
|
- if (map) {
|
|
|
- map.setMapStyleId(isBaseLayer.value ? '1' : '0')
|
|
|
- }
|
|
|
-
|
|
|
- if (isBaseLayer.value) {
|
|
|
- map.on('zoom', handleZoomChange)
|
|
|
- updateLayerVisibility()
|
|
|
- } else {
|
|
|
- map.off('zoom', handleZoomChange)
|
|
|
- hideAllLayers()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 缩放事件处理
|
|
|
-const handleZoomChange = () => {
|
|
|
- currentZoom.value = map.getZoom()
|
|
|
- updateLayerVisibility()
|
|
|
-}
|
|
|
-
|
|
|
-// 图层可见性逻辑
|
|
|
-const updateLayerVisibility = () => {
|
|
|
- const zoom = currentZoom.value
|
|
|
- const rules = [
|
|
|
- { min: 0, max: 5, types: ['province'] },
|
|
|
- { min: 5, max: 10, types: ['city'] },
|
|
|
- { min: 10, max: 20, types: ['county'] }
|
|
|
- ]
|
|
|
-
|
|
|
- rules.forEach(rule => {
|
|
|
- const isActive = zoom >= rule.min && zoom <= rule.max
|
|
|
- rule.types.forEach(type => {
|
|
|
- layerVisibility[type] = isActive && isBaseLayer.value
|
|
|
- geoLayers[type]?.setVisible(isActive && isBaseLayer.value)
|
|
|
- })
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// 清理方法
|
|
|
-const hideAllLayers = () => {
|
|
|
- Object.values(geoLayers).forEach(layer => {
|
|
|
- if (layer) layer.setVisible(false)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-const loadSDK = () => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (window.TMap?.service?.Geocoder) {
|
|
|
- TMap.value = window.TMap
|
|
|
- return resolve(window.TMap)
|
|
|
- }
|
|
|
-
|
|
|
- const script = document.createElement('script')
|
|
|
- script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
|
|
|
- window.initTMap = () => {
|
|
|
- if (!window.TMap?.service?.Geocoder) {
|
|
|
- reject(new Error('地图SDK加载失败'))
|
|
|
- return
|
|
|
- }
|
|
|
- TMap.value = window.TMap
|
|
|
- resolve(window.TMap)
|
|
|
- }
|
|
|
-
|
|
|
- script.onerror = (err) => {
|
|
|
- reject(`地图资源加载失败: ${err.message}`)
|
|
|
- document.head.removeChild(script)
|
|
|
- }
|
|
|
-
|
|
|
- document.head.appendChild(script)
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// 初始化数据
|
|
|
-const initData = () => {
|
|
|
- state.excelData = [
|
|
|
- { 土壤编号: "土1", 地点: "广西武鸣", dust_emissions: 5.34, longitude: 106.476143, latitude: 23.891756 },
|
|
|
- { 土壤编号: "土2", 地点: "广西河池", dust_emissions: 3.96, longitude: 107.476143, latitude: 24.891756 },
|
|
|
- { 土壤编号: "土3", 地点: "海南澄迈老城镇罗驿村委会罗驿洋", dust_emissions: 4.56, longitude: 110.125, latitude: 19.901756 },
|
|
|
- { 土壤编号: "土4", 地点: "广东江门新会", dust_emissions: 4.0, longitude: 109.476143, latitude: 22.461756 },
|
|
|
- { 土壤编号: "土5", 地点: "广州增城Z6", dust_emissions: 4.77, longitude: 110.476143, latitude: 21.891756 },
|
|
|
- { 土壤编号: "土6", 地点: "广州增城Z8", dust_emissions: 4.59, longitude: 111.476143, latitude: 22.891756 },
|
|
|
- { 土壤编号: "土7", 地点: "湖南岳阳", dust_emissions: 5.14, longitude: 112.476143, latitude: 23.891756 },
|
|
|
- { 土壤编号: "土8", 地点: "广东韶关武江", dust_emissions: 5.07, longitude: 113.476143, latitude: 24.891756 },
|
|
|
- { 土壤编号: "土9", 地点: "海南临高头星村", dust_emissions: 4.12, longitude: 109.684993, latitude: 19.83774 },
|
|
|
- { 土壤编号: "土10", 地点: "海南临高周礼村", dust_emissions: 5.0, longitude: 109.710703, latitude: 19.89222 },
|
|
|
- { 土壤编号: "土11", 地点: "海南澄迈金江", dust_emissions: 4.6, longitude: 110.069537, latitude: 19.81189 },
|
|
|
- { 土壤编号: "土12", 地点: "海南临高南贤村", dust_emissions: 4.2, longitude: 109.768714, latitude: 19.874323 },
|
|
|
- { 土壤编号: "土13", 地点: "海南澄迈金江北让村", dust_emissions: 4.5, longitude: 110.096765, latitude: 19.814288 },
|
|
|
- { 土壤编号: "土14", 地点: "广西扶绥", dust_emissions: 4.71, longitude: 107.7717789, latitude: 22.5166902 },
|
|
|
- { 土壤编号: "土15", 地点: "广西江州", dust_emissions: 4.31, longitude: 107.56347787, latitude: 22.6022203 },
|
|
|
- { 土壤编号: "土16", 地点: "广西龙州", dust_emissions: 5.15, longitude: 106.7870847, latitude: 22.3496497 },
|
|
|
- { 土壤编号: "土17", 地点: "广西大新", dust_emissions: 4.71, longitude: 107.0230641, latitude: 22.5857946 },
|
|
|
- { 土壤编号: "土18", 地点: "湖南岳阳荣家湾", dust_emissions: 5.04, longitude: 113.059629, latitude: 29.267061 },
|
|
|
- { 土壤编号: "土19", 地点: "湖南长沙", dust_emissions: 5.08, longitude: 113.059629, latitude: 28.440613 },
|
|
|
- { 土壤编号: "土20", 地点: "浙江", dust_emissions: 4.8, longitude: 111.45527, latitude: 24.395235 },
|
|
|
- { 土壤编号: "土21", 地点: "云南陆良", dust_emissions: 4.67, longitude: 112.45527, latitude: 25.395235 },
|
|
|
- { 土壤编号: "土22", 地点: "南昌横龙镇南园组", dust_emissions: 4.8, longitude: 113.45527, latitude: 26.395235 },
|
|
|
- { 土壤编号: "土23", 地点: "南昌横龙枫塘南园", dust_emissions: 5.1, longitude: 114.45527, latitude: 27.395235 },
|
|
|
- { 土壤编号: "土24", 地点: "南昌横龙镇院塘村", dust_emissions: 4.27, longitude: 114.852, latitude: 27.3947 },
|
|
|
- { 土壤编号: "土25", 地点: "江西山庄乡秀水村黄田组", dust_emissions: 4.27, longitude: 114.852, latitude: 27.5247 },
|
|
|
- { 土壤编号: "土26", 地点: "贵州双星村", dust_emissions: 4.7, longitude: 106.852, latitude: 27.3147},
|
|
|
- { 土壤编号: "土27", 地点: "湖南永州八宝镇唐家州", dust_emissions: 4.57, longitude: 113.952, latitude: 26.08147 },
|
|
|
- { 土壤编号: "土28", 地点: "湖南永州金洞", dust_emissions: 5.3, longitude: 112.1564, latitude: 26.1685 },
|
|
|
- { 土壤编号: "土29", 地点: "祁阳县中国农业科学院红壤实验室", dust_emissions: 4.75, longitude: 111.4, latitude: 22.24 },
|
|
|
- { 土壤编号: "土30", 地点: "福建福州1", dust_emissions: 4.31, longitude: 112.4, latitude: 23.24 },
|
|
|
- { 土壤编号: "土31", 地点: "福建福州2", dust_emissions: 4.38, longitude: 113.4, latitude: 24.24 },
|
|
|
- { 土壤编号: "土32", 地点: "广东省韶关市南雄市下塅村", dust_emissions: 5.51, longitude: 114.4, latitude: 25.24 },
|
|
|
- { 土壤编号: "土33", 地点: "广东省韶关市南雄市河塘西216米", dust_emissions: 6.44, longitude: 114.28, latitude: 25.14 },
|
|
|
- { 土壤编号: "土34", 地点: "广东省韶关市南雄市上何屋西南500米", dust_emissions: 5.25, longitude: 114.15, latitude: 24.86 },
|
|
|
- { 土壤编号: "土35", 地点: "广东省南雄市雄州街道林屋", dust_emissions: 4.62333333333333, longitude: 114.23, latitude: 25.4 },
|
|
|
- { 土壤编号: "土36", 地点: "广东省台山都斛镇", dust_emissions: 3.0, longitude: 112.34, latitude: 27.31 },
|
|
|
- { 土壤编号: "土52", 地点: "湖南省长沙市浏阳市永安镇千鹭湖", dust_emissions: 4.72333333333333, longitude: 113.34, latitude: 28.31 },
|
|
|
- { 土壤编号: "土53", 地点: "湖南省长沙市浏阳市湖南农大实习基地", dust_emissions: 5.55333333333333, longitude: 113.83, latitude: 28.3 },
|
|
|
- { 土壤编号: "土54", 地点: "湖南省邵阳市罗市镇1", dust_emissions: 4.64, longitude: 110.35, latitude: 25.47 },
|
|
|
- { 土壤编号: "土55", 地点: "湖南省邵阳市罗市镇2", dust_emissions: 5.01333333333333, longitude: 111.35, latitude: 26.47 },
|
|
|
- { 土壤编号: "土56", 地点: "湖南省邵阳市罗市镇3", dust_emissions: 5.18, longitude: 112.35, latitude: 27.47 },
|
|
|
- { 土壤编号: "土57", 地点: "长沙县高桥镇的省农科院高桥科研基地1", dust_emissions: 5.1, longitude: 113.35, latitude: 28.47 },
|
|
|
- { 土壤编号: "土58", 地点: "长沙县高桥镇的省农科院高桥科研基地2", dust_emissions: 4.92, longitude: 113.35, latitude: 28.47 },
|
|
|
- { 土壤编号: "土59", 地点: "湖南省长沙市望城区桐林坳社区", dust_emissions: 3.0, longitude: 112.8, latitude: 28.37 },
|
|
|
- { 土壤编号: "土60", 地点: "湖南省益阳市赫山区泥江口镇", dust_emissions: 3.0, longitude: 107.37, latitude: 21.92 },
|
|
|
- { 土壤编号: "土70", 地点: "南宁市兴宁区柳杨路26号", dust_emissions: 3.0, longitude: 108.37, latitude: 22.92 },
|
|
|
- { 土壤编号: "土71", 地点: "南宁市兴宁区柳杨路广西私享家家具用品", dust_emissions: 3.0, longitude: 108.37, latitude: 23.94 },
|
|
|
- { 土壤编号: "土72", 地点: "南宁市兴宁区004乡道", dust_emissions: 6.24666666666667, longitude: 108.39, latitude: 24.92 },
|
|
|
- { 土壤编号: "土73", 地点: "南宁市兴宁区G7201南宁绕城高速", dust_emissions: 3.0, longitude: 108.4, latitude: 25.94 },
|
|
|
- { 土壤编号: "土74", 地点: "南宁市兴宁区012县道", dust_emissions: 3.0, longitude: 108.41, latitude: 26.92 },
|
|
|
- { 土壤编号: "土75", 地点: "南宁市兴宁区那况路168号", dust_emissions: 3.0, longitude: 108.4, latitude: 27.9 },
|
|
|
- { 土壤编号: "土76", 地点: "南宁市西乡塘区翊武路", dust_emissions: 5.37, longitude: 108.35, latitude: 28.96 },
|
|
|
- { 土壤编号: "土77", 地点: "南宁市西乡塘区坛洛镇", dust_emissions: 3.0, longitude: 107.85, latitude: 29.92 },
|
|
|
- { 土壤编号: "土81", 地点: "铜仁职业技术学院", dust_emissions: 4.0, longitude: 108.85, latitude: 27.34 },
|
|
|
- { 土壤编号: "土87", 地点: "江西省红壤及种质资源研究所(进贤基地)1", dust_emissions: 4.55, longitude: 116.17, latitude: 28.34 },
|
|
|
- { 土壤编号: "土88", 地点: "江西省红壤及种质资源研究所(进贤基地)2", dust_emissions: 4.99333333333333, longitude: 116.17, latitude: 28.34 }
|
|
|
- ].map(item => {
|
|
|
- const lat = Number(item.latitude);
|
|
|
- const lng = Number(item.longitude);
|
|
|
-
|
|
|
- if (isNaN(lat) || isNaN(lng)) {
|
|
|
- console.error('无效的经纬度数据:', item);
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- ...item,
|
|
|
- latitude: lat,
|
|
|
- longitude: lng
|
|
|
- };
|
|
|
- }).filter(item => item !== null);
|
|
|
-}
|
|
|
-
|
|
|
-// 初始化地图
|
|
|
-const initMap = async () => {
|
|
|
- try {
|
|
|
- isLoading.value = true
|
|
|
- await loadSDK()
|
|
|
-
|
|
|
- map = new TMap.value.Map(mapContainer.value, {
|
|
|
- center: new TMap.value.LatLng(24.81088,113.59762),
|
|
|
- zoom: 12,
|
|
|
- renderOptions: {
|
|
|
- preserveDrawingBuffer: true, // 必须开启以支持截图
|
|
|
- antialias: true
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // const defaultStyle = new TMap.value.MarkerStyle({
|
|
|
- // width: 34,
|
|
|
- // height: 34,
|
|
|
- // anchor: { x: 17, y: 34 },
|
|
|
- // src: markerIcon
|
|
|
- // })
|
|
|
-
|
|
|
- // markersLayer = new TMap.value.MultiMarker({
|
|
|
- // map: map,
|
|
|
- // styles: { default: defaultStyle }
|
|
|
- // })
|
|
|
- const geojsonData = await loadGeoJSON('https://soilgd.com:8000/api/vector/export/all?table_name=unit_ceil');
|
|
|
- initMapWithGeoJSON(geojsonData, map);
|
|
|
- await initSurveyDataLayer(map);
|
|
|
- filterSurveyDataLayer(currentSurveyFilter.value)
|
|
|
- // 绑定点击事件
|
|
|
- // map.on('click', handleMapClick)
|
|
|
- // markersLayer.on('click', handleMarkerClick)
|
|
|
- // 新增地图就绪状态监听
|
|
|
- map.on('idle', () => {
|
|
|
- isMapReady.value = true
|
|
|
- })
|
|
|
-
|
|
|
- loadData()
|
|
|
- updateMarkers()
|
|
|
- } catch (err) {
|
|
|
- error.value = err.message
|
|
|
- } finally {
|
|
|
- isLoading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 加载数据并创建标记
|
|
|
-const loadData = () => {
|
|
|
- const geometries = state.excelData.map(item => ({
|
|
|
- id: item.土壤编号,
|
|
|
- styleId: 'default',
|
|
|
- position: new TMap.value.LatLng(item.latitude, item.longitude),
|
|
|
- properties: {
|
|
|
- title: item.地点,
|
|
|
- phValue: parseFloat(item.dust_emissions).toFixed(2),
|
|
|
- isTemp: false
|
|
|
- },
|
|
|
- }))
|
|
|
- markersLayer.setGeometries(geometries)
|
|
|
-}
|
|
|
-
|
|
|
-// 新增截图方法
|
|
|
-const exportMapImage = async () => {
|
|
|
- try {
|
|
|
- isExporting.value = true
|
|
|
-
|
|
|
- // 等待地图稳定
|
|
|
- await new Promise(resolve => setTimeout(resolve, 300))
|
|
|
-
|
|
|
- const canvas = await html2canvas(mapContainer.value, {
|
|
|
- useCORS: true,
|
|
|
- scale: window.devicePixelRatio || 2,
|
|
|
- backgroundColor: null,
|
|
|
- logging: true,
|
|
|
- onclone: (clonedDoc) => {
|
|
|
- // 处理控件可见性
|
|
|
- clonedDoc.querySelectorAll('.tmap-control').forEach(control => {
|
|
|
- control.style.visibility = exportSettings.showMapControls ? 'visible' : 'hidden'
|
|
|
- })
|
|
|
-
|
|
|
- // 添加水印
|
|
|
- if(exportSettings.showWatermark){
|
|
|
- const watermark = document.createElement('div')
|
|
|
- watermark.style = `
|
|
|
- position: absolute;
|
|
|
- bottom: 20px;
|
|
|
- right: 20px;
|
|
|
- color: rgba(0,0,0,0.2);
|
|
|
- font-size: 24px;
|
|
|
- transform: rotate(-15deg);
|
|
|
- z-index: 9999;
|
|
|
- `
|
|
|
- watermark.textContent = '机密地图 - 禁止外传'
|
|
|
- clonedDoc.body.appendChild(watermark)
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // 转换为Blob
|
|
|
- canvas.toBlob(blob => {
|
|
|
- const link = document.createElement('a')
|
|
|
- link.download = `土壤地图_${new Date().toISOString().slice(0,10)}.png`
|
|
|
- link.href = URL.createObjectURL(blob)
|
|
|
- link.click()
|
|
|
- URL.revokeObjectURL(link.href)
|
|
|
- }, 'image/png', exportSettings.quality)
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('截图失败:', error)
|
|
|
- error.value = '截图失败,请尝试缩小地图层级'
|
|
|
- setTimeout(() => error.value = null, 3000)
|
|
|
- } finally {
|
|
|
- isExporting.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 更新标记
|
|
|
-const updateMarkers = () => {
|
|
|
- const markers = state.excelData.map((item, index) => ({
|
|
|
- id: `marker-${index + 1}`,
|
|
|
- styleId: 'default',
|
|
|
- position: new TMap.value.LatLng(item.latitude, item.longitude),
|
|
|
- properties: {
|
|
|
- title: item.地点,
|
|
|
- phValue: item.dust_emissions,
|
|
|
- isTemp: false
|
|
|
- },
|
|
|
- }))
|
|
|
- markersLayer.setGeometries(markers)
|
|
|
-}
|
|
|
-
|
|
|
-// 新增Marker点击事件处理
|
|
|
-const handleMarkerClick = (e) => {
|
|
|
- const marker = e.geometry
|
|
|
- if (!marker) return
|
|
|
-
|
|
|
- // 关闭之前的信息窗口
|
|
|
- if (activeMarker.value?.id === marker.id) {
|
|
|
- infoWindow.close()
|
|
|
- activeMarker.value = null
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 创建信息窗口内容
|
|
|
- const content = `
|
|
|
- <div style="padding:12px">
|
|
|
- <h3>${marker.properties.title}</h3>
|
|
|
- <p>PH值: ${marker.properties.phValue}</p>
|
|
|
- </div>
|
|
|
- `
|
|
|
-
|
|
|
- // 打开信息窗口
|
|
|
- infoWindow = new TMap.value.InfoWindow({
|
|
|
- map: map,
|
|
|
- position: marker.position,
|
|
|
- content: content,
|
|
|
- offset: {x: 0, y: -32}
|
|
|
- })
|
|
|
-
|
|
|
- // 记录当前激活的Marker
|
|
|
- activeMarker.value = marker
|
|
|
-
|
|
|
- // 点击其他区域关闭窗口
|
|
|
- map.on('click', closeInfoWindow)
|
|
|
-}
|
|
|
-
|
|
|
-const manageTempMarker = {
|
|
|
- add: (lat, lng, phValue) => {
|
|
|
- if (activeTempMarker.value) {
|
|
|
- markersLayer.remove("-999")
|
|
|
- }
|
|
|
-
|
|
|
- const tempMarker = markersLayer.add({
|
|
|
- id: "-999",
|
|
|
- position: new TMap.value.LatLng(lat, lng),
|
|
|
- styleId: 'temp',
|
|
|
- properties: {
|
|
|
- title: '克里金插值',
|
|
|
- phValue: parseFloat(phValue).toFixed(2),
|
|
|
- isTemp: true
|
|
|
- }
|
|
|
- })
|
|
|
- activeTempMarker.value = tempMarker
|
|
|
- },
|
|
|
- remove: () => {
|
|
|
- if (activeTempMarker.value) {
|
|
|
- markersLayer.remove("-999")
|
|
|
- activeTempMarker.value = null
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// const handleMapClick = async (e) => {
|
|
|
-// if (selectedPolygon.value) {
|
|
|
-// resetPolygonStyle();
|
|
|
-// infoWindow.value?.close();
|
|
|
-// }
|
|
|
-// const now = Date.now()
|
|
|
-
|
|
|
-// if (now - state.lastTapTime < 1000) return
|
|
|
-// state.lastTapTime = now
|
|
|
-
|
|
|
-// try {
|
|
|
-// const latLng = e?.latLng
|
|
|
-// if (!latLng) throw new Error("地图点击事件缺少坐标信息")
|
|
|
-
|
|
|
-// const lat = Number(latLng.lat)
|
|
|
-// const lng = Number(latLng.lng)
|
|
|
-
|
|
|
-// if (!isValidCoordinate(lat, lng)) throw new Error(`非法坐标值 (${lat}, ${lng})`)
|
|
|
-
|
|
|
-// console.log('有效坐标:', lat, lng)
|
|
|
-
|
|
|
-// const result = await reverseGeocode(lat, lng)
|
|
|
-// if (!validateLocation(result)) throw new Error('非有效陆地区域')
|
|
|
-
|
|
|
-// const phValue = await getPhValue(lng, lat)
|
|
|
-
|
|
|
-// // 使用封装方法添加临时标记
|
|
|
-// manageTempMarker.add(lat, lng, phValue)
|
|
|
-
|
|
|
-// if (infoWindow.value) {
|
|
|
-// infoWindow.value.close()
|
|
|
-// }
|
|
|
-// infoWindow.value = new TMap.value.InfoWindow({
|
|
|
-// map: map,
|
|
|
-// position: manageTempMarker.activeTempMarker.value.getPosition(),
|
|
|
-// content: `
|
|
|
-// <div style="padding:12px">
|
|
|
-// <h3>${manageTempMarker.activeTempMarker.value.properties.title}</h3>
|
|
|
-// <p>PH值: ${manageTempMarker.activeTempMarker.value.properties.phValue}</p>
|
|
|
-// </div>
|
|
|
-// `
|
|
|
-// })
|
|
|
-// infoWindow.value.open()
|
|
|
-// } catch (error) {
|
|
|
-// console.error('操作失败详情:', error)
|
|
|
-// error.value = error.message.includes('非法坐标')
|
|
|
-// ? '请点击有效地图区域'
|
|
|
-// : '服务暂时不可用,请稍后重试'
|
|
|
-// setTimeout(() => error.value = null, 3000)
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
-// // 关闭信息窗口时同步移除临时标记
|
|
|
-// const closeInfoWindow = () => {
|
|
|
-// if (activeTempMarker.value) {
|
|
|
-// manageTempMarker.remove()
|
|
|
-// }
|
|
|
-// if (infoWindow.value) {
|
|
|
-// infoWindow.value.close()
|
|
|
-// infoWindow.value = null
|
|
|
-// }
|
|
|
-// map.off('click', closeInfoWindow)
|
|
|
-// }
|
|
|
-
|
|
|
-
|
|
|
-// // 验证坐标有效性
|
|
|
-// const isValidCoordinate = (lat, lng) => {
|
|
|
-// return !isNaN(lat) && !isNaN(lng) &&
|
|
|
-// lat >= -90 && lat <= 90 &&
|
|
|
-// lng >= -180 && lng <= 180
|
|
|
-// }
|
|
|
-
|
|
|
-// // 逆地理编码
|
|
|
-// const reverseGeocode = (lat, lng) => {
|
|
|
-// return new Promise((resolve, reject) => {
|
|
|
-// const callbackName = `tmap_callback_${Date.now()}`
|
|
|
-// window[callbackName] = (response) => {
|
|
|
-// delete window[callbackName]
|
|
|
-// document.body.removeChild(script)
|
|
|
-// if (response.status !== 0) reject(response.message)
|
|
|
-// else resolve(response.result)
|
|
|
-// }
|
|
|
-
|
|
|
-// const script = document.createElement('script')
|
|
|
-// script.src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${tMapConfig.key}&output=jsonp&callback=${callbackName}`
|
|
|
-// script.onerror = reject
|
|
|
-// document.body.appendChild(script)
|
|
|
-// })
|
|
|
-// }
|
|
|
-
|
|
|
-// // 验证地理位置
|
|
|
-// const validateLocation = (result) => {
|
|
|
-// if (!result || !result.address_component) {
|
|
|
-// return false;
|
|
|
-// }
|
|
|
-// return result.address_component.nation === '中国' &&
|
|
|
-// !['香港特别行政区', '澳门特别行政区', '台湾省'].includes(
|
|
|
-// result.address_component.province
|
|
|
-// )
|
|
|
-// }
|
|
|
-
|
|
|
-// // 获取PH值
|
|
|
-// const getPhValue = async (lng, lat) => {
|
|
|
-// try {
|
|
|
-// const { data } = await axios.post('https://soilgd.com:5000/kriging_interpolation', {
|
|
|
-// file_name: 'emissions.xlsx',
|
|
|
-// emission_column: 'dust_emissions',
|
|
|
-// points: [[lng, lat]]
|
|
|
-// })
|
|
|
-// return parseFloat(data.interpolated_concentrations[0]).toFixed(2)
|
|
|
-// } catch (error) {
|
|
|
-// console.error('获取PH值失败:', error)
|
|
|
-// throw error
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
-async function loadGeoJSON(url) {
|
|
|
- const response = await fetch(url);
|
|
|
- return await response.json();
|
|
|
-}
|
|
|
-
|
|
|
-const handleRegionChange = async (districtNames) => {
|
|
|
- isLoading.value = true;
|
|
|
- console.log('收到区域变更:', districtNames)
|
|
|
- currentSurveyFilter.value = districtNames;
|
|
|
-
|
|
|
-
|
|
|
- // // 删除已取消选择的图层
|
|
|
- // Array.from(districtLayers.value.keys()).forEach(name => {
|
|
|
- // if (!districtNames.includes(name)) {
|
|
|
- // const layer = districtLayers.value.get(name)
|
|
|
- // layer.setMap(null) // 正确销毁图层
|
|
|
- // districtLayers.value.delete(name)
|
|
|
- // }
|
|
|
- // })
|
|
|
-
|
|
|
- // // 添加新选择的图层
|
|
|
- // await Promise.all(districtNames.map(async name => {
|
|
|
- // if (!districtLayers.value.has(name)) {
|
|
|
- // try {
|
|
|
- // const geoData = await loadGeoJSON(`/data/${name}.geojson`)
|
|
|
-
|
|
|
- // // 创建独立图层实例
|
|
|
- // const layer = new TMap.value.vector.GeoJSONLayer({
|
|
|
- // map: map, // 确保传入当前地图实例
|
|
|
- // data: geoData,
|
|
|
- // zIndex: 3,
|
|
|
- // styles: {
|
|
|
- // // 按腾讯地图规范定义样式
|
|
|
- // polygonStyle: new TMap.value.PolygonStyle({
|
|
|
- // color: randomRGBA(0.3),
|
|
|
- // borderColor: '#FF0000',
|
|
|
- // borderWidth: 2
|
|
|
- // })
|
|
|
- // }
|
|
|
- // })
|
|
|
-
|
|
|
- // districtLayers.value.set(name, layer)
|
|
|
- // } catch (error) {
|
|
|
- // console.error(`加载【${name}】边界失败:`, error)
|
|
|
- // }
|
|
|
- // }
|
|
|
- // }))
|
|
|
- filterSurveyDataLayer(districtNames);
|
|
|
- isLoading.value = false;
|
|
|
-}
|
|
|
-
|
|
|
-const filterSurveyDataLayer = (selectedRegions) => {
|
|
|
- // ===== 1. 销毁旧图层 ===== [1,3](@ref)
|
|
|
- if (surveyDataLayer.value) {
|
|
|
- surveyDataLayer.value.setMap(null); // 从地图解除关联
|
|
|
- surveyDataLayer.value.destroy(); // 释放内存资源
|
|
|
- surveyDataLayer.value = null; // 清除引用
|
|
|
- }
|
|
|
-
|
|
|
- const mergedCategoryColors = {
|
|
|
- ...categoryColors,
|
|
|
- };
|
|
|
-
|
|
|
- // 创建样式(包含默认分类)
|
|
|
- const pointStyles = Object.keys(mergedCategoryColors).map(category => ({
|
|
|
- id: category,
|
|
|
- style: new TMap.value.MarkerStyle({
|
|
|
- width: 12,
|
|
|
- height: 12,
|
|
|
- anchor: { x: 6, y: 6 },
|
|
|
- src: createColoredCircle(mergedCategoryColors[category])
|
|
|
- })
|
|
|
- }));
|
|
|
- const layerId = `survey-layer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
- surveyDataLayer.value = new TMap.value.MultiMarker({
|
|
|
- id: layerId,
|
|
|
- map: map,
|
|
|
- styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
|
|
|
- geometries: [] // 初始空数据
|
|
|
- });
|
|
|
- if (!surveyDataLayer?.value) {
|
|
|
- throw new Error("调查数据图层未初始化")
|
|
|
- }
|
|
|
- if (!combinedSurveyFeatures?.value) {
|
|
|
- throw new Error("调查数据未加载")
|
|
|
- }
|
|
|
- console.groupCollapsed("[区域过滤] 调试信息");
|
|
|
- console.log("🔄 收到过滤请求,当前选中区域:", selectedRegions);
|
|
|
- console.log("📦 原始数据总量:", combinedSurveyFeatures.value.length);
|
|
|
- console.log(combinedSurveyFeatures.value);
|
|
|
-
|
|
|
- const filtered = selectedRegions.length === 0
|
|
|
- ? combinedSurveyFeatures.value
|
|
|
- : combinedSurveyFeatures.value.filter(feature => {
|
|
|
- const xmc = feature.properties.XMC || '';
|
|
|
- return selectedRegions.some(region => xmc.includes(region));
|
|
|
- });
|
|
|
- console.log("✅ 过滤后数据量:", filtered.length);
|
|
|
- console.log("🔍 示例过滤后数据:", filtered.slice(0,3).map(f => ({
|
|
|
- id: f.properties.ID || f.properties.OBJECTID,
|
|
|
- XMC: f.properties.XMC,
|
|
|
- CMC: f.properties.CMC,
|
|
|
- H_XTFX: f.properties.H_XTFX,
|
|
|
- })));
|
|
|
- console.groupEnd();
|
|
|
-
|
|
|
- try {
|
|
|
- surveyDataLayer.value.setGeometries(filtered.map(feature => ({
|
|
|
- id: feature.properties.ID || feature.properties.OBJECTID,
|
|
|
- styleId: feature.properties.H_XTFX || feature.properties.h_xtfx || '其他',
|
|
|
- position: new TMap.value.LatLng(
|
|
|
- feature.geometry.coordinates[1],
|
|
|
- feature.geometry.coordinates[0]
|
|
|
- ),
|
|
|
- properties: {
|
|
|
- ...feature.properties,
|
|
|
- H_XTFX: feature.properties.H_XTFX || '其他'
|
|
|
- }
|
|
|
-
|
|
|
- })));
|
|
|
- console.log("🗺️ 图层更新成功");
|
|
|
- } catch (e) {
|
|
|
- console.error("[图层操作异常]", e);
|
|
|
- error.value = `地图更新失败: ${e.message}`;
|
|
|
- setTimeout(() => error.value = null, 5000);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-function initMapWithGeoJSON(geojsonData, map) {
|
|
|
- // 销毁旧图层
|
|
|
- if (geoJSONLayer) {
|
|
|
- geoJSONLayer.setMap(null);
|
|
|
- geoJSONLayer = null;
|
|
|
- }
|
|
|
- // 创建 GeoJSONLayer
|
|
|
- geoJSONLayer = new TMap.value.vector.GeoJSONLayer({
|
|
|
- map: map,
|
|
|
- data: geojsonData,
|
|
|
- zIndex: 10,
|
|
|
- polygonStyle: new TMap.value.PolygonStyle({ // 必须用 PolygonStyle 类实例
|
|
|
- color: 'rgba(255, 0, 0, 0.25)',
|
|
|
- showBorder: true,
|
|
|
- borderColor: '#FF0000',
|
|
|
- borderWidth: 2
|
|
|
- })
|
|
|
- });
|
|
|
-
|
|
|
- // 获取多边形覆盖层
|
|
|
- multiPolygon = geoJSONLayer.getGeometryOverlay('polygon');
|
|
|
-
|
|
|
- // 高亮选中图层
|
|
|
- const highlightLayer = new TMap.value.MultiPolygon({
|
|
|
- map,
|
|
|
- zIndex: 20,
|
|
|
- styles: {
|
|
|
- highlight: new TMap.value.PolygonStyle({ // 注意要改为 PolygonStyle
|
|
|
- color: 'rgba(0, 123, 255, 0.5)', // 半透明蓝色填充
|
|
|
- borderColor: '#00FF00', // 荧光绿边框
|
|
|
- borderWidth: 2, // 加粗边框
|
|
|
- showBorder: true,
|
|
|
- extrudeHeight: 15 // 3D拔起效果[1](@ref)
|
|
|
- })
|
|
|
- }});
|
|
|
- // 高亮区域
|
|
|
- let highlightGeometry = {
|
|
|
- id: 'highlightGeo',
|
|
|
- styleId: 'highlight'
|
|
|
- }
|
|
|
-
|
|
|
- // 绑定点击事件(替换原有的事件监听)
|
|
|
- multiPolygon.on('hover', (e) => {
|
|
|
- if (e.geometry) {
|
|
|
- // 鼠标选中时高亮区域覆盖
|
|
|
- highlightGeometry.paths = e.geometry.paths;
|
|
|
- highlightLayer.updateGeometries([highlightGeometry]);
|
|
|
- } else {
|
|
|
- // 鼠标移出时取消高亮区域覆盖
|
|
|
- highlightLayer.setGeometries([]);
|
|
|
- }
|
|
|
- })
|
|
|
-};
|
|
|
-
|
|
|
-
|
|
|
-// 加载调查数据并初始化图层
|
|
|
-const initSurveyDataLayer = async (map) => {
|
|
|
- try {
|
|
|
- isLoading.value = true
|
|
|
- const geoJsonFiles = [
|
|
|
- 'https://soilgd.com:8000/api/vector/export/all?table_name=surveydata',
|
|
|
- '/data/河池土壤样品.geojson',
|
|
|
- '/data/河池农产品样品.geojson',
|
|
|
- ];
|
|
|
-
|
|
|
- const surveyDataArray = await Promise.all(geoJsonFiles.map(loadGeoJSON));
|
|
|
- const features = surveyDataArray.flatMap(geoData => geoData.features);
|
|
|
-
|
|
|
- // 保存原始数据用于过滤
|
|
|
- combinedSurveyFeatures.value = features;
|
|
|
-
|
|
|
- // 合并颜色配置(添加默认分类)
|
|
|
- const mergedCategoryColors = {
|
|
|
- ...categoryColors,
|
|
|
- };
|
|
|
-
|
|
|
- // 创建样式(包含默认分类)
|
|
|
- const pointStyles = Object.keys(mergedCategoryColors).map(category => ({
|
|
|
- id: category,
|
|
|
- style: new TMap.value.MarkerStyle({
|
|
|
- width: 12,
|
|
|
- height: 12,
|
|
|
- anchor: { x: 6, y: 6 },
|
|
|
- src: createColoredCircle(mergedCategoryColors[category])
|
|
|
- })
|
|
|
- }));
|
|
|
-
|
|
|
- // 初始化图层(处理缺失属性)
|
|
|
- surveyDataLayer.value = new TMap.value.MultiMarker({
|
|
|
- map: map,
|
|
|
- styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
|
|
|
- geometries: combinedSurveyFeatures.value.map(feature => ({
|
|
|
- id: feature.properties.ID || feature.properties.OBJECTID,
|
|
|
- styleId: feature.properties.H_XTFX || feature.properties.h_xtfx || '其他', // 设置默认值
|
|
|
- position: new TMap.value.LatLng(
|
|
|
- feature.geometry.coordinates[1],
|
|
|
- feature.geometry.coordinates[0]
|
|
|
- ),
|
|
|
- properties: {
|
|
|
- ...feature.properties,
|
|
|
- // 强制添加H_XTFX字段保证数据一致性
|
|
|
- H_XTFX: feature.properties.H_XTFX|| '其他'
|
|
|
- }
|
|
|
- }))
|
|
|
- });
|
|
|
-
|
|
|
- // 添加点击事件
|
|
|
- surveyDataLayer.value.on('click', (event) => {
|
|
|
- const prop = event.geometry.properties;
|
|
|
- if (currentInfoWindow) currentInfoWindow.close();
|
|
|
- currentInfoWindow = new TMap.value.InfoWindow({
|
|
|
- map: map,
|
|
|
- position: event.geometry.position,
|
|
|
- content: `
|
|
|
- <div class="point-info">
|
|
|
- <h3>${prop.XMC} ${prop.ZMC} ${prop.CMC}</h3>
|
|
|
- <p>${prop.H_XTFX}</p>
|
|
|
- </div>
|
|
|
- `
|
|
|
- });
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- console.error("调查数据加载失败:", error);
|
|
|
- // 添加详细错误日志
|
|
|
- console.groupCollapsed("[错误详情]");
|
|
|
- console.error("错误对象:", error);
|
|
|
- console.trace("调用堆栈");
|
|
|
- console.groupEnd();
|
|
|
- } finally {
|
|
|
- isLoading.value = false
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 生成圆形图标的base64数据
|
|
|
-const createColoredCircle = (color) => {
|
|
|
- const canvas = document.createElement('canvas');
|
|
|
- canvas.width = 24;
|
|
|
- canvas.height = 24;
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
-
|
|
|
- // 绘制圆形
|
|
|
- ctx.beginPath();
|
|
|
- ctx.arc(12, 12, 8, 0, 2 * Math.PI);
|
|
|
- ctx.fillStyle = color;
|
|
|
- ctx.fill();
|
|
|
-
|
|
|
- // 添加白色边框
|
|
|
- ctx.strokeStyle = 'black';
|
|
|
- ctx.lineWidth = 2;
|
|
|
- ctx.stroke();
|
|
|
-
|
|
|
- return canvas.toDataURL();
|
|
|
-};
|
|
|
-
|
|
|
- const toggleSoilTypeLayer = () => {
|
|
|
- if (!multiPolygon) {
|
|
|
- console.error('利用类型图层未初始化');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (multiPolygon) {
|
|
|
- multiPolygon.setVisible(state.showSoilTypes);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const toggleSurveyDataLayer = () => {
|
|
|
- if (!surveyDataLayer) {
|
|
|
- console.error('调查数据图层未初始化');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (surveyDataLayer) {
|
|
|
- surveyDataLayer.value.setVisible(state.showSurveyData);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
-// // 切换覆盖层
|
|
|
-// const toggleOverlay = () => {
|
|
|
-// if (state.showOverlay) {
|
|
|
-// overlay = new TMap.value.ImageGroundLayer({
|
|
|
-// map: map,
|
|
|
-// bounds: new TMap.value.LatLngBounds(
|
|
|
-// new TMap.value.LatLng(18.17, 103.55),
|
|
|
-// new TMap.value.LatLng(32.32, 119.82)
|
|
|
-// ),
|
|
|
-// src: 'https://soilgd.com/images/farmland_cut.png'
|
|
|
-// })
|
|
|
-// } else {
|
|
|
-// if (overlay) {
|
|
|
-// overlay.setMap(null)
|
|
|
-// overlay = null
|
|
|
-// }
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- try {
|
|
|
- await loadSDK()
|
|
|
- initData()
|
|
|
- await initMap()
|
|
|
- await initBaseLayers()
|
|
|
- } catch (err) {
|
|
|
- error.value = err.message
|
|
|
- }
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- if (activeTempMarker.value) {
|
|
|
- manageTempMarker.remove()
|
|
|
- }
|
|
|
- if (markersLayer) markersLayer.setMap(null)
|
|
|
- if (overlay) overlay.setMap(null)
|
|
|
- if (infoWindow.value) {
|
|
|
- infoWindow.value.close()
|
|
|
- infoWindow.value = null
|
|
|
- }
|
|
|
- if (soilTypeLayer) {
|
|
|
- soilTypeLayer.destroy();
|
|
|
- soilTypeLayer = null;
|
|
|
- }
|
|
|
- if (surveyDataLayer.value) {
|
|
|
- surveyDataLayer.value.setMap(null);
|
|
|
- surveyDataLayer.value.destroy();
|
|
|
- }
|
|
|
- map.off('zoom', handleZoomChange)
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.basemap-toggle {
|
|
|
- margin-top: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.basemap-toggle button {
|
|
|
- padding: 8px 16px;
|
|
|
- background: #3876ff;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- border-radius: 20px;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.basemap-toggle button:hover {
|
|
|
- background: #2b5dc5;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-/* 图层过渡动画 */
|
|
|
-.tmap-geojson-layer {
|
|
|
- transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.map-toolbar {
|
|
|
- position: relative; /* 确保层级上下文 */
|
|
|
- z-index: 1000; /* 低于子组件下拉菜单的z-index */
|
|
|
- padding: 12px;
|
|
|
- background: rgba(255, 255, 255, 0.9);
|
|
|
- backdrop-filter: blur(5px);
|
|
|
-}
|
|
|
-
|
|
|
-.compact-region-selector {
|
|
|
- width: 800px;
|
|
|
- max-width: 100%;
|
|
|
-
|
|
|
- /* 重置可能影响子组件的样式 */
|
|
|
- .selection-container {
|
|
|
- gap: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- .select-group {
|
|
|
- min-width: 180px;
|
|
|
- }
|
|
|
-
|
|
|
- /* 移动端适配 */
|
|
|
- @media (max-width: 768px) {
|
|
|
- width: 100%;
|
|
|
-
|
|
|
- .selection-container {
|
|
|
- flex-wrap: wrap;
|
|
|
- }
|
|
|
-
|
|
|
- .select-group {
|
|
|
- flex: 1 1 30%;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.map-page {
|
|
|
- position: relative;
|
|
|
- width: 100vw;
|
|
|
- height: 100vh;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-}
|
|
|
-
|
|
|
-.map-container {
|
|
|
- flex: 1;
|
|
|
- height: calc(100vh - 48px); /* 对应新的工具栏高度 */
|
|
|
- position: relative;
|
|
|
- background: #f5f5f7;
|
|
|
-}
|
|
|
-.control-panel {
|
|
|
- position: fixed;
|
|
|
- top: 80px; /* 下移避开工具栏 */
|
|
|
- right: 24px;
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
- padding: 16px;
|
|
|
- border-radius: 12px;
|
|
|
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
- backdrop-filter: blur(8px);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
- z-index: 1000;
|
|
|
- min-width: 240px;
|
|
|
- transition: all 0.3s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.control-panel:hover {
|
|
|
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
|
|
- transform: translateY(-2px);
|
|
|
-}
|
|
|
-
|
|
|
-.control-panel label {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- padding: 8px 12px;
|
|
|
- border-radius: 8px;
|
|
|
- transition: background 0.2s ease;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.control-panel label:hover {
|
|
|
- background: rgba(56, 118, 255, 0.05);
|
|
|
-}
|
|
|
-
|
|
|
-.control-panel input[type="checkbox"] {
|
|
|
- width: 18px;
|
|
|
- height: 18px;
|
|
|
- border: 2px solid #3876ff;
|
|
|
- border-radius: 4px;
|
|
|
- appearance: none;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.control-panel input[type="checkbox"]:checked {
|
|
|
- background: #3876ff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center;
|
|
|
- background-size: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.export-controls {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
- margin-top: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.export-controls button {
|
|
|
- padding: 10px 16px;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
- border: none;
|
|
|
- border-radius: 8px;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s ease;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- background: #3876ff;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.export-controls button:disabled {
|
|
|
- background: #e0e0e0;
|
|
|
- color: #9e9e9e;
|
|
|
- cursor: not-allowed;
|
|
|
- opacity: 0.8;
|
|
|
-}
|
|
|
-
|
|
|
-.export-controls button:not(:disabled):hover {
|
|
|
- background: #2b5dc5;
|
|
|
- box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
|
|
|
-}
|
|
|
-
|
|
|
-/* 新增加载提示样式 */
|
|
|
-.loading-overlay {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- background: rgba(255, 255, 255, 0.8);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- z-index: 9999;
|
|
|
-}
|
|
|
-
|
|
|
-.loading-spinner {
|
|
|
- width: 50px;
|
|
|
- height: 50px;
|
|
|
- border: 5px solid #f3f3f3;
|
|
|
- border-top: 5px solid #3876ff;
|
|
|
- border-radius: 50%;
|
|
|
- animation: spin 1s linear infinite;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes spin {
|
|
|
- 0% { transform: rotate(0deg); }
|
|
|
- 100% { transform: rotate(360deg); }
|
|
|
-}
|
|
|
-
|
|
|
-/* 响应式调整 */
|
|
|
-@media (max-width: 768px) {
|
|
|
- .control-panel {
|
|
|
- top: 16px;
|
|
|
- right: 16px;
|
|
|
- left: 16px;
|
|
|
- width: auto;
|
|
|
- min-width: auto;
|
|
|
- }
|
|
|
-
|
|
|
- .export-controls {
|
|
|
- flex-direction: row;
|
|
|
- flex-wrap: wrap;
|
|
|
- }
|
|
|
-
|
|
|
- .export-controls button {
|
|
|
- flex: 1;
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/* 信息窗口样式 */
|
|
|
-:deep(.tmap-infowindow) {
|
|
|
- padding: 12px;
|
|
|
- min-width: 200px;
|
|
|
- border-radius: 8px;
|
|
|
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
|
- background-color: white;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.tmap-infowindow h3) {
|
|
|
- margin: 0 0 8px;
|
|
|
- font-size: 16px;
|
|
|
- color: #333;
|
|
|
-}
|
|
|
-
|
|
|
-:deep(.tmap-infowindow p) {
|
|
|
- margin: 4px 0;
|
|
|
- color: #666;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-.polygon-info {
|
|
|
- padding: 12px;
|
|
|
- max-width: 300px;
|
|
|
-
|
|
|
- h3 {
|
|
|
- margin: 0 0 8px;
|
|
|
- color: #333;
|
|
|
- font-size: 16px;
|
|
|
- }
|
|
|
-
|
|
|
- table {
|
|
|
- width: 100%;
|
|
|
- border-collapse: collapse;
|
|
|
-
|
|
|
- tr {
|
|
|
- border-bottom: 1px solid #eee;
|
|
|
- }
|
|
|
-
|
|
|
- th, td {
|
|
|
- padding: 6px 4px;
|
|
|
- text-align: left;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
-
|
|
|
- th {
|
|
|
- color: #666;
|
|
|
- white-space: nowrap;
|
|
|
- padding-right: 8px;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-.point-info {
|
|
|
- padding: 12px;
|
|
|
- min-width: 200px;
|
|
|
-
|
|
|
- h3 {
|
|
|
- margin: 0 0 8px;
|
|
|
- font-size: 14px;
|
|
|
- color: white;
|
|
|
- padding: 4px 8px;
|
|
|
- border-radius: 4px;
|
|
|
- display: inline-block;
|
|
|
- background: var(--category-color);
|
|
|
- }
|
|
|
-
|
|
|
- p {
|
|
|
- margin: 6px 0;
|
|
|
- font-size: 13px;
|
|
|
- line-height: 1.4;
|
|
|
-
|
|
|
- &:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/* 动态类别颜色 */
|
|
|
-.point-info h3[data-category="优先保护类"] { --category-color: #00C853; }
|
|
|
-.point-info h3[data-category="安全利用类"] { --category-color: #FFD600; }
|
|
|
-.point-info h3[data-category="严格管控类"] { --category-color: #D50000; }
|
|
|
-.point-info h3[data-category="其他"] { --category-color: #CCCCCC; }
|
|
|
-.point-info h3[data-category="土壤样品"] { --category-color: #2196F3; }
|
|
|
-.point-info h3[data-category="农产品样品"] { --category-color: #4CAF50; }
|
|
|
-.highlight-status {
|
|
|
- padding: 8px;
|
|
|
- background: rgba(0, 255, 0, 0.1);
|
|
|
- border-left: 3px solid #00FF00;
|
|
|
- margin-top: 12px;
|
|
|
-}
|
|
|
-.map-legend {
|
|
|
- position: fixed;
|
|
|
- left: 20px;
|
|
|
- bottom: 80px;
|
|
|
- z-index: 1000;
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 15px;
|
|
|
- backdrop-filter: blur(8px);
|
|
|
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
- transition: all 0.3s ease;
|
|
|
- width: 220px;
|
|
|
-}
|
|
|
-
|
|
|
-.map-legend.active {
|
|
|
- bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.legend-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.close-btn {
|
|
|
- background: none;
|
|
|
- border: none;
|
|
|
- font-size: 20px;
|
|
|
- cursor: pointer;
|
|
|
- color: #666;
|
|
|
-}
|
|
|
-
|
|
|
-.legend-section {
|
|
|
- max-height: 180px;
|
|
|
- overflow-y: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.legend-scale {
|
|
|
- display: flex;
|
|
|
- flex-direction: column; /* 改为纵向排列 */
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.scale-item {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 6px;
|
|
|
-}
|
|
|
-
|
|
|
-.color-box {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
- border-radius: 3px;
|
|
|
- border: 1px solid #ccc;
|
|
|
-}
|
|
|
-
|
|
|
-.legend-source {
|
|
|
- font-size: 12px;
|
|
|
- color: #666;
|
|
|
- line-height: 1.4;
|
|
|
-}
|
|
|
-
|
|
|
-/* 响应式调整 */
|
|
|
-@media (max-width: 768px) {
|
|
|
- .map-legend {
|
|
|
- width: 180px;
|
|
|
- bottom: 10px;
|
|
|
- }
|
|
|
-
|
|
|
- .scale-item {
|
|
|
- flex-direction: column;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
-}
|
|
|
-.legend-controls {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 16px;
|
|
|
- position: relative;
|
|
|
- z-index: 1001; /* 确保在图例面板之上 */
|
|
|
-}
|
|
|
-
|
|
|
-.legend-controls button {
|
|
|
- padding: 8px 16px;
|
|
|
- background: #3876ff;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- border-radius: 20px;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.legend-controls button:hover{
|
|
|
- background: #2b5dc5;
|
|
|
-}
|
|
|
-
|
|
|
-/* 响应式调整 */
|
|
|
-@media (max-width: 768px) {
|
|
|
- .legend-btn {
|
|
|
- padding: 8px 16px;
|
|
|
- font-size: 13px;
|
|
|
- border-radius: 20px;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|