tencentMapView.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. <template>
  2. <div class="map-page">
  3. <div ref="mapContainer"
  4. class="map-container"
  5. ></div>
  6. <div v-if="error" class="error">{{ error }}</div>
  7. <!-- 覆盖层控制 -->
  8. <!-- <div class="control-panel">
  9. <label>
  10. <input type="checkbox" v-model="state.showOverlay" @change="toggleOverlay" />
  11. 显示土壤类型覆盖
  12. </label>
  13. </div> -->
  14. <div class="control-panel">
  15. <label>
  16. <input type="checkbox" v-model="state.showSoilTypes" @change="toggleSoilTypeLayer" />
  17. 显示韶关市评估单元
  18. </label>
  19. <label>
  20. <input type="checkbox" v-model="state.showSurveyData" @change="toggleSurveyDataLayer" />
  21. 显示韶关市调查数据
  22. </label>
  23. <!-- 截图控制 -->
  24. <div class="export-controls">
  25. <button @click="exportMapImage" :disabled="!isMapReady">
  26. {{ isExporting ? '生成中...' : '导出截图' }}
  27. </button>
  28. </div>
  29. </div>
  30. </div>
  31. </template>
  32. <script setup>
  33. import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
  34. import html2canvas from 'html2canvas'
  35. const isExporting = ref(false)
  36. const isMapReady = ref(false)
  37. const exportSettings = reactive({
  38. quality: 0.9,
  39. showMapControls: false,
  40. showWatermark: true
  41. })
  42. const mapContainer = ref(null)
  43. const activeMarker = ref(null)
  44. const error = ref(null)
  45. let activeTempMarker = ref(null)
  46. let infoWindow = ref(null)
  47. let map = null
  48. let markersLayer = null
  49. let overlay = null
  50. const state = reactive({
  51. showOverlay: false,
  52. showSoilTypes: true,
  53. showSurveyData: true,
  54. excelData: [],
  55. lastTapTime: 0
  56. })
  57. let soilTypeLayer = null
  58. let geoJSONLayer;
  59. let currentInfoWindow = null;
  60. let surveyDataLayer = ref(null);
  61. let multiPolygon;
  62. const categoryColors = { // 分类颜色配置
  63. '优先保护类': '#00C853', // 绿色
  64. '安全利用类': '#FFD600', // 黄色
  65. '严格管控类': '#D50000' // 红色
  66. };
  67. const tMapConfig = reactive({
  68. key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
  69. geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
  70. })
  71. const loadSDK = () => {
  72. return new Promise((resolve, reject) => {
  73. if (window.TMap?.service?.Geocoder) {
  74. TMap.value = window.TMap
  75. return resolve(window.TMap)
  76. }
  77. const script = document.createElement('script')
  78. script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
  79. window.initTMap = () => {
  80. if (!window.TMap?.service?.Geocoder) {
  81. reject(new Error('地图SDK加载失败'))
  82. return
  83. }
  84. TMap.value = window.TMap
  85. resolve(window.TMap)
  86. }
  87. script.onerror = (err) => {
  88. reject(`地图资源加载失败: ${err.message}`)
  89. document.head.removeChild(script)
  90. }
  91. document.head.appendChild(script)
  92. })
  93. }
  94. // 初始化数据
  95. const initData = () => {
  96. state.excelData = [
  97. { 土壤编号: "土1", 地点: "广西武鸣", dust_emissions: 5.34, longitude: 106.476143, latitude: 23.891756 },
  98. { 土壤编号: "土2", 地点: "广西河池", dust_emissions: 3.96, longitude: 107.476143, latitude: 24.891756 },
  99. { 土壤编号: "土3", 地点: "海南澄迈老城镇罗驿村委会罗驿洋", dust_emissions: 4.56, longitude: 110.125, latitude: 19.901756 },
  100. { 土壤编号: "土4", 地点: "广东江门新会", dust_emissions: 4.0, longitude: 109.476143, latitude: 22.461756 },
  101. { 土壤编号: "土5", 地点: "广州增城Z6", dust_emissions: 4.77, longitude: 110.476143, latitude: 21.891756 },
  102. { 土壤编号: "土6", 地点: "广州增城Z8", dust_emissions: 4.59, longitude: 111.476143, latitude: 22.891756 },
  103. { 土壤编号: "土7", 地点: "湖南岳阳", dust_emissions: 5.14, longitude: 112.476143, latitude: 23.891756 },
  104. { 土壤编号: "土8", 地点: "广东韶关武江", dust_emissions: 5.07, longitude: 113.476143, latitude: 24.891756 },
  105. { 土壤编号: "土9", 地点: "海南临高头星村", dust_emissions: 4.12, longitude: 109.684993, latitude: 19.83774 },
  106. { 土壤编号: "土10", 地点: "海南临高周礼村", dust_emissions: 5.0, longitude: 109.710703, latitude: 19.89222 },
  107. { 土壤编号: "土11", 地点: "海南澄迈金江", dust_emissions: 4.6, longitude: 110.069537, latitude: 19.81189 },
  108. { 土壤编号: "土12", 地点: "海南临高南贤村", dust_emissions: 4.2, longitude: 109.768714, latitude: 19.874323 },
  109. { 土壤编号: "土13", 地点: "海南澄迈金江北让村", dust_emissions: 4.5, longitude: 110.096765, latitude: 19.814288 },
  110. { 土壤编号: "土14", 地点: "广西扶绥", dust_emissions: 4.71, longitude: 107.7717789, latitude: 22.5166902 },
  111. { 土壤编号: "土15", 地点: "广西江州", dust_emissions: 4.31, longitude: 107.56347787, latitude: 22.6022203 },
  112. { 土壤编号: "土16", 地点: "广西龙州", dust_emissions: 5.15, longitude: 106.7870847, latitude: 22.3496497 },
  113. { 土壤编号: "土17", 地点: "广西大新", dust_emissions: 4.71, longitude: 107.0230641, latitude: 22.5857946 },
  114. { 土壤编号: "土18", 地点: "湖南岳阳荣家湾", dust_emissions: 5.04, longitude: 113.059629, latitude: 29.267061 },
  115. { 土壤编号: "土19", 地点: "湖南长沙", dust_emissions: 5.08, longitude: 113.059629, latitude: 28.440613 },
  116. { 土壤编号: "土20", 地点: "浙江", dust_emissions: 4.8, longitude: 111.45527, latitude: 24.395235 },
  117. { 土壤编号: "土21", 地点: "云南陆良", dust_emissions: 4.67, longitude: 112.45527, latitude: 25.395235 },
  118. { 土壤编号: "土22", 地点: "南昌横龙镇南园组", dust_emissions: 4.8, longitude: 113.45527, latitude: 26.395235 },
  119. { 土壤编号: "土23", 地点: "南昌横龙枫塘南园", dust_emissions: 5.1, longitude: 114.45527, latitude: 27.395235 },
  120. { 土壤编号: "土24", 地点: "南昌横龙镇院塘村", dust_emissions: 4.27, longitude: 114.852, latitude: 27.3947 },
  121. { 土壤编号: "土25", 地点: "江西山庄乡秀水村黄田组", dust_emissions: 4.27, longitude: 114.852, latitude: 27.5247 },
  122. { 土壤编号: "土26", 地点: "贵州双星村", dust_emissions: 4.7, longitude: 106.852, latitude: 27.3147},
  123. { 土壤编号: "土27", 地点: "湖南永州八宝镇唐家州", dust_emissions: 4.57, longitude: 113.952, latitude: 26.08147 },
  124. { 土壤编号: "土28", 地点: "湖南永州金洞", dust_emissions: 5.3, longitude: 112.1564, latitude: 26.1685 },
  125. { 土壤编号: "土29", 地点: "祁阳县中国农业科学院红壤实验室", dust_emissions: 4.75, longitude: 111.4, latitude: 22.24 },
  126. { 土壤编号: "土30", 地点: "福建福州1", dust_emissions: 4.31, longitude: 112.4, latitude: 23.24 },
  127. { 土壤编号: "土31", 地点: "福建福州2", dust_emissions: 4.38, longitude: 113.4, latitude: 24.24 },
  128. { 土壤编号: "土32", 地点: "广东省韶关市南雄市下塅村", dust_emissions: 5.51, longitude: 114.4, latitude: 25.24 },
  129. { 土壤编号: "土33", 地点: "广东省韶关市南雄市河塘西216米", dust_emissions: 6.44, longitude: 114.28, latitude: 25.14 },
  130. { 土壤编号: "土34", 地点: "广东省韶关市南雄市上何屋西南500米", dust_emissions: 5.25, longitude: 114.15, latitude: 24.86 },
  131. { 土壤编号: "土35", 地点: "广东省南雄市雄州街道林屋", dust_emissions: 4.62333333333333, longitude: 114.23, latitude: 25.4 },
  132. { 土壤编号: "土36", 地点: "广东省台山都斛镇", dust_emissions: 3.0, longitude: 112.34, latitude: 27.31 },
  133. { 土壤编号: "土52", 地点: "湖南省长沙市浏阳市永安镇千鹭湖", dust_emissions: 4.72333333333333, longitude: 113.34, latitude: 28.31 },
  134. { 土壤编号: "土53", 地点: "湖南省长沙市浏阳市湖南农大实习基地", dust_emissions: 5.55333333333333, longitude: 113.83, latitude: 28.3 },
  135. { 土壤编号: "土54", 地点: "湖南省邵阳市罗市镇1", dust_emissions: 4.64, longitude: 110.35, latitude: 25.47 },
  136. { 土壤编号: "土55", 地点: "湖南省邵阳市罗市镇2", dust_emissions: 5.01333333333333, longitude: 111.35, latitude: 26.47 },
  137. { 土壤编号: "土56", 地点: "湖南省邵阳市罗市镇3", dust_emissions: 5.18, longitude: 112.35, latitude: 27.47 },
  138. { 土壤编号: "土57", 地点: "长沙县高桥镇的省农科院高桥科研基地1", dust_emissions: 5.1, longitude: 113.35, latitude: 28.47 },
  139. { 土壤编号: "土58", 地点: "长沙县高桥镇的省农科院高桥科研基地2", dust_emissions: 4.92, longitude: 113.35, latitude: 28.47 },
  140. { 土壤编号: "土59", 地点: "湖南省长沙市望城区桐林坳社区", dust_emissions: 3.0, longitude: 112.8, latitude: 28.37 },
  141. { 土壤编号: "土60", 地点: "湖南省益阳市赫山区泥江口镇", dust_emissions: 3.0, longitude: 107.37, latitude: 21.92 },
  142. { 土壤编号: "土70", 地点: "南宁市兴宁区柳杨路26号", dust_emissions: 3.0, longitude: 108.37, latitude: 22.92 },
  143. { 土壤编号: "土71", 地点: "南宁市兴宁区柳杨路广西私享家家具用品", dust_emissions: 3.0, longitude: 108.37, latitude: 23.94 },
  144. { 土壤编号: "土72", 地点: "南宁市兴宁区004乡道", dust_emissions: 6.24666666666667, longitude: 108.39, latitude: 24.92 },
  145. { 土壤编号: "土73", 地点: "南宁市兴宁区G7201南宁绕城高速", dust_emissions: 3.0, longitude: 108.4, latitude: 25.94 },
  146. { 土壤编号: "土74", 地点: "南宁市兴宁区012县道", dust_emissions: 3.0, longitude: 108.41, latitude: 26.92 },
  147. { 土壤编号: "土75", 地点: "南宁市兴宁区那况路168号", dust_emissions: 3.0, longitude: 108.4, latitude: 27.9 },
  148. { 土壤编号: "土76", 地点: "南宁市西乡塘区翊武路", dust_emissions: 5.37, longitude: 108.35, latitude: 28.96 },
  149. { 土壤编号: "土77", 地点: "南宁市西乡塘区坛洛镇", dust_emissions: 3.0, longitude: 107.85, latitude: 29.92 },
  150. { 土壤编号: "土81", 地点: "铜仁职业技术学院", dust_emissions: 4.0, longitude: 108.85, latitude: 27.34 },
  151. { 土壤编号: "土87", 地点: "江西省红壤及种质资源研究所(进贤基地)1", dust_emissions: 4.55, longitude: 116.17, latitude: 28.34 },
  152. { 土壤编号: "土88", 地点: "江西省红壤及种质资源研究所(进贤基地)2", dust_emissions: 4.99333333333333, longitude: 116.17, latitude: 28.34 }
  153. ].map(item => {
  154. const lat = Number(item.latitude);
  155. const lng = Number(item.longitude);
  156. if (isNaN(lat) || isNaN(lng)) {
  157. console.error('无效的经纬度数据:', item);
  158. return null;
  159. }
  160. return {
  161. ...item,
  162. latitude: lat,
  163. longitude: lng
  164. };
  165. }).filter(item => item !== null);
  166. }
  167. // 初始化地图
  168. const initMap = async () => {
  169. try {
  170. await loadSDK()
  171. map = new TMap.value.Map(mapContainer.value, {
  172. center: new TMap.value.LatLng(24.81088,113.59762),
  173. zoom: 12,
  174. renderOptions: {
  175. preserveDrawingBuffer: true, // 必须开启以支持截图
  176. antialias: true
  177. }
  178. })
  179. // const defaultStyle = new TMap.value.MarkerStyle({
  180. // width: 34,
  181. // height: 34,
  182. // anchor: { x: 17, y: 34 },
  183. // src: markerIcon
  184. // })
  185. // markersLayer = new TMap.value.MultiMarker({
  186. // map: map,
  187. // styles: { default: defaultStyle }
  188. // })
  189. const geojsonData = await loadGeoJSON('/data/单元格.geojson');
  190. initMapWithGeoJSON(geojsonData, map);
  191. await initSurveyDataLayer(map);
  192. // 绑定点击事件
  193. // map.on('click', handleMapClick)
  194. // markersLayer.on('click', handleMarkerClick)
  195. // 新增地图就绪状态监听
  196. map.on('idle', () => {
  197. isMapReady.value = true
  198. })
  199. loadData()
  200. updateMarkers()
  201. } catch (err) {
  202. error.value = err.message
  203. }
  204. }
  205. // 加载数据并创建标记
  206. const loadData = () => {
  207. const geometries = state.excelData.map(item => ({
  208. id: item.土壤编号,
  209. styleId: 'default',
  210. position: new TMap.value.LatLng(item.latitude, item.longitude),
  211. properties: {
  212. title: item.地点,
  213. phValue: parseFloat(item.dust_emissions).toFixed(2),
  214. isTemp: false
  215. },
  216. }))
  217. markersLayer.setGeometries(geometries)
  218. }
  219. // 新增截图方法
  220. const exportMapImage = async () => {
  221. try {
  222. isExporting.value = true
  223. // 等待地图稳定
  224. await new Promise(resolve => setTimeout(resolve, 300))
  225. const canvas = await html2canvas(mapContainer.value, {
  226. useCORS: true,
  227. scale: window.devicePixelRatio || 2,
  228. backgroundColor: null,
  229. logging: true,
  230. onclone: (clonedDoc) => {
  231. // 处理控件可见性
  232. clonedDoc.querySelectorAll('.tmap-control').forEach(control => {
  233. control.style.visibility = exportSettings.showMapControls ? 'visible' : 'hidden'
  234. })
  235. // 添加水印
  236. if(exportSettings.showWatermark){
  237. const watermark = document.createElement('div')
  238. watermark.style = `
  239. position: absolute;
  240. bottom: 20px;
  241. right: 20px;
  242. color: rgba(0,0,0,0.2);
  243. font-size: 24px;
  244. transform: rotate(-15deg);
  245. z-index: 9999;
  246. `
  247. watermark.textContent = '机密地图 - 禁止外传'
  248. clonedDoc.body.appendChild(watermark)
  249. }
  250. }
  251. })
  252. // 转换为Blob
  253. canvas.toBlob(blob => {
  254. const link = document.createElement('a')
  255. link.download = `土壤地图_${new Date().toISOString().slice(0,10)}.png`
  256. link.href = URL.createObjectURL(blob)
  257. link.click()
  258. URL.revokeObjectURL(link.href)
  259. }, 'image/png', exportSettings.quality)
  260. } catch (error) {
  261. console.error('截图失败:', error)
  262. error.value = '截图失败,请尝试缩小地图层级'
  263. setTimeout(() => error.value = null, 3000)
  264. } finally {
  265. isExporting.value = false
  266. }
  267. }
  268. // 更新标记
  269. const updateMarkers = () => {
  270. const markers = state.excelData.map((item, index) => ({
  271. id: `marker-${index + 1}`,
  272. styleId: 'default',
  273. position: new TMap.value.LatLng(item.latitude, item.longitude),
  274. properties: {
  275. title: item.地点,
  276. phValue: item.dust_emissions,
  277. isTemp: false
  278. },
  279. }))
  280. markersLayer.setGeometries(markers)
  281. }
  282. // 新增Marker点击事件处理
  283. const handleMarkerClick = (e) => {
  284. const marker = e.geometry
  285. if (!marker) return
  286. // 关闭之前的信息窗口
  287. if (activeMarker.value?.id === marker.id) {
  288. infoWindow.close()
  289. activeMarker.value = null
  290. return
  291. }
  292. // 创建信息窗口内容
  293. const content = `
  294. <div style="padding:12px">
  295. <h3>${marker.properties.title}</h3>
  296. <p>PH值: ${marker.properties.phValue}</p>
  297. </div>
  298. `
  299. // 打开信息窗口
  300. infoWindow = new TMap.value.InfoWindow({
  301. map: map,
  302. position: marker.position,
  303. content: content,
  304. offset: {x: 0, y: -32}
  305. })
  306. // 记录当前激活的Marker
  307. activeMarker.value = marker
  308. // 点击其他区域关闭窗口
  309. map.on('click', closeInfoWindow)
  310. }
  311. const manageTempMarker = {
  312. add: (lat, lng, phValue) => {
  313. if (activeTempMarker.value) {
  314. markersLayer.remove("-999")
  315. }
  316. const tempMarker = markersLayer.add({
  317. id: "-999",
  318. position: new TMap.value.LatLng(lat, lng),
  319. styleId: 'temp',
  320. properties: {
  321. title: '克里金插值',
  322. phValue: parseFloat(phValue).toFixed(2),
  323. isTemp: true
  324. }
  325. })
  326. activeTempMarker.value = tempMarker
  327. },
  328. remove: () => {
  329. if (activeTempMarker.value) {
  330. markersLayer.remove("-999")
  331. activeTempMarker.value = null
  332. }
  333. }
  334. }
  335. // const handleMapClick = async (e) => {
  336. // if (selectedPolygon.value) {
  337. // resetPolygonStyle();
  338. // infoWindow.value?.close();
  339. // }
  340. // const now = Date.now()
  341. // if (now - state.lastTapTime < 1000) return
  342. // state.lastTapTime = now
  343. // try {
  344. // const latLng = e?.latLng
  345. // if (!latLng) throw new Error("地图点击事件缺少坐标信息")
  346. // const lat = Number(latLng.lat)
  347. // const lng = Number(latLng.lng)
  348. // if (!isValidCoordinate(lat, lng)) throw new Error(`非法坐标值 (${lat}, ${lng})`)
  349. // console.log('有效坐标:', lat, lng)
  350. // const result = await reverseGeocode(lat, lng)
  351. // if (!validateLocation(result)) throw new Error('非有效陆地区域')
  352. // const phValue = await getPhValue(lng, lat)
  353. // // 使用封装方法添加临时标记
  354. // manageTempMarker.add(lat, lng, phValue)
  355. // if (infoWindow.value) {
  356. // infoWindow.value.close()
  357. // }
  358. // infoWindow.value = new TMap.value.InfoWindow({
  359. // map: map,
  360. // position: manageTempMarker.activeTempMarker.value.getPosition(),
  361. // content: `
  362. // <div style="padding:12px">
  363. // <h3>${manageTempMarker.activeTempMarker.value.properties.title}</h3>
  364. // <p>PH值: ${manageTempMarker.activeTempMarker.value.properties.phValue}</p>
  365. // </div>
  366. // `
  367. // })
  368. // infoWindow.value.open()
  369. // } catch (error) {
  370. // console.error('操作失败详情:', error)
  371. // error.value = error.message.includes('非法坐标')
  372. // ? '请点击有效地图区域'
  373. // : '服务暂时不可用,请稍后重试'
  374. // setTimeout(() => error.value = null, 3000)
  375. // }
  376. // }
  377. // // 关闭信息窗口时同步移除临时标记
  378. // const closeInfoWindow = () => {
  379. // if (activeTempMarker.value) {
  380. // manageTempMarker.remove()
  381. // }
  382. // if (infoWindow.value) {
  383. // infoWindow.value.close()
  384. // infoWindow.value = null
  385. // }
  386. // map.off('click', closeInfoWindow)
  387. // }
  388. // // 验证坐标有效性
  389. // const isValidCoordinate = (lat, lng) => {
  390. // return !isNaN(lat) && !isNaN(lng) &&
  391. // lat >= -90 && lat <= 90 &&
  392. // lng >= -180 && lng <= 180
  393. // }
  394. // // 逆地理编码
  395. // const reverseGeocode = (lat, lng) => {
  396. // return new Promise((resolve, reject) => {
  397. // const callbackName = `tmap_callback_${Date.now()}`
  398. // window[callbackName] = (response) => {
  399. // delete window[callbackName]
  400. // document.body.removeChild(script)
  401. // if (response.status !== 0) reject(response.message)
  402. // else resolve(response.result)
  403. // }
  404. // const script = document.createElement('script')
  405. // script.src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${tMapConfig.key}&output=jsonp&callback=${callbackName}`
  406. // script.onerror = reject
  407. // document.body.appendChild(script)
  408. // })
  409. // }
  410. // // 验证地理位置
  411. // const validateLocation = (result) => {
  412. // if (!result || !result.address_component) {
  413. // return false;
  414. // }
  415. // return result.address_component.nation === '中国' &&
  416. // !['香港特别行政区', '澳门特别行政区', '台湾省'].includes(
  417. // result.address_component.province
  418. // )
  419. // }
  420. // // 获取PH值
  421. // const getPhValue = async (lng, lat) => {
  422. // try {
  423. // const { data } = await axios.post('https://soilgd.com:5000/kriging_interpolation', {
  424. // file_name: 'emissions.xlsx',
  425. // emission_column: 'dust_emissions',
  426. // points: [[lng, lat]]
  427. // })
  428. // return parseFloat(data.interpolated_concentrations[0]).toFixed(2)
  429. // } catch (error) {
  430. // console.error('获取PH值失败:', error)
  431. // throw error
  432. // }
  433. // }
  434. async function loadGeoJSON(url) {
  435. const response = await fetch(url);
  436. return await response.json();
  437. }
  438. function initMapWithGeoJSON(geojsonData, map) {
  439. // 销毁旧图层
  440. if (geoJSONLayer) {
  441. geoJSONLayer.setMap(null);
  442. geoJSONLayer = null;
  443. }
  444. // 创建 GeoJSONLayer
  445. geoJSONLayer = new TMap.value.vector.GeoJSONLayer({
  446. map: map,
  447. data: geojsonData,
  448. zIndex: 1,
  449. polygonStyle: new TMap.value.PolygonStyle({ // 必须用 PolygonStyle 类实例
  450. color: 'rgba(255, 0, 0, 0.25)',
  451. showBorder: true,
  452. borderColor: '#FF0000',
  453. borderWidth: 2
  454. })
  455. });
  456. // 获取多边形覆盖层
  457. multiPolygon = geoJSONLayer.getGeometryOverlay('polygon');
  458. // 高亮选中图层
  459. const highlightLayer = new TMap.value.MultiPolygon({
  460. map,
  461. zIndex: 2,
  462. styles: {
  463. highlight: new TMap.value.PolygonStyle({ // 注意要改为 PolygonStyle
  464. color: 'rgba(0, 123, 255, 0.5)', // 半透明蓝色填充
  465. borderColor: '#00FF00', // 荧光绿边框
  466. borderWidth: 2, // 加粗边框
  467. showBorder: true,
  468. extrudeHeight: 15 // 3D拔起效果[1](@ref)
  469. })
  470. }});
  471. // 高亮区域
  472. let highlightGeometry = {
  473. id: 'highlightGeo',
  474. styleId: 'highlight'
  475. }
  476. // 绑定点击事件(替换原有的事件监听)
  477. multiPolygon.on('hover', (e) => {
  478. if (e.geometry) {
  479. // 鼠标选中时高亮区域覆盖
  480. highlightGeometry.paths = e.geometry.paths;
  481. highlightLayer.updateGeometries([highlightGeometry]);
  482. } else {
  483. // 鼠标移出时取消高亮区域覆盖
  484. highlightLayer.setGeometries([]);
  485. }
  486. })
  487. };
  488. // 加载调查数据并初始化图层
  489. const initSurveyDataLayer = async (map) => {
  490. try {
  491. // 加载GeoJSON数据
  492. const surveyData = await loadGeoJSON('/data/调查数据.geojson');
  493. // 创建分类样式
  494. const pointStyles = Object.keys(categoryColors).map(category => ({
  495. id: category,
  496. style: new TMap.value.MarkerStyle({
  497. width: 12,
  498. height: 12,
  499. anchor: { x: 6, y: 6 },
  500. src: createColoredCircle(categoryColors[category]) // 生成圆形图标
  501. })
  502. }));
  503. // 初始化图层
  504. surveyDataLayer = new TMap.value.MultiMarker({
  505. map: map,
  506. styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
  507. geometries: surveyData.features.map(feature => ({
  508. id: feature.properties.ID,
  509. styleId: feature.properties.H_XTFX,
  510. position: new TMap.value.LatLng(
  511. feature.geometry.coordinates[1],
  512. feature.geometry.coordinates[0]
  513. ),
  514. properties: {
  515. ...feature.properties,
  516. }
  517. }))
  518. });
  519. // 添加点击事件
  520. surveyDataLayer.on('click', (event) => {
  521. const prop = event.geometry.properties;
  522. if (currentInfoWindow) currentInfoWindow.close();
  523. currentInfoWindow = new TMap.value.InfoWindow({
  524. map: map,
  525. position: event.geometry.position,
  526. content: `
  527. <div class="point-info">
  528. <h3>${prop.XMC} ${prop.ZMC} ${prop.CMC}</h3>
  529. <p>${prop.H_XTFX}</p>
  530. </div>
  531. `
  532. });
  533. });
  534. } catch (error) {
  535. console.error('调查数据加载失败:', error);
  536. }
  537. };
  538. // 生成圆形图标的base64数据
  539. const createColoredCircle = (color) => {
  540. const canvas = document.createElement('canvas');
  541. canvas.width = 24;
  542. canvas.height = 24;
  543. const ctx = canvas.getContext('2d');
  544. // 绘制圆形
  545. ctx.beginPath();
  546. ctx.arc(12, 12, 8, 0, 2 * Math.PI);
  547. ctx.fillStyle = color;
  548. ctx.fill();
  549. // 添加白色边框
  550. ctx.strokeStyle = 'black';
  551. ctx.lineWidth = 2;
  552. ctx.stroke();
  553. return canvas.toDataURL();
  554. };
  555. const toggleSoilTypeLayer = () => {
  556. if (!multiPolygon) {
  557. console.error('利用类型图层未初始化');
  558. return;
  559. }
  560. if (multiPolygon) {
  561. multiPolygon.setVisible(state.showSoilTypes);
  562. }
  563. };
  564. const toggleSurveyDataLayer = () => {
  565. if (!surveyDataLayer) {
  566. console.error('调查数据图层未初始化');
  567. return;
  568. }
  569. if (surveyDataLayer) {
  570. surveyDataLayer.setVisible(state.showSurveyData);
  571. }
  572. };
  573. // // 切换覆盖层
  574. // const toggleOverlay = () => {
  575. // if (state.showOverlay) {
  576. // overlay = new TMap.value.ImageGroundLayer({
  577. // map: map,
  578. // bounds: new TMap.value.LatLngBounds(
  579. // new TMap.value.LatLng(18.17, 103.55),
  580. // new TMap.value.LatLng(32.32, 119.82)
  581. // ),
  582. // src: 'https://soilgd.com/images/farmland_cut.png'
  583. // })
  584. // } else {
  585. // if (overlay) {
  586. // overlay.setMap(null)
  587. // overlay = null
  588. // }
  589. // }
  590. // }
  591. onMounted(async () => {
  592. try {
  593. await loadSDK()
  594. initData()
  595. await initMap()
  596. } catch (err) {
  597. error.value = err.message
  598. }
  599. })
  600. onBeforeUnmount(() => {
  601. if (activeTempMarker.value) {
  602. manageTempMarker.remove()
  603. }
  604. if (markersLayer) markersLayer.setMap(null)
  605. if (overlay) overlay.setMap(null)
  606. if (infoWindow.value) {
  607. infoWindow.value.close()
  608. infoWindow.value = null
  609. }
  610. if (farmlandLayer) {
  611. farmlandLayer.destroy();
  612. farmlandLayer = null;
  613. }
  614. if (bboxLayer) {
  615. bboxLayer.destroy();
  616. bboxLayer = null;
  617. }
  618. if (soilTypeLayer) {
  619. soilTypeLayer.destroy();
  620. soilTypeLayer = null;
  621. }
  622. if (surveyDataLayer) {
  623. surveyDataLayer.destroy();
  624. surveyDataLayer = null;
  625. }
  626. })
  627. </script>
  628. <style scoped>
  629. .map-page {
  630. position: relative;
  631. width: 100vw;
  632. height: 100vh;
  633. }
  634. .map-container {
  635. width: 100%;
  636. height: 100vh !important;
  637. min-height: 600px;
  638. }
  639. .control-panel {
  640. position: fixed;
  641. top: 24px;
  642. right: 24px;
  643. background: rgba(255, 255, 255, 0.95);
  644. padding: 16px;
  645. border-radius: 12px;
  646. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  647. backdrop-filter: blur(8px);
  648. border: 1px solid rgba(255, 255, 255, 0.2);
  649. z-index: 1000;
  650. min-width: 240px;
  651. transition: all 0.3s ease;
  652. }
  653. .control-panel:hover {
  654. box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  655. transform: translateY(-2px);
  656. }
  657. .control-panel label {
  658. display: flex;
  659. align-items: center;
  660. gap: 8px;
  661. padding: 8px 12px;
  662. border-radius: 8px;
  663. transition: background 0.2s ease;
  664. cursor: pointer;
  665. }
  666. .control-panel label:hover {
  667. background: rgba(56, 118, 255, 0.05);
  668. }
  669. .control-panel input[type="checkbox"] {
  670. width: 18px;
  671. height: 18px;
  672. border: 2px solid #3876ff;
  673. border-radius: 4px;
  674. appearance: none;
  675. cursor: pointer;
  676. transition: all 0.2s ease;
  677. }
  678. .control-panel input[type="checkbox"]:checked {
  679. 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;
  680. background-size: 12px;
  681. }
  682. .export-controls {
  683. display: flex;
  684. flex-direction: column;
  685. gap: 12px;
  686. margin-top: 16px;
  687. }
  688. .export-controls button {
  689. padding: 10px 16px;
  690. font-size: 14px;
  691. font-weight: 500;
  692. border: none;
  693. border-radius: 8px;
  694. cursor: pointer;
  695. transition: all 0.2s ease;
  696. display: flex;
  697. align-items: center;
  698. gap: 8px;
  699. background: #3876ff;
  700. color: white;
  701. }
  702. .export-controls button:disabled {
  703. background: #e0e0e0;
  704. color: #9e9e9e;
  705. cursor: not-allowed;
  706. opacity: 0.8;
  707. }
  708. .export-controls button:not(:disabled):hover {
  709. background: #2b5dc5;
  710. box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
  711. }
  712. /* 新增加载动画 */
  713. @keyframes spin {
  714. 0% { transform: rotate(0deg); }
  715. 100% { transform: rotate(360deg); }
  716. }
  717. .loading-spinner {
  718. width: 18px;
  719. height: 18px;
  720. border: 2px solid rgba(255, 255, 255, 0.3);
  721. border-top-color: white;
  722. border-radius: 50%;
  723. animation: spin 0.8s linear infinite;
  724. }
  725. /* 响应式调整 */
  726. @media (max-width: 768px) {
  727. .control-panel {
  728. top: 16px;
  729. right: 16px;
  730. left: 16px;
  731. width: auto;
  732. min-width: auto;
  733. }
  734. .export-controls {
  735. flex-direction: row;
  736. flex-wrap: wrap;
  737. }
  738. .export-controls button {
  739. flex: 1;
  740. justify-content: center;
  741. }
  742. }
  743. /* 信息窗口样式 */
  744. :deep(.tmap-infowindow) {
  745. padding: 12px;
  746. min-width: 200px;
  747. border-radius: 8px;
  748. box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  749. background-color: white;
  750. }
  751. :deep(.tmap-infowindow h3) {
  752. margin: 0 0 8px;
  753. font-size: 16px;
  754. color: #333;
  755. }
  756. :deep(.tmap-infowindow p) {
  757. margin: 4px 0;
  758. color: #666;
  759. font-size: 14px;
  760. }
  761. .polygon-info {
  762. padding: 12px;
  763. max-width: 300px;
  764. h3 {
  765. margin: 0 0 8px;
  766. color: #333;
  767. font-size: 16px;
  768. }
  769. table {
  770. width: 100%;
  771. border-collapse: collapse;
  772. tr {
  773. border-bottom: 1px solid #eee;
  774. }
  775. th, td {
  776. padding: 6px 4px;
  777. text-align: left;
  778. font-size: 14px;
  779. }
  780. th {
  781. color: #666;
  782. white-space: nowrap;
  783. padding-right: 8px;
  784. }
  785. }
  786. }
  787. .point-info {
  788. padding: 12px;
  789. min-width: 200px;
  790. h3 {
  791. margin: 0 0 8px;
  792. font-size: 14px;
  793. color: white;
  794. padding: 4px 8px;
  795. border-radius: 4px;
  796. display: inline-block;
  797. background: var(--category-color);
  798. }
  799. p {
  800. margin: 6px 0;
  801. font-size: 13px;
  802. line-height: 1.4;
  803. &:last-child {
  804. margin-bottom: 0;
  805. }
  806. }
  807. }
  808. /* 动态类别颜色 */
  809. .point-info h3[data-category="优先保护类"] { --category-color: #00C853; }
  810. .point-info h3[data-category="安全利用类"] { --category-color: #FFD600; }
  811. .point-info h3[data-category="严格管控类"] { --category-color: #D50000; }
  812. .highlight-status {
  813. padding: 8px;
  814. background: rgba(0, 255, 0, 0.1);
  815. border-left: 3px solid #00FF00;
  816. margin-top: 12px;
  817. }
  818. </style>