feat(dashboard): 添加首页地图展示功能
- 在 Dashboard 组件中集成锡林郭勒盟区域地图 - 实现地图数据接口和区域详情接口 - 添加地图交互功能,支持点击和悬停事件 - 更新开发计划和需求文档,增加地图展示功能
This commit is contained in:
2
frontend/dashboard/node_modules/.vite/deps/echarts.js
generated
vendored
2
frontend/dashboard/node_modules/.vite/deps/echarts.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
__export
|
||||
} from "./chunk-2GTGKKMZ.js";
|
||||
} from "./chunk-5WWUZCGV.js";
|
||||
|
||||
// node_modules/tslib/tslib.es6.js
|
||||
var extendStatics = function(d, b) {
|
||||
|
||||
247
frontend/dashboard/src/components/map/RegionDetail.vue
Normal file
247
frontend/dashboard/src/components/map/RegionDetail.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="region-detail-overlay" v-if="visible" @click="closeOverlay">
|
||||
<div class="region-detail-card" @click.stop>
|
||||
<div class="card-header">
|
||||
<h2>{{ regionData.region?.name }} 详情</h2>
|
||||
<button class="close-button" @click="closeOverlay">×</button>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="region-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">牛只数量</div>
|
||||
<div class="stat-value">{{ regionData.region?.cattle_count || 0 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">牧场数量</div>
|
||||
<div class="stat-value">{{ regionData.region?.farm_count || 0 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">产值</div>
|
||||
<div class="stat-value">¥{{ formatNumber(regionData.region?.output_value || 0) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">趋势</div>
|
||||
<div class="stat-value" :class="regionData.region?.trend">
|
||||
{{ regionData.region?.trend === 'up' ? '↑ 上升' : '↓ 下降' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="farms-section" v-if="regionData.farms && regionData.farms.length > 0">
|
||||
<h3>牧场列表</h3>
|
||||
<div class="farms-list">
|
||||
<div
|
||||
class="farm-item"
|
||||
v-for="farm in regionData.farms"
|
||||
:key="farm.id"
|
||||
>
|
||||
<div class="farm-name">{{ farm.name }}</div>
|
||||
<div class="farm-details">
|
||||
<span>牛只: {{ farm.cattle_count }}</span>
|
||||
<span>产值: ¥{{ formatNumber(farm.output_value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-farms" v-else>
|
||||
<p>暂无牧场数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'RegionDetail',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
regionData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const closeOverlay = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(2) + '亿';
|
||||
}
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(2) + '万';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
return {
|
||||
closeOverlay,
|
||||
formatNumber
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-detail-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.region-detail-card {
|
||||
background: linear-gradient(135deg, #0f2027, #20555d);
|
||||
border-radius: 12px;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(90vh - 100px);
|
||||
}
|
||||
|
||||
.region-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-value.up {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-value.down {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.farms-section h3 {
|
||||
color: #4CAF50;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.farms-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.farm-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.farm-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.farm-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.no-farms {
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.region-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.farm-details {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,18 +5,27 @@
|
||||
class="map-canvas"
|
||||
:style="{ width: '100%', height: height + 'px' }"
|
||||
></div>
|
||||
|
||||
<RegionDetail
|
||||
:visible="showRegionDetail"
|
||||
:region-data="selectedRegionData"
|
||||
@close="closeRegionDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import * as turf from '@turf/turf';
|
||||
import RegionDetail from './RegionDetail.vue';
|
||||
import { fetchRegionDetail } from '@/services/dashboard.js';
|
||||
|
||||
export default {
|
||||
name: 'ThreeDMap',
|
||||
components: {
|
||||
RegionDetail
|
||||
},
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
@@ -35,8 +44,12 @@ export default {
|
||||
},
|
||||
setup(props) {
|
||||
const mapContainer = ref(null);
|
||||
const showRegionDetail = ref(false);
|
||||
const selectedRegionData = ref({});
|
||||
let scene, camera, renderer, controls;
|
||||
let animationId = null;
|
||||
let landmarks = [];
|
||||
let raycaster, mouse;
|
||||
|
||||
// 初始化3D场景
|
||||
const initScene = () => {
|
||||
@@ -78,6 +91,14 @@ export default {
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// 初始化射线检测器
|
||||
raycaster = new THREE.Raycaster();
|
||||
mouse = new THREE.Vector2();
|
||||
|
||||
// 添加事件监听
|
||||
renderer.domElement.addEventListener('click', onMouseClick, false);
|
||||
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
|
||||
|
||||
// 创建地形和地标
|
||||
createTerrain();
|
||||
createLandmarks();
|
||||
@@ -91,7 +112,7 @@ export default {
|
||||
// 创建一个简单的平面作为地形基础
|
||||
const geometry = new THREE.PlaneGeometry(2000, 2000, 50, 50);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0x4CAF50,
|
||||
color: 0x2c5364,
|
||||
wireframe: false,
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
@@ -99,7 +120,7 @@ export default {
|
||||
|
||||
const terrain = new THREE.Mesh(geometry, material);
|
||||
terrain.rotation.x = -Math.PI / 2;
|
||||
terrain.position.y = -10;
|
||||
terrain.position.y = -20;
|
||||
terrain.receiveShadow = true;
|
||||
scene.add(terrain);
|
||||
|
||||
@@ -115,53 +136,155 @@ export default {
|
||||
|
||||
// 创建地标
|
||||
const createLandmarks = () => {
|
||||
// 创建锡林郭勒盟主要旗县的地标
|
||||
const locations = [
|
||||
{ name: '锡林浩特市', position: [0, 0, 0], color: 0x2196F3 },
|
||||
{ name: '东乌珠穆沁旗', position: [-200, 0, 100], color: 0xFF9800 },
|
||||
{ name: '西乌珠穆沁旗', position: [200, 0, 100], color: 0xF44336 },
|
||||
{ name: '镶黄旗', position: [-100, 0, -150], color: 0x9C27B0 },
|
||||
{ name: '正镶白旗', position: [100, 0, -150], color: 0x4CAF50 }
|
||||
];
|
||||
// 清除现有的地标
|
||||
landmarks.forEach(landmark => {
|
||||
scene.remove(landmark.mesh);
|
||||
scene.remove(landmark.label);
|
||||
});
|
||||
landmarks = [];
|
||||
|
||||
locations.forEach(location => {
|
||||
// 根据传入的地图数据创建地标
|
||||
props.mapData.forEach((location, index) => {
|
||||
// 计算位置(基于锡林浩特市为中心点)
|
||||
const x = (location.coordinates[0] - props.center[0]) * 5000;
|
||||
const z = (props.center[1] - location.coordinates[1]) * 5000;
|
||||
|
||||
// 根据牛只数量确定地标大小
|
||||
const size = Math.max(20, Math.min(50, (location.cattle_count || location.cattleCount) / 1000));
|
||||
const height = Math.max(30, Math.min(100, (location.cattle_count || location.cattleCount) / 500));
|
||||
|
||||
// 创建地标圆柱体
|
||||
const geometry = new THREE.CylinderGeometry(20, 20, 50, 32);
|
||||
const geometry = new THREE.CylinderGeometry(size, size, height, 32);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: location.color,
|
||||
color: getColorByCattleCount(location.cattle_count || location.cattleCount),
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
});
|
||||
const cylinder = new THREE.Mesh(geometry, material);
|
||||
cylinder.position.set(...location.position);
|
||||
cylinder.position.y = 25;
|
||||
cylinder.position.set(x, height/2 - 20, z);
|
||||
cylinder.castShadow = true;
|
||||
cylinder.userData = { location }; // 保存位置信息用于点击检测
|
||||
scene.add(cylinder);
|
||||
|
||||
// 添加地标名称
|
||||
addLabel(location.name, location.position, location.color);
|
||||
// 添加地标名称和数据
|
||||
const label = addLabel(
|
||||
`${location.name}\n牛只: ${location.cattle_count || location.cattleCount}\n牧场: ${location.farm_count || location.farmCount}`,
|
||||
[x, height + 20, z],
|
||||
getColorByCattleCount(location.cattle_count || location.cattleCount)
|
||||
);
|
||||
|
||||
landmarks.push({
|
||||
mesh: cylinder,
|
||||
label: label,
|
||||
data: location
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 根据牛只数量获取颜色
|
||||
const getColorByCattleCount = (count) => {
|
||||
if (count > 20000) return 0x4CAF50; // 绿色 - 数量多
|
||||
if (count > 15000) return 0xFFEB3B; // 黄色 - 数量中等
|
||||
return 0xF44336; // 红色 - 数量较少
|
||||
};
|
||||
|
||||
// 添加标签
|
||||
const addLabel = (text, position, color) => {
|
||||
// 创建简单的文本标签(在实际项目中可以使用更复杂的文本渲染)
|
||||
// 创建简单的文本标签
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = 256;
|
||||
canvas.height = 128;
|
||||
canvas.width = 512;
|
||||
canvas.height = 256;
|
||||
|
||||
context.fillStyle = `#${color.toString(16).padStart(6, '0')}`;
|
||||
context.font = '24px Arial';
|
||||
context.fillStyle = `#${new THREE.Color(color).getHexString()}`;
|
||||
context.font = '24px Microsoft YaHei';
|
||||
context.textAlign = 'center';
|
||||
context.fillText(text, 128, 64);
|
||||
context.textBaseline = 'middle';
|
||||
|
||||
// 支持多行文本
|
||||
const lines = text.split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
context.fillText(line, 256, 128 + (i - (lines.length-1)/2) * 30);
|
||||
});
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({ map: texture });
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.position.set(position[0], 80, position[2]);
|
||||
sprite.scale.set(100, 50, 1);
|
||||
sprite.position.set(position[0], position[1], position[2]);
|
||||
sprite.scale.set(200, 100, 1);
|
||||
scene.add(sprite);
|
||||
return sprite;
|
||||
};
|
||||
|
||||
// 鼠标点击事件处理
|
||||
const onMouseClick = async (event) => {
|
||||
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// 通过摄像机和鼠标位置更新射线
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
// 计算物体和射线的交点
|
||||
const intersects = raycaster.intersectObjects(scene.children);
|
||||
|
||||
for (let i = 0; i < intersects.length; i++) {
|
||||
if (intersects[i].object.userData.location) {
|
||||
const region = intersects[i].object.userData.location;
|
||||
selectedRegionData.value = await fetchRegionDetail(region.id);
|
||||
showRegionDetail.value = true;
|
||||
highlightRegion(intersects[i].object);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标移动事件处理(悬停效果)
|
||||
const onMouseMove = (event) => {
|
||||
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// 通过摄像机和鼠标位置更新射线
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
// 计算物体和射线的交点
|
||||
const intersects = raycaster.intersectObjects(scene.children);
|
||||
|
||||
// 重置所有地标的颜色
|
||||
resetRegionColors();
|
||||
|
||||
// 高亮显示悬停的地標
|
||||
for (let i = 0; i < intersects.length; i++) {
|
||||
if (intersects[i].object.userData.location) {
|
||||
intersects[i].object.material.emissive = new THREE.Color(0x222222);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 高亮选中区域
|
||||
const highlightRegion = (mesh) => {
|
||||
resetRegionColors();
|
||||
mesh.material.emissive = new THREE.Color(0x333333);
|
||||
};
|
||||
|
||||
// 重置区域颜色
|
||||
const resetRegionColors = () => {
|
||||
scene.children.forEach(child => {
|
||||
if (child.isMesh && child.userData.location) {
|
||||
child.material.emissive = new THREE.Color(0x000000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭区域详情
|
||||
const closeRegionDetail = () => {
|
||||
showRegionDetail.value = false;
|
||||
selectedRegionData.value = {};
|
||||
resetRegionColors();
|
||||
};
|
||||
|
||||
// 动画循环
|
||||
@@ -187,6 +310,13 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// 监听地图数据变化
|
||||
watch(() => props.mapData, () => {
|
||||
if (scene) {
|
||||
createLandmarks();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 清理资源
|
||||
const cleanup = () => {
|
||||
if (animationId) {
|
||||
@@ -194,6 +324,8 @@ export default {
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
renderer.domElement.removeEventListener('click', onMouseClick, false);
|
||||
renderer.domElement.removeEventListener('mousemove', onMouseMove, false);
|
||||
mapContainer.value.removeChild(renderer.domElement);
|
||||
renderer.dispose();
|
||||
}
|
||||
@@ -221,7 +353,10 @@ export default {
|
||||
});
|
||||
|
||||
return {
|
||||
mapContainer
|
||||
mapContainer,
|
||||
showRegionDetail,
|
||||
selectedRegionData,
|
||||
closeRegionDetail
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -231,6 +366,7 @@ export default {
|
||||
.three-d-map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-canvas {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3000/api/v1/dashboard';
|
||||
const API_BASE_URL = 'http://localhost:8000/api/v1/dashboard';
|
||||
|
||||
export const fetchOverviewData = async () => {
|
||||
try {
|
||||
@@ -50,4 +50,24 @@ export const fetchFinanceData = async (type) => {
|
||||
console.error('Error fetching finance data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMapData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/regions`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching map data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRegionDetail = async (regionId) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/region/${regionId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching region detail:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
@@ -53,25 +53,16 @@
|
||||
|
||||
<!-- 中间区域 -->
|
||||
<div class="center-section">
|
||||
<!-- 产业概览 -->
|
||||
<!-- 产业概览地图 -->
|
||||
<div class="center-top">
|
||||
<div class="center-border card">
|
||||
<h2>产业概览</h2>
|
||||
<div class="center-content">
|
||||
<div class="total-count">
|
||||
<div class="count-title">牛只总数</div>
|
||||
<div class="count-value">128,456</div>
|
||||
<div class="count-unit">头</div>
|
||||
</div>
|
||||
<div class="total-count">
|
||||
<div class="count-title">牧场数量</div>
|
||||
<div class="count-value">1,245</div>
|
||||
<div class="count-unit">个</div>
|
||||
</div>
|
||||
<div class="growth-rate">
|
||||
<div class="rate-title">同比增长</div>
|
||||
<div class="rate-value positive">5.2%</div>
|
||||
</div>
|
||||
<h2>锡林郭勒盟产业分布</h2>
|
||||
<div class="map-container">
|
||||
<ThreeDMap
|
||||
:height="300"
|
||||
:map-data="mapData"
|
||||
ref="threeDMap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,15 +117,21 @@
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import ThreeDMap from '@/components/map/ThreeDMap.vue'
|
||||
import { fetchMapData } from '@/services/dashboard.js'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
ThreeDMap
|
||||
},
|
||||
setup() {
|
||||
const currentTime = ref(new Date().toLocaleString())
|
||||
const breedingChart = ref(null)
|
||||
const transactionChart = ref(null)
|
||||
const regionChart = ref(null)
|
||||
const riskRadarChart = ref(null)
|
||||
const threeDMap = ref(null)
|
||||
|
||||
let breedingChartInstance = null
|
||||
let transactionChartInstance = null
|
||||
@@ -158,6 +155,9 @@ export default {
|
||||
{ time: '08-18 16:15', type: '运输风险', desc: '运输路线受阻', status: '已处理' }
|
||||
])
|
||||
|
||||
// 地图数据
|
||||
const mapData = ref([])
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 确保 DOM 元素已正确绑定
|
||||
@@ -255,6 +255,35 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取地图数据
|
||||
const loadMapData = async () => {
|
||||
try {
|
||||
const data = await fetchMapData()
|
||||
if (data.regions && data.regions.length > 0) {
|
||||
mapData.value = data.regions
|
||||
} else {
|
||||
// 使用默认数据
|
||||
mapData.value = [
|
||||
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
|
||||
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
|
||||
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
|
||||
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
|
||||
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取地图数据失败:', error)
|
||||
// 使用默认数据
|
||||
mapData.value = [
|
||||
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
|
||||
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
|
||||
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
|
||||
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
|
||||
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString()
|
||||
@@ -270,6 +299,7 @@ export default {
|
||||
|
||||
onMounted(() => {
|
||||
initCharts()
|
||||
loadMapData()
|
||||
timer = setInterval(updateTime, 1000)
|
||||
window.addEventListener('resize', resizeCharts)
|
||||
})
|
||||
@@ -287,10 +317,12 @@ export default {
|
||||
currentTime,
|
||||
keyMetrics,
|
||||
riskData,
|
||||
mapData,
|
||||
breedingChart,
|
||||
transactionChart,
|
||||
regionChart,
|
||||
riskRadarChart
|
||||
riskRadarChart,
|
||||
threeDMap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,15 +457,15 @@ export default {
|
||||
}
|
||||
|
||||
.center-top {
|
||||
height: 20%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.center-middle {
|
||||
height: 50%;
|
||||
height: 35%;
|
||||
}
|
||||
|
||||
.center-bottom {
|
||||
height: 30%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
.center-border, .center-chart-border {
|
||||
@@ -442,6 +474,10 @@ export default {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.center-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user