farmlandQualityAssessment.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <template>
  2. <div>
  3. <!-- 顶部信息卡片区域 -->
  4. <div class="dashboard">
  5. <!-- 合并的统计与分布卡片 -->
  6. <div class="dashboard-card combined-card">
  7. <div class="card-title">统计与分布</div>
  8. <div class="combined-content">
  9. <!-- 左侧:单元统计 -->
  10. <div class="statistics-section">
  11. <h3>单元统计</h3>
  12. <div class="statistics">
  13. <div class="stat-item">
  14. <div class="stat-value">{{ statistics.total_units }}</div>
  15. <div class="stat-label">总单元数</div>
  16. </div>
  17. <div class="stat-item">
  18. <div class="stat-value">{{ statistics.units_with_data }}</div>
  19. <div class="stat-label">有数据单元</div>
  20. </div>
  21. <!-- 仅当无数据单元数不为0时显示 -->
  22. <div class="stat-item" v-if="statistics.units_without_data !== 0">
  23. <div class="stat-value">{{ statistics.units_without_data }}</div>
  24. <div class="stat-label">无数据单元</div>
  25. </div>
  26. </div>
  27. </div>
  28. <!-- 右侧:分类分布 -->
  29. <div class="distribution-section">
  30. <h3>分类分布</h3>
  31. <div class="distribution">
  32. <div v-for="(count, category) in statistics.category_distribution" :key="category"
  33. class="category-dist" :style="{ backgroundColor: categoryColors[category] }">
  34. <div class="dist-category">{{ category }}</div>
  35. <div class="dist-count">{{ count }}</div>
  36. </div>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. <!-- 饼图卡片 -->
  42. <div class="dashboard-card chart-card">
  43. <div class="card-title">点位分类分布</div>
  44. <div ref="pointPieChart" class="chart"></div>
  45. </div>
  46. </div>
  47. <!-- 地图区域 -->
  48. <div class="map-area">
  49. <div class="map-title">土壤分类分布地图</div>
  50. <div ref="mapContainer" class="map-container"></div>
  51. </div>
  52. </div>
  53. </template>
  54. <script>
  55. import * as echarts from 'echarts';
  56. import { api8000 } from '@/utils/request'; // 导入api8000
  57. // 分类颜色配置
  58. const categoryColors = {
  59. '优先保护类': 'rgba(255, 214, 0, 0.7)', // #FFD600 转换为RGBA
  60. '安全利用类': 'rgba(0, 200, 83, 0.7)', // #00C853 转换为RGBA
  61. '严格管控类': 'rgba(213, 0, 0, 0.7)' // #D50000 转换为RGBA
  62. };
  63. export default {
  64. name: 'CategoryMap',
  65. data() {
  66. return {
  67. map: null,
  68. geoJSONLayer: null,
  69. shaoguanBoundaryLayer:null,
  70. multiPolygon: null,
  71. categoryColors,
  72. statistics: {
  73. total_units: 0,
  74. units_with_data: 0,
  75. units_without_data: 0,
  76. category_distribution: {
  77. '优先保护类': 0,
  78. '安全利用类': 0,
  79. '严格管控类': 0
  80. },
  81. point_distribution: {
  82. '优先保护类': 0,
  83. '安全利用类': 0,
  84. '严格管控类': 0
  85. }
  86. },
  87. groupingData: [] // 存储接口数据
  88. };
  89. },
  90. async mounted() {
  91. // 获取分类数据
  92. await this.fetchGroupingData();
  93. // 初始化地图
  94. await this.initMap();
  95. // 初始化点位分布饼图
  96. this.initPointPieChart();
  97. },
  98. methods: {
  99. // 获取分类数据
  100. async fetchGroupingData() {
  101. try {
  102. const response = await api8000.get(`/api/unit-grouping/h_xtfx`);
  103. if (response.data.success) {
  104. this.groupingData = response.data.data;
  105. this.statistics = response.data.statistics;
  106. }
  107. } catch (error) {
  108. console.error('获取分类数据失败:', error);
  109. }
  110. },
  111. // 初始化地图
  112. async initMap() {
  113. // 加载TMap SDK
  114. const TMap = await this.loadSDK();
  115. // 创建地图实例
  116. this.map = new TMap.Map(this.$refs.mapContainer, {
  117. center: new TMap.LatLng(24.81088, 113.59762),
  118. zoom: 12,
  119. mapStyleId: 'style1'
  120. });
  121. // 加载GeoJSON数据
  122. const geojsonData = await this.loadGeoJSON('/data/单元格.geojson');
  123. // 初始化GeoJSON图层 - 传递TMap对象
  124. this.initMapWithGeoJSON(geojsonData, TMap);
  125. //加载并初始化韶关边界图层
  126. const shaoguanBoundaryGeojson = await this.fetchShaoguanBoundary();
  127. this.initShaoguanBoundaryLayer(shaoguanBoundaryGeojson,TMap);
  128. },
  129. // 加载SDK
  130. loadSDK() {
  131. return new Promise((resolve, reject) => {
  132. if (window.TMap) return resolve(window.TMap);
  133. const script = document.createElement('script');
  134. script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${import.meta.env.VITE_TMAP_KEY}&callback=initTMap`;
  135. window.initTMap = () => {
  136. if (!window.TMap) {
  137. reject(new Error('TMap SDK 加载失败'));
  138. return;
  139. }
  140. resolve(window.TMap);
  141. };
  142. script.onerror = (err) => {
  143. reject(new Error('加载地图SDK失败'));
  144. document.head.removeChild(script);
  145. };
  146. document.head.appendChild(script);
  147. });
  148. },
  149. // 新增:获取韶关市边界GeoJSON数据
  150. async fetchShaoguanBoundary() {
  151. try {
  152. // 替换为用户实际的韶关市边界接口地址
  153. const boundaryUrl = "http://localhost:8000/api/vector/boundary?table_name=counties&field_name=city_name&field_value=%E9%9F%B6%E5%85%B3%E5%B8%82";
  154. const response = await fetch(boundaryUrl);
  155. if (!response.ok) {
  156. throw new Error(`获取韶关市边界失败: ${response.statusText}`);
  157. }
  158. const boundaryGeoJSON = await response.json();
  159. // 验证GeoJSON格式(必须是FeatureCollection,geometry为Polygon/MultiPolygon)
  160. if (boundaryGeoJSON.type !== "FeatureCollection") {
  161. throw new Error("韶关市边界数据不是有效的FeatureCollection");
  162. }
  163. return boundaryGeoJSON;
  164. } catch (error) {
  165. console.error("韶关市边界数据加载失败:", error);
  166. return { type: "FeatureCollection", features: [] }; // 返回空数据避免地图崩溃
  167. }
  168. },
  169. // 新增:初始化韶关市边界图层
  170. initShaoguanBoundaryLayer(boundaryGeoJSON, TMap) {
  171. try {
  172. if (!boundaryGeoJSON.features.length) {
  173. console.warn("韶关市边界数据为空,不渲染边界");
  174. return;
  175. }
  176. const lightEarthYellow = "rgba(245, 222, 179, 0.4)";
  177. // 创建边界图层(独立于单元格图层)
  178. this.shaoguanBoundaryLayer = new TMap.vector.GeoJSONLayer({
  179. map: this.map, // 绑定到现有地图实例
  180. data: boundaryGeoJSON, // 边界GeoJSON数据
  181. // 边界样式配置:突出边框,透明填充(不遮挡下方单元格)
  182. polygonStyle: new TMap.PolygonStyle({
  183. color: lightEarthYellow, // 填充色:透明
  184. showBorder: true, // 显示边框
  185. borderColor: '#000000', // 边框颜色:蓝色(醒目)
  186. borderWidth: 3 // 边框宽度:3px(确保清晰)
  187. })
  188. });
  189. // 确保边界图层在最上层(覆盖单元格,不遮挡交互)
  190. this.shaoguanBoundaryLayer.setZIndex(1);
  191. } catch (error) {
  192. console.error("初始化韶关市边界图层失败:", error);
  193. }
  194. },
  195. // 加载GeoJSON数据
  196. async loadGeoJSON(url) {
  197. try {
  198. const response = await fetch(url);
  199. if (!response.ok) {
  200. throw new Error(`加载GeoJSON失败: ${response.statusText}`);
  201. }
  202. return await response.json();
  203. } catch (error) {
  204. console.error('加载GeoJSON数据失败:', error);
  205. return { type: 'FeatureCollection', features: [] };
  206. }
  207. },
  208. // 初始化GeoJSON图层 - 使用MultiPolygon的setStyles方法
  209. initMapWithGeoJSON(geojsonData, TMap) {
  210. try {
  211. // 创建分类映射表
  212. const categoryMap = {};
  213. this.groupingData.forEach(item => {
  214. categoryMap[item.OBJECTID] = item.h_xtfx;
  215. });
  216. // 处理GeoJSON特征
  217. geojsonData.features.forEach(feature => {
  218. const objectId = feature.properties.OBJECTID;
  219. const category = categoryMap[objectId];
  220. // 添加分类属性
  221. feature.properties.category = category;
  222. });
  223. // 检查TMap对象是否有效
  224. if (!TMap || !TMap.PolygonStyle) {
  225. throw new Error('TMap对象无效,缺少PolygonStyle');
  226. }
  227. // 创建GeoJSON图层
  228. this.geoJSONLayer = new TMap.vector.GeoJSONLayer({
  229. map: this.map,
  230. data: geojsonData,
  231. polygonStyle: new TMap.PolygonStyle({
  232. color: 'rgba(0,0,0,0)',
  233. showBorder: false
  234. })
  235. });
  236. // 获取多边形覆盖层
  237. this.multiPolygon = this.geoJSONLayer.getGeometryOverlay('polygon');
  238. this.multiPolygon.setMap(this.map);
  239. const polygons = this.multiPolygon.getGeometries();
  240. // 创建样式映射
  241. const styles = {};
  242. // 遍历所有多边形,为每个多边形设置样式和唯一ID
  243. // 遍历所有多边形,为每个多边形设置样式
  244. polygons.forEach((polygon) => {
  245. // 直接访问properties属性
  246. const properties = polygon.properties;
  247. const category = properties.category;
  248. // 使用多边形的id作为样式ID的键
  249. const styleId = `style_${polygon.id}`;
  250. // 根据分类设置颜色
  251. const color = category ? this.categoryColors[category] : '#CCCCCC';
  252. // 添加样式到映射表
  253. styles[styleId] = new TMap.PolygonStyle({
  254. color: color,
  255. showBorder: true,
  256. borderColor: '#000000',
  257. borderWidth: 2
  258. });
  259. // 关键修复:为每个多边形设置样式ID(正确方式)
  260. polygon.styleId = styleId; // 直接设置属性
  261. });
  262. // 使用setStyles方法一次性设置所有样式
  263. this.multiPolygon.setStyles(styles);
  264. // 更新几何体以应用新样式
  265. this.multiPolygon.updateGeometries(polygons);
  266. this.geoJSONLayer.setZIndex(100);
  267. } catch (error) {
  268. console.error('初始化GeoJSON图层失败:', error);
  269. }
  270. },
  271. // 初始化点位分布饼图
  272. initPointPieChart() {
  273. const chartDom = this.$refs.pointPieChart;
  274. if (!chartDom) return;
  275. const chart = echarts.init(chartDom);
  276. // 准备饼图数据
  277. const pieData = Object.entries(this.statistics.category_distribution).map(([name, value]) => ({
  278. name,
  279. value,
  280. itemStyle: { color: this.categoryColors[name] || '#CCCCCC' }
  281. }));
  282. const option = {
  283. title: {
  284. text: '点位分类分布',
  285. left: 'center',
  286. textStyle: {
  287. fontSize: 18,
  288. fontWeight: 'bold'
  289. }
  290. },
  291. tooltip: {
  292. trigger: 'item',
  293. formatter: '{b}: {c} ({d}%)'
  294. },
  295. legend: {
  296. orient: 'horizontal',
  297. bottom: 10,
  298. data: Object.keys(this.statistics.point_distribution)
  299. },
  300. series: [
  301. {
  302. name: '点位分布',
  303. type: 'pie',
  304. radius: ['40%', '70%'],
  305. center: ['50%', '45%'],
  306. avoidLabelOverlap: true,
  307. itemStyle: {
  308. borderRadius: 10,
  309. borderColor: '#fff',
  310. borderWidth: 2
  311. },
  312. label: {
  313. show: true,
  314. formatter: '{b}: {c}\n({d}%)',
  315. fontSize: 14
  316. },
  317. emphasis: {
  318. label: {
  319. show: true,
  320. fontSize: 16,
  321. fontWeight: 'bold'
  322. },
  323. itemStyle: {
  324. shadowBlur: 10,
  325. shadowOffsetX: 0,
  326. shadowColor: 'rgba(0, 0, 0, 0.5)'
  327. }
  328. },
  329. data: pieData
  330. }
  331. ]
  332. };
  333. chart.setOption(option);
  334. // 响应式调整
  335. window.addEventListener('resize', () => {
  336. chart.resize();
  337. });
  338. }
  339. }
  340. };
  341. </script>
  342. <style scoped>
  343. /* 顶部信息卡片区域 */
  344. .dashboard {
  345. display: flex;
  346. flex-wrap: wrap;
  347. gap: 20px;
  348. margin-bottom: 20px;
  349. }
  350. .dashboard-card {
  351. flex: 1;
  352. min-width: 280px;
  353. background: white;
  354. border-radius: 12px;
  355. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  356. padding: 20px;
  357. transition: transform 0.3s ease;
  358. }
  359. .dashboard-card:hover {
  360. transform: translateY(-3px);
  361. }
  362. .chart-card {
  363. min-width: 350px;
  364. display: flex;
  365. flex-direction: column;
  366. }
  367. .combined-card {
  368. min-width: 500px;
  369. }
  370. .card-title {
  371. font-size: 18px;
  372. font-weight: 600;
  373. color: #1a1a1a;
  374. padding-bottom: 12px;
  375. margin-bottom: 15px;
  376. border-bottom: 1px solid #eee;
  377. }
  378. /* 合并内容样式 */
  379. .combined-content {
  380. display: flex;
  381. gap: 20px;
  382. }
  383. .statistics-section, .distribution-section {
  384. flex: 1;
  385. }
  386. .statistics-section h3, .distribution-section h3 {
  387. font-size: 16px;
  388. font-weight: 600;
  389. margin-bottom: 15px;
  390. color: #409EFF;
  391. text-align: center;
  392. }
  393. /* 统计信息样式 */
  394. .statistics {
  395. display: flex;
  396. flex-direction: column;
  397. gap: 15px;
  398. }
  399. .stat-item {
  400. text-align: center;
  401. padding: 15px;
  402. background: #f8fafc;
  403. border-radius: 8px;
  404. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
  405. transition: all 0.3s ease;
  406. }
  407. .stat-item:hover {
  408. transform: translateY(-3px);
  409. box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
  410. }
  411. .stat-value {
  412. font-size: 26px;
  413. font-weight: 700;
  414. color: #409EFF;
  415. margin-bottom: 5px;
  416. }
  417. .stat-label {
  418. font-size: 14px;
  419. color: #666;
  420. }
  421. /* 分类分布样式 */
  422. .distribution {
  423. display: flex;
  424. flex-direction: column;
  425. gap: 12px;
  426. }
  427. .category-dist {
  428. padding: 15px;
  429. border-radius: 8px;
  430. color: white;
  431. display: flex;
  432. justify-content: space-between;
  433. align-items: center;
  434. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  435. transition: transform 0.2s ease;
  436. }
  437. .category-dist:hover {
  438. transform: scale(1.02);
  439. }
  440. .dist-category {
  441. font-size: 16px;
  442. font-weight: 600;
  443. }
  444. .dist-count {
  445. font-size: 24px;
  446. font-weight: bold;
  447. }
  448. /* 饼图样式 */
  449. .chart {
  450. height: 300px;
  451. width: 100%;
  452. margin-top: 10px;
  453. }
  454. /* 地图区域样式 */
  455. .map-area {
  456. margin-top: 20px;
  457. background: white;
  458. border-radius: 12px;
  459. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  460. overflow: hidden;
  461. }
  462. .map-title {
  463. font-size: 18px;
  464. font-weight: 600;
  465. padding: 15px 25px;
  466. background-color: #f8fafc;
  467. border-bottom: 1px solid #eee;
  468. }
  469. .map-container {
  470. width: 100%;
  471. height: 60vh; /* 调整为60vh,更紧凑 */
  472. min-height: 400px; /* 降低最小高度 */
  473. max-height: 700px; /* 添加最大高度限制 */
  474. }
  475. /* 响应式调整 */
  476. @media (max-width: 992px) {
  477. .dashboard {
  478. flex-direction: column;
  479. }
  480. .dashboard-card {
  481. width: 100%;
  482. }
  483. .combined-content {
  484. flex-direction: column;
  485. }
  486. .combined-card, .chart-card {
  487. min-width: 100%;
  488. }
  489. /* 移动设备上地图高度调整 */
  490. .map-container {
  491. height: 55vh;
  492. min-height: 350px;
  493. max-height: 600px;
  494. }
  495. }
  496. /* 小屏幕设备进一步调整 */
  497. @media (max-width: 768px) {
  498. .map-container {
  499. height: 50vh;
  500. min-height: 300px;
  501. max-height: 500px;
  502. }
  503. }
  504. </style>