添加大屏
This commit is contained in:
1698
datav/src/components/Alert.vue
Normal file
1698
datav/src/components/Alert.vue
Normal file
File diff suppressed because it is too large
Load Diff
689
datav/src/components/Dashboard.vue
Normal file
689
datav/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,689 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="main-title">宁夏智慧畜牧可视化大屏</span>
|
||||
<span class="sub-title">宁夏回族自治区</span>
|
||||
</div>
|
||||
<div class="time-info">
|
||||
<span class="date">{{ currentDate }}</span>
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
<span class="weather">22°C 晴朗</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="left-panel">
|
||||
<!-- 畜牧产业基础分析 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📊</span>
|
||||
畜牧产业基础分析
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">牛存栏</div>
|
||||
<div class="stat-value">200 <span class="unit">万</span></div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">羊存栏</div>
|
||||
<div class="stat-value">200 <span class="unit">万</span></div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">猪存栏</div>
|
||||
<div class="stat-value">200 <span class="unit">万</span></div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">合作社数量</div>
|
||||
<div class="stat-value">200 <span class="unit">个</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 养殖品种分布 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">🐄</span>
|
||||
养殖品种分布
|
||||
</div>
|
||||
<div class="chart-container" ref="breedChart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 全年文档管理统计 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📋</span>
|
||||
全年文档管理统计
|
||||
</div>
|
||||
<div class="chart-container" ref="docChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中央地图区域 -->
|
||||
<div class="center-panel">
|
||||
<Map3D />
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="right-panel">
|
||||
<!-- 流动资源分析 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">💰</span>
|
||||
流动资源分析
|
||||
</div>
|
||||
<div class="resource-list">
|
||||
<div class="resource-item">
|
||||
<span class="resource-name">资金</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: 75%;"></div>
|
||||
</div>
|
||||
<span class="resource-value">7500000</span>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-name">饲料</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: 60%;"></div>
|
||||
</div>
|
||||
<span class="resource-value">6000000</span>
|
||||
</div>
|
||||
<div class="resource-item">
|
||||
<span class="resource-name">设备</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: 85%;"></div>
|
||||
</div>
|
||||
<span class="resource-value">8500000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每日资源统计 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📈</span>
|
||||
每日资源统计
|
||||
</div>
|
||||
<div class="chart-container" ref="dailyChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部图表区域 -->
|
||||
<div class="bottom-panel">
|
||||
<div class="chart-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📊</span>
|
||||
全年文档管理统计
|
||||
</div>
|
||||
<div class="chart-container" ref="yearChart"></div>
|
||||
</div>
|
||||
<div class="chart-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📈</span>
|
||||
近七日资源统计
|
||||
</div>
|
||||
<div class="chart-container" ref="weekChart"></div>
|
||||
</div>
|
||||
<div class="chart-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📊</span>
|
||||
每日资源流入
|
||||
</div>
|
||||
<div class="chart-container" ref="inflowChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import Map3D from './Map3D.vue'
|
||||
|
||||
// 响应式数据
|
||||
const currentDate = ref('')
|
||||
const currentTime = ref('')
|
||||
|
||||
// 图表引用
|
||||
const breedChart = ref(null)
|
||||
const docChart = ref(null)
|
||||
const dailyChart = ref(null)
|
||||
const yearChart = ref(null)
|
||||
const weekChart = ref(null)
|
||||
const inflowChart = ref(null)
|
||||
|
||||
// 定时器
|
||||
let timeInterval = null
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
currentDate.value = now.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).replace(/\//g, '-')
|
||||
currentTime.value = now.toLocaleTimeString('zh-CN', {
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 养殖品种分布饼图
|
||||
if (breedChart.value) {
|
||||
const chart = echarts.init(breedChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 35, name: '牛', itemStyle: { color: '#84acf0' } }, /* 地图顶部材质颜色 */
|
||||
{ value: 25, name: '羊', itemStyle: { color: '#7af4ff' } }, /* 地图光源颜色 */
|
||||
{ value: 20, name: '猪', itemStyle: { color: '#00F6FF' } }, /* 养殖场标签颜色 */
|
||||
{ value: 20, name: '其他', itemStyle: { color: '#123024' } } /* 地图侧面材质颜色 */
|
||||
],
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 全年文档管理统计图表
|
||||
if (docChart.value) {
|
||||
const chart = echarts.init(docChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
left: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
splitLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
data: [50, 80, 65, 90, 120, 100],
|
||||
itemStyle: {
|
||||
color: '#7af4ff'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#7af4ff'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 每日资源统计柱状图
|
||||
if (dailyChart.value) {
|
||||
const chart = echarts.init(dailyChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
left: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
splitLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [120, 200, 150, 80, 70, 110, 130],
|
||||
itemStyle: {
|
||||
color: '#84acf0' /* 使用地图顶部材质颜色,移除渐变 */
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 底部年度图表
|
||||
if (yearChart.value) {
|
||||
const chart = echarts.init(yearChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
left: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
splitLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [300, 450, 320, 520],
|
||||
itemStyle: {
|
||||
color: '#00F6FF'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 近七日资源统计
|
||||
if (weekChart.value) {
|
||||
const chart = echarts.init(weekChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
left: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1日', '2日', '3日', '4日', '5日', '6日', '7日'],
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#fff', fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
splitLine: { lineStyle: { color: '#333' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
data: [80, 95, 110, 90, 120, 85, 130],
|
||||
itemStyle: {
|
||||
color: '#123024'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#123024'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 每日资源流入
|
||||
if (inflowChart.value) {
|
||||
const chart = echarts.init(inflowChart.value)
|
||||
chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['30%', '60%'],
|
||||
data: [
|
||||
{ value: 40, name: '资金流入', itemStyle: { color: '#84acf0' } },
|
||||
{ value: 30, name: '物资流入', itemStyle: { color: '#7af4ff' } },
|
||||
{ value: 20, name: '设备流入', itemStyle: { color: '#00F6FF' } },
|
||||
{ value: 10, name: '其他', itemStyle: { color: '#123024' } }
|
||||
],
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
// 延迟初始化图表,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #0c1426; /* 移除渐变,使用地图基础深色 */
|
||||
color: #fff;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 科技感背景效果 */
|
||||
.dashboard-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
rgba(132, 172, 240, 0.05),
|
||||
rgba(18, 48, 36, 0.05);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
background: rgba(132, 172, 240, 0.1); /* 使用地图顶部材质颜色 */
|
||||
border-bottom: 2px solid #84acf0; /* 地图顶部材质颜色 */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #84acf0; /* 单色替代渐变 */
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from { box-shadow: 0 0 5px rgba(132, 172, 240, 0.5); }
|
||||
to { box-shadow: 0 0 20px rgba(132, 172, 240, 0.8); }
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
text-shadow: 0 0 20px rgba(132, 172, 240, 0.5);
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.date, .time {
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.weather {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 主体内容 */
|
||||
.main-content {
|
||||
height: calc(100vh - 240px);
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.left-panel, .right-panel {
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.center-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 面板样式 */
|
||||
.panel-section {
|
||||
background: rgba(132, 172, 240, 0.05); /* 使用地图颜色 */
|
||||
border: 1px solid rgba(132, 172, 240, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #84acf0; /* 地图顶部材质颜色 */
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(132, 172, 240, 0.2);
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 统计数据网格 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: rgba(18, 48, 36, 0.3); /* 使用地图侧面颜色 */
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(132, 172, 240, 0.2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 地图容器 */
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(18, 48, 36, 0.2); /* 使用地图侧面颜色 */
|
||||
border: 2px solid rgba(132, 172, 240, 0.3);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.map-content {
|
||||
height: calc(100% - 80px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 资源列表 */
|
||||
.resource-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.resource-name {
|
||||
width: 60px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(18, 48, 36, 0.3); /* 使用地图侧面颜色 */
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: #84acf0; /* 单色替代渐变 */
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
font-size: 12px;
|
||||
color: #84acf0; /* 使用地图顶部材质颜色 */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 底部面板 */
|
||||
.bottom-panel {
|
||||
height: 160px;
|
||||
display: flex;
|
||||
padding: 0 20px 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
flex: 1;
|
||||
background: rgba(132, 172, 240, 0.05); /* 使用地图颜色 */
|
||||
border: 1px solid rgba(132, 172, 240, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #84acf0; /* 地图顶部材质颜色 */
|
||||
}
|
||||
|
||||
.chart-section .chart-container {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1920px) {
|
||||
.main-title { font-size: 28px; }
|
||||
.left-panel, .right-panel { width: 320px; }
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
.main-title { font-size: 24px; }
|
||||
.left-panel, .right-panel { width: 300px; }
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
218
datav/src/components/FarmPopup.vue
Normal file
218
datav/src/components/FarmPopup.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div v-if="visible" class="farm-popup-overlay" @click="closePopup">
|
||||
<div class="farm-popup" @click.stop>
|
||||
<div class="popup-header">
|
||||
<h3>{{ farm.name }}</h3>
|
||||
<button class="close-btn" @click="closePopup">×</button>
|
||||
</div>
|
||||
<div class="popup-content">
|
||||
<div class="farm-info">
|
||||
<div class="info-item">
|
||||
<!-- <span class="label">养殖类型:</span>
|
||||
<span class="value">{{ farm.type }}</span> -->
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">存栏数量:</span>
|
||||
<span class="value highlight">{{ farm.livestock.toLocaleString() }} 头</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">占地面积:</span>
|
||||
<span class="value">{{ farm.area }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">成立时间:</span>
|
||||
<span class="value">{{ farm.established }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">联系方式:</span>
|
||||
<span class="value">{{ farm.contact }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<!-- <button class="action-btn primary">查看详情</button>
|
||||
<button class="action-btn secondary">联系养殖场</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FarmPopup',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
farm: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
closePopup() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.farm-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.farm-popup {
|
||||
background: rgba(15, 25, 45, 0.95);
|
||||
border: 2px solid #00d4ff;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
box-shadow:
|
||||
0 0 30px rgba(0, 212, 255, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
animation: popupSlideIn 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes popupSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
background: #00d4ff;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.popup-header h3 {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.farm-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #a0a8b8;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value.highlight {
|
||||
color: #00d4ff;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #00d4ff;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: transparent;
|
||||
color: #00d4ff;
|
||||
border: 2px solid #00d4ff;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: #00d4ff;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
1216
datav/src/components/Home.vue
Normal file
1216
datav/src/components/Home.vue
Normal file
File diff suppressed because it is too large
Load Diff
935
datav/src/components/Map3D.vue
Normal file
935
datav/src/components/Map3D.vue
Normal file
@@ -0,0 +1,935 @@
|
||||
<template>
|
||||
<div class="map-3d-container">
|
||||
<div id="app-32-map"></div>
|
||||
<FarmPopup
|
||||
:visible="showPopup"
|
||||
:farm="selectedFarmData"
|
||||
@close="closePopup"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Earth3d as BaseEarth } from '@/utils';
|
||||
import TWEEN from '@tweenjs/tween.js';
|
||||
import * as THREE from 'three';
|
||||
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { random } from '@/utils';
|
||||
import useFileLoader from '@/hooks/useFileLoader.js';
|
||||
import useCountry from '@/hooks/useCountry.js';
|
||||
import useCoord from '@/hooks/useCoord.js';
|
||||
import useConversionStandardData from '@/hooks/useConversionStandardData.js';
|
||||
import useSequenceFrameAnimate from '@/hooks/useSequenceFrameAnimate';
|
||||
import useCSS2DRender from '@/hooks/useCSS2DRenderer';
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
|
||||
import FarmPopup from './FarmPopup.vue';
|
||||
|
||||
let centerXY = [106.2581, 38.4681]; // 宁夏回族自治区中心坐标
|
||||
|
||||
// 养殖场数据
|
||||
const farmData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '东方养殖场',
|
||||
position: [106.8581, 38.8681], // 银川市附近
|
||||
livestock: 25234,
|
||||
area: '1200亩',
|
||||
type: '肉牛养殖',
|
||||
established: '2018年',
|
||||
contact: '张经理 138****1234'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '西部牧场',
|
||||
position: [106.2581, 38.1181], // 吴忠市附近
|
||||
livestock: 32475,
|
||||
area: '1800亩',
|
||||
type: '奶牛养殖',
|
||||
established: '2016年',
|
||||
contact: '李经理 139****5678'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '南山农场',
|
||||
position: [106.0581, 36.0681], // 固原市附近
|
||||
livestock: 28900,
|
||||
area: '1500亩',
|
||||
type: '肉牛养殖',
|
||||
established: '2019年',
|
||||
contact: '王经理 137****9012'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '北岭牧业',
|
||||
position: [105.1881, 37.5181], // 中卫市附近
|
||||
livestock: 21100,
|
||||
area: '1000亩',
|
||||
type: '混合养殖',
|
||||
established: '2020年',
|
||||
contact: '赵经理 136****3456'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '中心养殖基地',
|
||||
position: [106.2581, 38.4681], // 保持中心位置(银川市中心)
|
||||
livestock: 19000,
|
||||
area: '900亩',
|
||||
type: '肉牛养殖',
|
||||
established: '2017年',
|
||||
contact: '陈经理 135****7890'
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
name: '3dMap30',
|
||||
components: {
|
||||
FarmPopup
|
||||
},
|
||||
setup() {
|
||||
let baseEarth = null;
|
||||
let farmMarkers = []; // 存储养殖场标记
|
||||
let selectedFarm = null; // 当前选中的养殖场
|
||||
|
||||
// 弹窗状态管理
|
||||
const showPopup = ref(false);
|
||||
const selectedFarmData = ref(null);
|
||||
|
||||
// 重置
|
||||
const resize = () => {
|
||||
baseEarth.resize();
|
||||
};
|
||||
|
||||
const { requestData } = useFileLoader();
|
||||
const { transfromGeoJSON } = useConversionStandardData();
|
||||
const { getBoundingBox, geoSphereCoord } = useCoord();
|
||||
const { createCountryFlatLine } = useCountry();
|
||||
const { initCSS2DRender, create2DTag } = useCSS2DRender();
|
||||
// 序列帧
|
||||
const { createSequenceFrame } = useSequenceFrameAnimate();
|
||||
|
||||
const texture = new THREE.TextureLoader();
|
||||
const textureMap = texture.load('/data/map/gz-map.jpg');
|
||||
const texturefxMap = texture.load('/data/map/gz-map-fx.jpg');
|
||||
const rotatingApertureTexture = texture.load('/data/map/rotatingAperture.png');
|
||||
const rotatingPointTexture = texture.load('/data/map/rotating-point2.png');
|
||||
const circlePoint = texture.load('/data/map/circle-point.png');
|
||||
const sceneBg = texture.load('/data/map/scene-bg2.png');
|
||||
textureMap.wrapS = texturefxMap.wrapS = THREE.RepeatWrapping;
|
||||
textureMap.wrapT = texturefxMap.wrapT = THREE.RepeatWrapping;
|
||||
textureMap.flipY = texturefxMap.flipY = false;
|
||||
textureMap.rotation = texturefxMap.rotation = THREE.MathUtils.degToRad(45);
|
||||
const scale = 0.128;
|
||||
textureMap.repeat.set(scale, scale);
|
||||
texturefxMap.repeat.set(scale, scale);
|
||||
const topFaceMaterial = new THREE.MeshPhongMaterial({
|
||||
map: textureMap,
|
||||
color: '#84acf0',
|
||||
combine: THREE.MultiplyOperation,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
});
|
||||
const sideMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0x123024,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
});
|
||||
const bottomZ = -0.2;
|
||||
// 初始化gui
|
||||
// const initGui = () => {
|
||||
// const gui = new GUI();
|
||||
// const guiParams = {
|
||||
// topColor: '84acf0',
|
||||
// sideColor: '#123024',
|
||||
// scale:0.1,
|
||||
// };
|
||||
// gui.addColor(guiParams, 'topColor').onChange((val) => {
|
||||
// topFaceMaterial.color = new THREE.Color(val);
|
||||
// });
|
||||
// gui.addColor(guiParams, 'sideColor').onChange((val) => {
|
||||
// sideMaterial.color = new THREE.Color(val);
|
||||
// });
|
||||
// gui.add(guiParams, 'scale', 0, 1).onChange((val) => {
|
||||
// textureMap.repeat.set(val, val);
|
||||
// texturefxMap.repeat.set(val, val);
|
||||
// });
|
||||
// };
|
||||
// 初始化旋转光圈
|
||||
const initRotatingAperture = (scene, width) => {
|
||||
let plane = new THREE.PlaneGeometry(width, width);
|
||||
let material = new THREE.MeshBasicMaterial({
|
||||
map: rotatingApertureTexture,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
depthTest: true,
|
||||
});
|
||||
let mesh = new THREE.Mesh(plane, material);
|
||||
mesh.position.set(...centerXY, 0);
|
||||
mesh.scale.set(1.1, 1.1, 1.1);
|
||||
scene.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
// 初始化旋转点
|
||||
const initRotatingPoint = (scene, width) => {
|
||||
let plane = new THREE.PlaneGeometry(width, width);
|
||||
let material = new THREE.MeshBasicMaterial({
|
||||
map: rotatingPointTexture,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
depthTest: true,
|
||||
});
|
||||
let mesh = new THREE.Mesh(plane, material);
|
||||
mesh.position.set(...centerXY, bottomZ - 0.02);
|
||||
mesh.scale.set(1.1, 1.1, 1.1);
|
||||
scene.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
// 初始化背景
|
||||
const initSceneBg = (scene, width) => {
|
||||
let plane = new THREE.PlaneGeometry(width * 4, width * 4);
|
||||
let material = new THREE.MeshPhongMaterial({
|
||||
// color: 0x061920,
|
||||
color: '#2AF4FC',
|
||||
map: sceneBg,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
depthTest: true,
|
||||
});
|
||||
|
||||
let mesh = new THREE.Mesh(plane, material);
|
||||
mesh.position.set(...centerXY, bottomZ - 0.2);
|
||||
scene.add(mesh);
|
||||
};
|
||||
// 初始化原点
|
||||
const initCirclePoint = (scene, width) => {
|
||||
let plane = new THREE.PlaneGeometry(width, width);
|
||||
let material = new THREE.MeshPhongMaterial({
|
||||
color: 0x00ffff,
|
||||
map: circlePoint,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
// depthTest: false,
|
||||
});
|
||||
let mesh = new THREE.Mesh(plane, material);
|
||||
mesh.position.set(...centerXY, bottomZ - 0.1);
|
||||
// let mesh2 = mesh.clone()
|
||||
// mesh2.position.set(...centerXY, bottomZ - 0.001)
|
||||
scene.add(mesh);
|
||||
};
|
||||
// 初始化粒子
|
||||
const initParticle = (scene, bound) => {
|
||||
// 获取中心点和中间地图大小
|
||||
let { center, size } = bound;
|
||||
// 构建范围,中间地图的2倍
|
||||
let minX = center.x - size.x;
|
||||
let maxX = center.x + size.x;
|
||||
let minY = center.y - size.y;
|
||||
let maxY = center.y + size.y;
|
||||
let minZ = -6;
|
||||
let maxZ = 6;
|
||||
|
||||
let particleArr = [];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const particle = createSequenceFrame({
|
||||
image: './data/map/上升粒子1.png',
|
||||
width: 180,
|
||||
height: 189,
|
||||
frame: 9,
|
||||
column: 9,
|
||||
row: 1,
|
||||
speed: 0.5,
|
||||
});
|
||||
let particleScale = random(5, 10) / 1000;
|
||||
particle.scale.set(particleScale, particleScale, particleScale);
|
||||
particle.rotation.x = Math.PI / 2;
|
||||
let x = random(minX, maxX);
|
||||
let y = random(minY, maxY);
|
||||
let z = random(minZ, maxZ);
|
||||
particle.position.set(x, y, z);
|
||||
particleArr.push(particle);
|
||||
}
|
||||
scene.add(...particleArr);
|
||||
return particleArr;
|
||||
};
|
||||
// 创建顶部底部边线
|
||||
const initBorderLine = (data, mapGroup) => {
|
||||
let lineTop = createCountryFlatLine(
|
||||
data,
|
||||
{
|
||||
color: 0xffffff,
|
||||
linewidth: 0.0015,
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
},
|
||||
'Line2'
|
||||
);
|
||||
lineTop.position.z += 0.305;
|
||||
let lineBottom = createCountryFlatLine(
|
||||
data,
|
||||
{
|
||||
color: 0x61fbfd,
|
||||
linewidth: 0.002,
|
||||
// transparent: true,
|
||||
depthTest: false,
|
||||
},
|
||||
'Line2'
|
||||
);
|
||||
lineBottom.position.z -= 0.1905;
|
||||
// 添加边线
|
||||
mapGroup.add(lineTop);
|
||||
mapGroup.add(lineBottom);
|
||||
};
|
||||
|
||||
// 创建各市区边界线
|
||||
const initCityBorderLines = (data, mapGroup) => {
|
||||
// 为不同城市定义不同颜色
|
||||
const cityColors = {
|
||||
'银川市': 0x00ffff, // 青色
|
||||
'石嘴山市': 0xff6600, // 橙色
|
||||
'吴忠市': 0x0066ff, // 蓝色
|
||||
'固原市': 0xff0066, // 粉红色
|
||||
'中卫市': 0xffff00 // 黄色
|
||||
};
|
||||
|
||||
// console.log('开始创建城市边界线,数据:', data);
|
||||
|
||||
if (!data || !data.features) {
|
||||
// console.log('没有找到城市数据');
|
||||
return;
|
||||
}
|
||||
|
||||
data.features.forEach((feature, index) => {
|
||||
if (feature.geometry && feature.geometry.coordinates && feature.properties) {
|
||||
const cityName = feature.properties.name;
|
||||
const color = 0xffffff; // 统一设置为白色
|
||||
|
||||
// console.log(`创建城市边界线: ${cityName}`);
|
||||
|
||||
try {
|
||||
// 手动创建边界线,使用与地图相同的坐标处理方式
|
||||
const borderGroup = new THREE.Group();
|
||||
const coordinates = feature.geometry.coordinates;
|
||||
|
||||
coordinates.forEach((multiPolygon) => {
|
||||
multiPolygon.forEach((polygon) => {
|
||||
const points = [];
|
||||
|
||||
// 提取坐标点,与地图处理方式一致
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
let [x, y] = polygon[i];
|
||||
points.push(new THREE.Vector3(x, y, 0.31)); // 设置z坐标稍高于地面
|
||||
}
|
||||
|
||||
if (points.length > 1) {
|
||||
// 创建线条几何体
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: color,
|
||||
transparent: true,
|
||||
opacity: 1.0,
|
||||
linewidth: 5
|
||||
});
|
||||
|
||||
const line = new THREE.Line(geometry, material);
|
||||
borderGroup.add(line);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (borderGroup.children.length > 0) {
|
||||
borderGroup.name = `cityBorder_${cityName}`;
|
||||
mapGroup.add(borderGroup);
|
||||
// console.log(`已添加城市边界线: ${cityName}, 线条数量: ${borderGroup.children.length}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(`创建城市边界线时出错: ${cityName}`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// console.log('城市边界线创建完成,mapGroup子对象数量:', mapGroup.children.length);
|
||||
};
|
||||
// 已移除光柱功能,使用养殖场标记点替代
|
||||
|
||||
// 初始化养殖场标记
|
||||
const initFarmMarkers = (mapGroup) => {
|
||||
console.log('开始初始化养殖场标记,数据:', farmData);
|
||||
|
||||
farmData.forEach((farm, index) => {
|
||||
const [lng, lat] = farm.position;
|
||||
console.log(`创建养殖场标记 ${index + 1}: ${farm.name}, 坐标: [${lng}, ${lat}]`);
|
||||
|
||||
// 创建球体几何体作为标记点
|
||||
const geometry = new THREE.SphereGeometry(0.1, 32, 32); // 显著增大球体尺寸
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
|
||||
color: 0x00F6FF, // 红色
|
||||
transparent: false,
|
||||
side: THREE.DoubleSide // 确保双面渲染
|
||||
});
|
||||
|
||||
const farmMarker = new THREE.Mesh(geometry, material);
|
||||
|
||||
// 使用与边界线相同的坐标处理方式
|
||||
farmMarker.position.set(lng, lat, 0.5); // 设置较高的z值确保可见
|
||||
|
||||
// 设置用户数据
|
||||
farmMarker.userData = { ...farm, type: 'farmMarker' };
|
||||
|
||||
// 添加到场景
|
||||
mapGroup.add(farmMarker);
|
||||
farmMarkers.push(farmMarker);
|
||||
|
||||
// 创建养殖场名称标签
|
||||
const labelDiv = document.createElement('div');
|
||||
labelDiv.className = 'farm-label';
|
||||
labelDiv.textContent = farm.name;
|
||||
labelDiv.style.cssText = `
|
||||
font-size: 18px;
|
||||
color: #00F6FF;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #00F6FF;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
transform: translate(-50%, -100%);
|
||||
`;
|
||||
|
||||
const label = new CSS2DObject(labelDiv);
|
||||
label.position.copy(farmMarker.position);
|
||||
label.position.z += 0.3; // 使用Z轴提升标签高度
|
||||
label.visible = true;
|
||||
|
||||
mapGroup.add(label);
|
||||
|
||||
console.log(`养殖场标签 ${index + 1} 创建:`, {
|
||||
name: farm.name,
|
||||
position: label.position,
|
||||
visible: label.visible,
|
||||
element: labelDiv,
|
||||
elementStyle: labelDiv.style.cssText
|
||||
});
|
||||
|
||||
console.log(`养殖场标记点和标签 ${index + 1} 已添加: ${farm.name},位置: [${lng}, ${lat}, 0.5]`);
|
||||
});
|
||||
|
||||
console.log(`养殖场标记初始化完成,共创建 ${farmData.length} 个标记点`);
|
||||
console.log('farmMarkers数组:', farmMarkers);
|
||||
|
||||
// 强制触发一次CSS2D渲染
|
||||
if (baseEarth && baseEarth.css2dRender && baseEarth.scene && baseEarth.camera) {
|
||||
console.log('强制触发CSS2D渲染器渲染');
|
||||
baseEarth.css2dRender.render(baseEarth.scene, baseEarth.camera);
|
||||
}
|
||||
|
||||
// 创建一个测试CSS2D标签
|
||||
const testLabelDiv = document.createElement('div');
|
||||
testLabelDiv.textContent = '测试标签';
|
||||
testLabelDiv.style.cssText = `
|
||||
font-size: 20px;
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
background: rgba(255,255,255,0.9);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ff0000;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const testLabel = new CSS2DObject(testLabelDiv);
|
||||
testLabel.position.set(0, 0, 2); // 设置在场景中心上方
|
||||
testLabel.visible = true;
|
||||
|
||||
if (baseEarth && baseEarth.scene) {
|
||||
baseEarth.scene.add(testLabel);
|
||||
console.log('测试标签已添加到场景');
|
||||
}
|
||||
|
||||
// 在页面上显示调试信息
|
||||
const debugInfo = document.createElement('div');
|
||||
debugInfo.style.cssText = `
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
z-index: 20000;
|
||||
max-width: 300px;
|
||||
`;
|
||||
// debugInfo.innerHTML = `
|
||||
// <div>养殖场标记数量: ${farmData.length}</div>
|
||||
// <div>mapGroup子对象数量: ${mapGroup.children.length}</div>
|
||||
// <div>CSS2D渲染器: ${baseEarth && baseEarth.css2dRender ? '已初始化' : '未初始化'}</div>
|
||||
// <div>测试标签已添加</div>
|
||||
// `;
|
||||
document.body.appendChild(debugInfo);
|
||||
|
||||
// 10秒后移除调试信息和测试标签
|
||||
setTimeout(() => {
|
||||
if (debugInfo.parentNode) {
|
||||
debugInfo.parentNode.removeChild(debugInfo);
|
||||
}
|
||||
if (baseEarth && baseEarth.scene && testLabel) {
|
||||
baseEarth.scene.remove(testLabel);
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// 初始化养殖场点击事件处理
|
||||
const initFarmClickHandler = () => {
|
||||
console.log('开始初始化点击事件处理器');
|
||||
console.log('baseEarth:', baseEarth);
|
||||
console.log('baseEarth.container:', baseEarth.container);
|
||||
console.log('farmMarkers数量:', farmMarkers.length);
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
const onMouseClick = (event) => {
|
||||
console.log('点击事件触发');
|
||||
|
||||
// 计算鼠标位置
|
||||
const rect = baseEarth.container.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
console.log('鼠标坐标:', mouse.x, mouse.y);
|
||||
|
||||
// 设置射线
|
||||
raycaster.setFromCamera(mouse, baseEarth.camera);
|
||||
|
||||
// 检测与养殖场标记的交集
|
||||
const intersects = raycaster.intersectObjects(farmMarkers);
|
||||
|
||||
console.log('射线检测结果数量:', intersects.length);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const clickedMarker = intersects[0].object;
|
||||
console.log('点击了养殖场标记:', clickedMarker.userData.name);
|
||||
|
||||
if (clickedMarker.userData && clickedMarker.userData.type === 'farmMarker') {
|
||||
selectedFarm = clickedMarker.userData;
|
||||
showFarmPopup(clickedMarker.userData);
|
||||
}
|
||||
} else {
|
||||
console.log('未检测到养殖场标记点击');
|
||||
}
|
||||
};
|
||||
|
||||
baseEarth.container.addEventListener('click', onMouseClick);
|
||||
console.log('点击事件监听器已添加');
|
||||
};
|
||||
|
||||
// 显示养殖场弹窗
|
||||
const showFarmPopup = (farm) => {
|
||||
console.log('显示养殖场弹窗:', farm.name);
|
||||
selectedFarmData.value = farm;
|
||||
showPopup.value = true;
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closePopup = () => {
|
||||
showPopup.value = false;
|
||||
selectedFarmData.value = null;
|
||||
};
|
||||
// 创建标签
|
||||
const initLabel = (properties, scene) => {
|
||||
if(!properties.centroid && !properties.center && !properties.cp){
|
||||
// console.log('标签创建失败:缺少center、centroid或cp属性', properties.name);
|
||||
return false
|
||||
}
|
||||
// 设置标签的显示内容和位置
|
||||
let labelCenter = properties.center || properties.centroid || properties.cp;
|
||||
// console.log(`创建标签: ${properties.name}, 位置:`, labelCenter);
|
||||
|
||||
// 创建标签(使用简单参数,类似122222项目)
|
||||
var label = create2DTag(properties.name, 'map-32-label');
|
||||
scene.add(label);
|
||||
|
||||
// 调用show方法显示标签(参考122222项目的实现)
|
||||
label.show(properties.name, new THREE.Vector3(...labelCenter, 0.8));
|
||||
|
||||
// console.log(`标签创建并显示成功: ${properties.name}`);
|
||||
};
|
||||
onMounted(async () => {
|
||||
console.log('=== Map3D组件已挂载 ===');
|
||||
console.log('farmData:', farmData);
|
||||
// 等待DOM完全渲染
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// 检查容器尺寸但不阻止初始化
|
||||
const container = document.getElementById('app-32-map');
|
||||
// console.log('容器尺寸:', container ? `${container.offsetWidth}x${container.offsetHeight}` : '容器不存在');
|
||||
|
||||
if (container && (container.offsetWidth === 0 || container.offsetHeight === 0)) {
|
||||
// console.log('容器尺寸为0,但继续初始化...');
|
||||
// 减少等待时间,不阻止初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// 宁夏回族自治区数据
|
||||
let provinceData;
|
||||
try {
|
||||
provinceData = await requestData('./data/map/宁夏回族自治区.json');
|
||||
provinceData = transfromGeoJSON(provinceData);
|
||||
} catch (error) {
|
||||
// console.error('地图数据加载失败:', error);
|
||||
return; // 如果数据加载失败,直接返回
|
||||
}
|
||||
|
||||
class CurrentEarth extends BaseEarth {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.particleArr = []; // 初始化粒子数组
|
||||
}
|
||||
initCamera() {
|
||||
let { width, height } = this.options;
|
||||
let rate = width / height;
|
||||
// 设置45°的透视相机,更符合人眼观察
|
||||
this.camera = new THREE.PerspectiveCamera(45, rate, 0.001, 90000000);
|
||||
this.camera.up.set(0, 0, 1);
|
||||
// 宁夏
|
||||
this.camera.position.set(106.27777217804006, 35.660260562607277, 8.029548316292933); //相机在Three.js坐标系中的位置
|
||||
this.camera.lookAt(...centerXY, 0);
|
||||
}
|
||||
initModel() {
|
||||
try {
|
||||
// 创建组
|
||||
this.mapGroup = new THREE.Group();
|
||||
// 标签 初始化 - 确保使用有效的尺寸
|
||||
const validOptions = {
|
||||
...this.options,
|
||||
width: Math.max(this.options.width || this.container.offsetWidth || 800, 800),
|
||||
height: Math.max(this.options.height || this.container.offsetHeight || 600, 600)
|
||||
};
|
||||
this.css2dRender = initCSS2DRender(validOptions, this.container);
|
||||
|
||||
// 确保CSS2D渲染器的DOM元素有正确的样式
|
||||
this.css2dRender.domElement.style.zIndex = '10000'; // 确保在WebGL canvas之上
|
||||
this.css2dRender.domElement.style.pointerEvents = 'none';
|
||||
|
||||
console.log('CSS2D渲染器初始化完成:', {
|
||||
renderer: this.css2dRender,
|
||||
domElement: this.css2dRender.domElement,
|
||||
containerChildren: this.container.children.length
|
||||
});
|
||||
|
||||
// console.log('开始处理省份数据,features数量:', provinceData.features.length);
|
||||
provinceData.features.forEach((elem, index) => {
|
||||
// console.log(`处理第${index + 1}个feature:`, elem.properties.name);
|
||||
|
||||
// 定一个省份对象
|
||||
const province = new THREE.Object3D();
|
||||
// 坐标
|
||||
const coordinates = elem.geometry.coordinates;
|
||||
// city 属性
|
||||
const properties = elem.properties;
|
||||
|
||||
|
||||
// 循环坐标
|
||||
coordinates.forEach((multiPolygon) => {
|
||||
multiPolygon.forEach((polygon) => {
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
// 绘制shape
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
|
||||
let [x, y] = polygon[i];
|
||||
if (i === 0) {
|
||||
shape.moveTo(x, y);
|
||||
}
|
||||
shape.lineTo(x, y);
|
||||
}
|
||||
// 拉伸设置
|
||||
const extrudeSettings = {
|
||||
depth: 0.2,
|
||||
bevelEnabled: true,
|
||||
bevelSegments: 1,
|
||||
bevelThickness: 0.1,
|
||||
};
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
||||
const mesh = new THREE.Mesh(geometry, [topFaceMaterial, sideMaterial]);
|
||||
province.add(mesh);
|
||||
});
|
||||
});
|
||||
this.mapGroup.add(province);
|
||||
// 创建标点和标签
|
||||
|
||||
initLabel(properties, this.scene);
|
||||
});
|
||||
// 创建上下边框
|
||||
initBorderLine(provinceData, this.mapGroup);
|
||||
|
||||
// 创建各市区边界线
|
||||
initCityBorderLines(provinceData, this.mapGroup);
|
||||
|
||||
let earthGroupBound = getBoundingBox(this.mapGroup);
|
||||
centerXY = [earthGroupBound.center.x, earthGroupBound.center.y];
|
||||
let { size } = earthGroupBound;
|
||||
let width = size.x < size.y ? size.y + 1 : size.x + 1;
|
||||
// 添加背景,修饰元素
|
||||
this.rotatingApertureMesh = initRotatingAperture(this.scene, width);
|
||||
this.rotatingPointMesh = initRotatingPoint(this.scene, width - 2);
|
||||
initCirclePoint(this.scene, width);
|
||||
initSceneBg(this.scene, width);
|
||||
|
||||
// 将组添加到场景中
|
||||
// console.log('将mapGroup添加到场景中,mapGroup子对象数量:', this.mapGroup.children.length);
|
||||
this.scene.add(this.mapGroup);
|
||||
// console.log('场景中对象数量:', this.scene.children.length);
|
||||
this.particleArr = initParticle(this.scene, earthGroupBound);
|
||||
// console.log('粒子系统初始化完成');
|
||||
|
||||
// 养殖场标记将在baseEarth.run()完成后初始化
|
||||
|
||||
// 更新相机目标到新的中心点
|
||||
this.camera.lookAt(...centerXY, 0);
|
||||
if (this.controls) {
|
||||
this.controls.target.set(...centerXY, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
initGui();
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
}
|
||||
}
|
||||
getDataRenderMap() {}
|
||||
|
||||
destroy() {}
|
||||
initControls() {
|
||||
super.initControls();
|
||||
this.controls.target = new THREE.Vector3(...centerXY, 0);
|
||||
}
|
||||
initLight() {
|
||||
// 平行光1 - 主光源
|
||||
let directionalLight1 = new THREE.DirectionalLight(0x7af4ff, 1.2);
|
||||
directionalLight1.position.set(...centerXY, 50);
|
||||
directionalLight1.castShadow = false;
|
||||
|
||||
// 平行光2 - 辅助光源
|
||||
let directionalLight2 = new THREE.DirectionalLight(0x7af4ff, 0.8);
|
||||
directionalLight2.position.set(centerXY[0] + 20, centerXY[1] + 20, 40);
|
||||
directionalLight2.castShadow = false;
|
||||
|
||||
// 平行光3 - 顶部光源,增强边界线可见性
|
||||
let directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.6);
|
||||
directionalLight3.position.set(...centerXY, 80);
|
||||
directionalLight3.castShadow = false;
|
||||
|
||||
// 环境光 - 增强整体亮度
|
||||
let ambientLight = new THREE.AmbientLight(0x7af4ff, 1.5);
|
||||
|
||||
// 将光源添加到场景中
|
||||
this.addObject(directionalLight1);
|
||||
this.addObject(directionalLight2);
|
||||
this.addObject(directionalLight3);
|
||||
this.addObject(ambientLight);
|
||||
}
|
||||
initRenderer() {
|
||||
super.initRenderer();
|
||||
// this.renderer.outputEncoding = THREE.sRGBEncoding
|
||||
}
|
||||
loop() {
|
||||
this.animationStop = window.requestAnimationFrame(() => {
|
||||
this.loop();
|
||||
});
|
||||
// 检查渲染器是否存在
|
||||
if (!this.renderer || !this.scene || !this.camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查渲染器尺寸是否有效(放宽检查条件)
|
||||
if (this.renderer.domElement && (this.renderer.domElement.width === 0 || this.renderer.domElement.height === 0)) {
|
||||
// 只在连续多帧都是0尺寸时才跳过渲染
|
||||
if (!this.zeroSizeFrameCount) this.zeroSizeFrameCount = 0;
|
||||
this.zeroSizeFrameCount++;
|
||||
if (this.zeroSizeFrameCount > 10) {
|
||||
// console.warn('渲染器canvas尺寸持续为0,跳过渲染');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.zeroSizeFrameCount = 0;
|
||||
}
|
||||
|
||||
// 检查CSS2D渲染器是否有效(只检查是否存在,不检查尺寸)
|
||||
// CSS2DRenderer的domElement可能不会有正确的offsetWidth/offsetHeight
|
||||
|
||||
// 这里是你自己业务上需要的code
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
// 控制相机旋转缩放的更新
|
||||
if (this.options.controls.visibel && this.controls) {
|
||||
this.controls.update();
|
||||
}
|
||||
// 统计更新 - 添加更严格的检查
|
||||
if (this.options.statsVisibel && this.stats && this.stats.dom) {
|
||||
// 检查Stats内部canvas是否有效
|
||||
const canvas = this.stats.dom.querySelector('canvas');
|
||||
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
||||
this.stats.update();
|
||||
}
|
||||
}
|
||||
if (this.rotatingApertureMesh) {
|
||||
this.rotatingApertureMesh.rotation.z += 0.0005;
|
||||
}
|
||||
if (this.rotatingPointMesh) {
|
||||
this.rotatingPointMesh.rotation.z -= 0.0005;
|
||||
}
|
||||
// 渲染标签 - 使用CSS2D渲染器
|
||||
if (this.css2dRender && this.scene && this.camera) {
|
||||
this.css2dRender.render(this.scene, this.camera);
|
||||
// 每100帧输出一次调试信息
|
||||
if (this.frameCount === undefined) this.frameCount = 0;
|
||||
this.frameCount++;
|
||||
if (this.frameCount % 100 === 0) {
|
||||
console.log('CSS2D渲染器状态:', {
|
||||
renderer: !!this.css2dRender,
|
||||
scene: !!this.scene,
|
||||
camera: !!this.camera,
|
||||
sceneChildren: this.scene.children.length
|
||||
});
|
||||
}
|
||||
}
|
||||
// 粒子上升
|
||||
if (this.particleArr.length) {
|
||||
for (let i = 0; i < this.particleArr.length; i++) {
|
||||
this.particleArr[i].updateSequenceFrame();
|
||||
this.particleArr[i].position.z += 0.01;
|
||||
if (this.particleArr[i].position.z >= 6) {
|
||||
this.particleArr[i].position.z = -6;
|
||||
}
|
||||
}
|
||||
}
|
||||
TWEEN.update();
|
||||
}
|
||||
resize() {
|
||||
super.resize();
|
||||
|
||||
// 确保尺寸有效
|
||||
const validWidth = Math.max(this.options.width || 800, 800);
|
||||
const validHeight = Math.max(this.options.height || 600, 600);
|
||||
|
||||
// 更新options中的尺寸
|
||||
this.options.width = validWidth;
|
||||
this.options.height = validHeight;
|
||||
|
||||
// 确保渲染器已准备就绪再执行渲染
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
// 重新设置渲染器尺寸
|
||||
this.renderer.setSize(validWidth, validHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
// 这里是你自己业务上需要的code
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
if (this.css2dRender) {
|
||||
this.css2dRender.setSize(validWidth, validHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log('开始创建Earth实例...');
|
||||
baseEarth = new CurrentEarth({
|
||||
container: '#app-32-map',
|
||||
axesVisibel: false,
|
||||
controls: {
|
||||
enableDamping: true, // 阻尼
|
||||
maxPolarAngle: (Math.PI / 2) * 0.98,
|
||||
},
|
||||
});
|
||||
// console.log('Earth实例创建完成,开始运行...');
|
||||
baseEarth.run();
|
||||
// console.log('Earth实例运行完成');
|
||||
|
||||
// 将CSS2D渲染器赋值给baseEarth实例,确保渲染循环中能正确访问
|
||||
if (baseEarth && baseEarth.css2dRender) {
|
||||
// CSS2D渲染器已经在CurrentEarth类中初始化了
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 初始化养殖场标记
|
||||
console.log('准备调用initFarmMarkers,mapGroup:', baseEarth.mapGroup);
|
||||
initFarmMarkers(baseEarth.mapGroup);
|
||||
console.log('initFarmMarkers调用完成,mapGroup子对象数量:', baseEarth.mapGroup.children.length);
|
||||
|
||||
// 初始化养殖场点击事件监听器
|
||||
initFarmClickHandler();
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
return {
|
||||
showPopup,
|
||||
selectedFarmData,
|
||||
closePopup,
|
||||
initFarmMarkers
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (baseEarth) {
|
||||
// 清理Three.js资源
|
||||
if (baseEarth.renderer) {
|
||||
baseEarth.renderer.dispose();
|
||||
}
|
||||
if (baseEarth.scene) {
|
||||
baseEarth.scene.clear();
|
||||
}
|
||||
// 停止动画循环
|
||||
if (baseEarth.animationStop) {
|
||||
cancelAnimationFrame(baseEarth.animationStop);
|
||||
}
|
||||
baseEarth = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.map-3d-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#app-32-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.map-32-label {
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.farm-label {
|
||||
font-size: 16px;
|
||||
color: #00F6FF;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #00F6FF;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
display: block;
|
||||
visibility: visible !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user