Files
datav---Cattle-Industry/src/components/Map3D.vue

1437 lines
49 KiB
Vue
Raw Normal View History

<template>
<div class="map-3d-container">
<div id="app-32-map"></div>
<!-- 全国牛源地TOP5列表 -->
<div class="cattle-source-top5">
<div class="top5-header">
<h3>全国牛源地TOP3</h3>
</div>
<div class="top5-list">
<div
v-for="(item, index) in cattleSourceTop5"
:key="index"
class="top5-item"
>
<div class="rank">{{ index + 1 }}</div>
<div class="province">{{ item.province }}</div>
<div class="count">{{ item.count }}</div>
</div>
</div>
</div>
</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, onUnmounted, 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 = [...];
export default {
name: '3dMap30',
components: {
// FarmPopup // 移除弹窗组件注册
},
setup() {
let baseEarth = null;
// 移除养殖场标记相关变量
// let farmMarkers = []; // 存储养殖场标记
// let selectedFarm = null; // 当前选中的养殖场
// 移除弹窗状态管理
// const showPopup = ref(false);
// const selectedFarmData = ref(null);
// 全国牛源地TOP5数据
const cattleSourceTop5 = ref([
{ province: '广西', count: 760 },
{ province: '云南', count: 247 },
{ province: '贵州', count: 186 },
]);
// 重置
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.002, // 增加线宽,提高清晰度
transparent: true,
opacity: 0.8, // 增加不透明度
depthTest: false,
},
'Line2'
);
lineTop.position.z += 0.305;
let lineBottom = createCountryFlatLine(
data,
{
color: 0x61fbfd,
linewidth: 0.0025, // 增加底部线宽
transparent: true,
opacity: 0.9, // 增加底部线不透明度
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.32)); // 稍微提高z坐标确保边界线清晰可见
}
if (points.length > 1) {
// 创建线条几何体
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: color,
transparent: true,
opacity: 0.7, // 增加不透明度
linewidth: 2 // 增加线宽
});
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) => { ... };
// 移除养殖场点击事件处理函数
// const initFarmClickHandler = () => { ... };
// 移除弹窗相关函数
// const showFarmPopup = (farm) => { ... };
// const closePopup = () => { ... };
// 创建标签 - 优化省份名称标注位置和样式
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);
// 创建省份名称标签(使用优化的样式类)
var label = create2DTag(properties.name, 'province-name-label');
scene.add(label);
// 调整标签位置,避免与气泡标记重叠
label.show(properties.name, new THREE.Vector3(...labelCenter, 1.2));
// console.log(`标签创建并显示成功: ${properties.name}`);
};
// 省份牛品种和存栏量数据 - 扩展为所有省份
const provinceData = {
'北京市': {
breeds: ['荷斯坦牛', '西门塔尔牛', '安格斯牛'],
inventory: 45000,
color: 0x86FFFF
},
'天津市': {
breeds: ['荷斯坦牛', '西门塔尔牛'],
inventory: 32000,
color: 0x86FFFF
},
'河北省': {
breeds: ['西门塔尔牛', '安格斯牛', '夏洛莱牛', '荷斯坦牛'],
inventory: 1180000,
color: 0x86FFFF
},
'山西省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '晋南牛'],
inventory: 680000,
color: 0x86FFFF
},
'内蒙古自治区': {
breeds: ['西门塔尔牛', '安格斯牛', '夏洛莱牛', '利木赞牛'],
inventory: 1250000,
color: 0x86FFFF
},
'辽宁省': {
breeds: ['西门塔尔牛', '安格斯牛', '夏洛莱牛'],
inventory: 890000,
color: 0x86FFFF
},
'吉林省': {
breeds: ['西门塔尔牛', '安格斯牛', '延边牛'],
inventory: 750000,
color: 0x86FFFF
},
'黑龙江省': {
breeds: ['西门塔尔牛', '安格斯牛', '夏洛莱牛', '荷斯坦牛'],
inventory: 1380000,
color: 0x86FFFF
},
'上海市': {
breeds: ['荷斯坦牛', '西门塔尔牛'],
inventory: 28000,
color: 0x86FFFF
},
'江苏省': {
breeds: ['西门塔尔牛', '安格斯牛', '苏北牛'],
inventory: 520000,
color: 0x86FFFF
},
'浙江省': {
breeds: ['西门塔尔牛', '安格斯牛', '金华牛'],
inventory: 380000,
color: 0x86FFFF
},
'安徽省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '皖南牛'],
inventory: 650000,
color: 0x86FFFF
},
'福建省': {
breeds: ['西门塔尔牛', '安格斯牛', '闽南牛'],
inventory: 420000,
color: 0x86FFFF
},
'江西省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '赣州牛'],
inventory: 580000,
color: 0x86FFFF
},
'山东省': {
breeds: ['西门塔尔牛', '安格斯牛', '夏洛莱牛', '鲁西黄牛'],
inventory: 1420000,
color: 0x86FFFF
},
'河南省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '南阳牛'],
inventory: 1350000,
color: 0x86FFFF
},
'湖北省': {
breeds: ['西门塔尔牛', '安格斯牛', '江汉牛'],
inventory: 780000,
color: 0x86FFFF
},
'湖南省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '湘西牛'],
inventory: 820000,
color: 0x86FFFF
},
'广东省': {
breeds: ['西门塔尔牛', '安格斯牛', '雷州牛'],
inventory: 680000,
color: 0x86FFFF
},
'广西壮族自治区': {
breeds: ['西门塔尔牛', '安格斯牛', '德保矮马牛'],
inventory: 950000,
color: 0x86FFFF
},
'海南省': {
breeds: ['西门塔尔牛', '安格斯牛', '海南牛'],
inventory: 180000,
color: 0x86FFFF
},
'重庆市': {
breeds: ['西门塔尔牛', '夏洛莱牛', '川东牛'],
inventory: 450000,
color: 0x86FFFF
},
'四川省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '利木赞牛', '安格斯牛'],
inventory: 1150000,
color: 0x86FFFF
},
'贵州省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '关岭牛'],
inventory: 720000,
color: 0x86FFFF
},
'云南省': {
breeds: ['云南黄牛', '西门塔尔牛', '婆罗门牛', '安格斯牛'],
inventory: 890000,
color: 0x86FFFF
},
'西藏自治区': {
breeds: ['牦牛', '西门塔尔牛', '藏牛'],
inventory: 480000,
color: 0x86FFFF
},
'陕西省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '秦川牛'],
inventory: 680000,
color: 0x86FFFF
},
'甘肃省': {
breeds: ['西门塔尔牛', '夏洛莱牛', '利木赞牛', '秦川牛'],
inventory: 760000,
color: 0x86FFFF
},
'青海省': {
breeds: ['牦牛', '西门塔尔牛', '青海牛'],
inventory: 520000,
color: 0x86FFFF
},
'宁夏回族自治区': {
breeds: ['西门塔尔牛', '安格斯牛', '宁夏牛'],
inventory: 380000,
color: 0x86FFFF
},
'新疆维吾尔自治区': {
breeds: ['褐牛', '西门塔尔牛', '安格斯牛', '海福特牛'],
inventory: 980000,
color: 0x86FFFF
},
'台湾省': {
breeds: ['西门塔尔牛', '安格斯牛', '台湾牛'],
inventory: 120000,
color: 0x86FFFF
},
'香港特别行政区': {
breeds: ['进口牛', '西门塔尔牛'],
inventory: 5000,
color: 0x86FFFF
},
'澳门特别行政区': {
breeds: ['进口牛', '西门塔尔牛'],
inventory: 2000,
color: 0x86FFFF
}
};
// 创建省份气泡标记 - 修改为为所有省份创建标记点(排除港澳台)
const createProvinceBubble = (properties, scene) => {
const provinceName = properties.name;
// 排除香港、澳门、台湾
if (provinceName === '香港特别行政区' || provinceName === '澳门特别行政区' || provinceName === '台湾省') {
return;
}
// 为所有省份创建标记点,如果没有数据则使用默认值
const data = provinceData[provinceName] || {
breeds: ['西门塔尔牛', '安格斯牛'],
inventory: 100000,
color: 0x86FFFF
};
const center = properties.center || properties.centroid || properties.cp;
if (!center) {
console.warn(`省份 ${provinceName} 缺少坐标信息`);
return;
}
// 创建气泡几何体
const bubbleGeometry = new THREE.SphereGeometry(0.8, 16, 16);
const bubbleMaterial = new THREE.MeshBasicMaterial({
color: data.color,
transparent: true,
opacity: 0.8
});
const bubbleMesh = new THREE.Mesh(bubbleGeometry, bubbleMaterial);
bubbleMesh.position.set(center[0], center[1], 1.5);
bubbleMesh.name = `${provinceName}Bubble`;
bubbleMesh.userData = {
type: 'bubble',
province: provinceName,
cattleData: {
breeds: data.breeds,
inventory: data.inventory
}
};
scene.add(bubbleMesh);
// 为省份气泡添加名称标签
const provinceLabel = create2DTag(provinceName, 'province-label');
provinceLabel.show(provinceName, new THREE.Vector3(center[0], center[1], 2.5));
scene.add(provinceLabel);
// 添加气泡动画效果
const animateBubble = () => {
if (bubbleMesh) {
bubbleMesh.rotation.y += 0.01;
bubbleMesh.position.z = 1.5 + Math.sin(Date.now() * 0.003 + center[0] * 0.01) * 0.2;
}
};
// 将动画函数添加到全局动画循环中
if (!window.bubbleAnimations) {
window.bubbleAnimations = [];
}
window.bubbleAnimations.push(animateBubble);
console.log(`${provinceName}气泡标记和标签创建成功`);
};
// 创建悬浮信息提示框
const createTooltip = () => {
const tooltip = document.createElement('div');
tooltip.id = 'bubble-tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 8px;
font-size: 14px;
pointer-events: none;
z-index: 10001;
display: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid #84acf0;
max-width: 300px;
`;
document.body.appendChild(tooltip);
return tooltip;
};
// 鼠标交互处理
const initBubbleInteraction = (container, camera, scene) => {
const tooltip = createTooltip();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredBubble = null;
const onMouseMove = (event) => {
const rect = container.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 bubbles = scene.children.filter(child =>
child.userData && child.userData.type === 'bubble'
);
const intersects = raycaster.intersectObjects(bubbles);
if (intersects.length > 0) {
const bubble = intersects[0].object;
if (hoveredBubble !== bubble) {
// 重置之前悬浮的气泡
if (hoveredBubble) {
hoveredBubble.material.opacity = 0.8;
hoveredBubble.scale.set(1, 1, 1);
}
// 设置新悬浮的气泡
hoveredBubble = bubble;
bubble.material.opacity = 1.0;
bubble.scale.set(1.2, 1.2, 1.2);
// 显示提示框
const data = bubble.userData.cattleData;
tooltip.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px; color: #84acf0;">
${bubble.userData.province}
</div>
<div style="margin-bottom: 5px;">
<strong>主要牛品种</strong>
</div>
<div style="margin-bottom: 8px; padding-left: 10px;">
${data.breeds.join('、')}
</div>
<div>
<strong>存栏量</strong> ${data.inventory.toLocaleString()}
</div>
`;
tooltip.style.display = 'block';
}
// 更新提示框位置
tooltip.style.left = (event.clientX + 15) + 'px';
tooltip.style.top = (event.clientY - 10) + 'px';
} else {
// 没有悬浮在气泡上
if (hoveredBubble) {
hoveredBubble.material.opacity = 0.8;
hoveredBubble.scale.set(1, 1, 1);
hoveredBubble = null;
}
tooltip.style.display = 'none';
}
};
container.addEventListener('mousemove', onMouseMove);
// 返回清理函数
return () => {
container.removeEventListener('mousemove', onMouseMove);
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
};
};
onMounted(async () => {
console.log('=== Map3D组件已挂载 ===');
// console.log('farmData:', 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;
// 设置50°的透视相机最大化减少视野角度让地图显示最大
this.camera = new THREE.PerspectiveCamera(50, rate, 0.001, 90000000);
this.camera.up.set(0, 0, 1);
// 调整相机位置,让地图显示最大 - 使用更近的距离
this.camera.position.set(106.2581, 25.4681, 30); //相机在Three.js坐标系中的位置将在initModel中重新计算
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]);
// 为省份网格添加标识信息,用于悬停交互
mesh.userData = {
type: 'province',
name: properties.name,
originalMaterial: [topFaceMaterial, sideMaterial]
};
province.add(mesh);
});
});
// 设置省份组的名称和用户数据
province.name = `province_${properties.name}`;
province.userData = {
type: 'provinceGroup',
name: properties.name
};
this.mapGroup.add(province);
// 创建标点和标签
initLabel(properties, this.scene);
// 为每个省份创建气泡标记
createProvinceBubble(properties, this.scene);
});
// 创建上下边框
initBorderLine(provinceData, this.mapGroup);
// 创建各市区边界线
initCityBorderLines(provinceData, this.mapGroup);
let earthGroupBound = getBoundingBox(this.mapGroup);
// 初始化省份悬停交互功能
this.initProvinceHoverInteraction();
centerXY = [earthGroupBound.center.x, earthGroupBound.center.y];
let { size } = earthGroupBound;
let width = size.x < size.y ? size.y + 1 : size.x + 1;
// 计算合适的相机距离,让地图显示得更大一些
const mapSize = Math.max(size.x, size.y);
const cameraDistance = mapSize * 1.3; // 减少距离系数,让地图显示得更大
// 添加背景,修饰元素
// 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('粒子系统初始化完成');
// 初始化省份悬停交互功能
this.initProvinceHoverInteraction();
// 养殖场标记将在baseEarth.run()完成后初始化
// 更新相机目标到新的中心点
this.camera.lookAt(...centerXY, 0);
// 重新调整相机位置,让地图显示得更大
this.camera.position.set(centerXY[0], centerXY[1] - cameraDistance * 0.3, cameraDistance);
if (this.controls) {
this.controls.target.set(...centerXY, 0);
this.controls.object.position.copy(this.camera.position);
this.controls.update();
}
// 强制调整相机以确保地图完整显示
setTimeout(() => {
if (this.adjustCameraForFullView) {
this.adjustCameraForFullView();
}
}, 100);
initGui();
} catch (error) {
// console.log(error);
}
}
// 初始化省份悬停交互功能
initProvinceHoverInteraction() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredProvince = null;
// 创建高亮材质
const highlightTopMaterial = new THREE.MeshBasicMaterial({
color: 0x00d4ff,
transparent: true,
opacity: 0.8
});
const highlightSideMaterial = new THREE.MeshBasicMaterial({
color: 0x0099cc,
transparent: true,
opacity: 0.8
});
const onMouseMove = (event) => {
const rect = this.container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(this.mapGroup.children, true);
// 恢复之前高亮的省份
if (hoveredProvince) {
hoveredProvince.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = mesh.userData.originalMaterial;
}
});
hoveredProvince = null;
}
// 检查是否悬停在省份上
if (intersects.length > 0) {
const intersectedObject = intersects[0].object;
if (intersectedObject.userData.type === 'province') {
// 找到省份组
let provinceGroup = intersectedObject.parent;
while (provinceGroup && provinceGroup.userData.type !== 'provinceGroup') {
provinceGroup = provinceGroup.parent;
}
if (provinceGroup) {
hoveredProvince = provinceGroup;
// 高亮整个省份
provinceGroup.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = [highlightTopMaterial, highlightSideMaterial];
}
});
// 改变鼠标样式
this.container.style.cursor = 'pointer';
}
}
} else {
// 恢复默认鼠标样式
this.container.style.cursor = 'default';
}
};
// 添加事件监听器
this.container.addEventListener('mousemove', onMouseMove);
// 存储事件处理函数以便后续清理
this.provinceHoverHandler = onMouseMove;
}
getDataRenderMap() {}
destroy() {
// 清理省份悬停事件监听器
if (this.provinceHoverHandler && this.container) {
this.container.removeEventListener('mousemove', this.provinceHoverHandler);
}
}
initControls() {
super.initControls();
this.controls.target = new THREE.Vector3(...centerXY, 0);
// 设置控制器的初始距离,确保地图完整显示
this.controls.object.position.copy(this.camera.position);
this.controls.update();
// 添加自动调整相机位置的方法
this.adjustCameraForFullView = () => {
if (this.mapGroup && this.camera) {
const box = new THREE.Box3().setFromObject(this.mapGroup);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const distance = maxDim * 1.2; // 减少距离系数,让地图显示得更大
this.camera.position.set(center.x, center.y - distance * 0.3, distance);
this.camera.lookAt(center);
if (this.controls) {
this.controls.target.copy(center);
this.controls.object.position.copy(this.camera.position);
this.controls.update();
}
}
};
}
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;
}
}
}
// 执行气泡动画
if (window.bubbleAnimations) {
window.bubbleAnimations.forEach(animate => animate());
}
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,
minDistance: 2, // 最小缩放距离
maxDistance: 150, // 最大缩放距离
enableZoom: true, // 启用缩放
enableRotate: false, // 禁用旋转
enablePan: true, // 启用平移
},
});
// console.log('Earth实例创建完成开始运行...');
baseEarth.run();
// console.log('Earth实例运行完成');
// 将CSS2D渲染器赋值给baseEarth实例确保渲染循环中能正确访问
if (baseEarth && baseEarth.css2dRender) {
// CSS2D渲染器已经在CurrentEarth类中初始化了
}
// 移除养殖场标记初始化
// console.log('准备调用initFarmMarkersmapGroup:', baseEarth.mapGroup);
// initFarmMarkers(baseEarth.mapGroup);
// console.log('initFarmMarkers调用完成mapGroup子对象数量:', baseEarth.mapGroup.children.length);
// 在养殖场标记添加完成后,再次调整相机以确保完整显示
setTimeout(() => {
if (baseEarth && baseEarth.adjustCameraForFullView) {
baseEarth.adjustCameraForFullView();
}
}, 200);
// 移除养殖场点击事件监听器
// initFarmClickHandler();
// 初始化气泡交互功能
setTimeout(() => {
const container = document.getElementById('app-32-map');
if (container && baseEarth && baseEarth.camera && baseEarth.scene) {
const cleanupInteraction = initBubbleInteraction(container, baseEarth.camera, baseEarth.scene);
// 在组件卸载时清理事件监听器
onUnmounted(() => {
if (cleanupInteraction) {
cleanupInteraction();
}
});
}
}, 300);
window.addEventListener('resize', resize);
});
return {
// 移除弹窗相关返回值
// showPopup,
// selectedFarmData,
// closePopup,
// initFarmMarkers
cattleSourceTop5
};
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: 700px; /* 最大化增加最小高度 */
min-width: 900px; /* 最大化增加最小宽度 */
}
/* 响应式设计 - 适应不同屏幕尺寸 */
@media screen and (max-width: 1200px) {
.province-name-label {
font-size: 14px;
padding: 4px 8px;
min-width: 60px;
}
.province-label {
font-size: 11px;
padding: 2px 6px;
}
.cattle-source-top5 {
width: 180px;
padding: 8px;
}
.top5-header h3 {
font-size: 14px;
}
.top5-item {
padding: 6px 8px;
}
.top5-item .rank {
width: 18px;
height: 18px;
font-size: 9px;
}
.top5-item .province,
.top5-item .count {
font-size: 11px;
}
}
@media screen and (max-width: 768px) {
#app-32-map {
min-height: 500px;
min-width: 600px;
}
.province-name-label {
font-size: 12px;
padding: 3px 6px;
min-width: 50px;
}
.province-label {
font-size: 10px;
padding: 1px 4px;
}
.cattle-source-top5 {
width: 160px;
padding: 6px;
margin-left: 400px;
}
.top5-header h3 {
font-size: 12px;
}
.top5-item {
padding: 4px 6px;
}
.top5-item .rank {
width: 16px;
height: 16px;
font-size: 8px;
margin-right: 8px;
}
.top5-item .province,
.top5-item .count {
font-size: 10px;
}
}
/* 原有地图标签样式 */
.map-32-label {
font-size: 10px;
color: #fff;
z-index: 10;
position: relative;
}
/* 省份名称标签样式 - 优化显示效果和配色方案 */
.province-name-label {
font-size: 16px; /* 增大字号提高可读性 */
color: #ffffff;
font-weight: bold;
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.9), 0 0 10px rgba(0, 212, 255, 0.5); /* 增强文字阴影和发光效果 */
background: linear-gradient(135deg, rgba(0, 30, 60, 0.9), rgba(0, 50, 100, 0.8)); /* 渐变背景 */
padding: 6px 12px; /* 增加内边距 */
border-radius: 8px; /* 增大圆角 */
border: 2px solid rgba(0, 212, 255, 0.6); /* 增强边框 */
backdrop-filter: blur(8px); /* 增强背景模糊 */
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3); /* 添加阴影效果 */
z-index: 10;
position: relative;
pointer-events: none;
white-space: nowrap;
text-align: center;
min-width: 80px; /* 增加最小宽度 */
transition: all 0.3s ease; /* 添加过渡效果 */
}
/* 省份标签样式 - 气泡标记上的标签 */
.province-label {
font-size: 13px; /* 稍微增大字号 */
color: #00d4ff;
font-weight: bold;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.9), 2px 2px 4px rgba(0, 0, 0, 0.8); /* 增强阴影效果 */
background: linear-gradient(135deg, rgba(0, 30, 60, 0.8), rgba(0, 50, 100, 0.7)); /* 渐变背景 */
padding: 3px 8px; /* 增加内边距 */
border-radius: 6px; /* 增大圆角 */
border: 1px solid rgba(0, 212, 255, 0.7); /* 增强边框 */
backdrop-filter: blur(6px); /* 增强背景模糊 */
box-shadow: 0 2px 10px rgba(0, 212, 255, 0.4); /* 添加阴影效果 */
z-index: 10;
position: relative;
pointer-events: none;
transition: all 0.3s ease; /* 添加过渡效果 */
}
/* 全国牛源地TOP5列表样式 - 优化配色方案 */
.cattle-source-top5 {
position: absolute;
bottom:80px; /* 定位到底部 */
left: 20px; /* 定位到左侧 */
width: 220px;
background: linear-gradient(135deg, rgba(0, 30, 60, 0.95), rgba(0, 50, 100, 0.9)); /* 增强背景不透明度 */
border: 2px solid rgba(0, 212, 255, 0.8); /* 增强边框 */
border-radius: 8px; /* 增大圆角 */
padding: 12px; /* 增加内边距 */
box-shadow: 0 6px 25px rgba(0, 212, 255, 0.4); /* 增强阴影效果 */
backdrop-filter: blur(12px); /* 增强背景模糊 */
z-index: 1000;
margin-left: 600px;
}
.top5-header {
margin-bottom: 12px; /* 增加底部边距 */
text-align: center;
}
.top5-header h3 {
color: #00d4ff;
font-size: 16px; /* 增大字号 */
font-weight: bold;
margin: 0;
text-shadow: 0 0 12px rgba(0, 212, 255, 0.9), 2px 2px 4px rgba(0, 0, 0, 0.8); /* 增强文字效果 */
}
.top5-list {
display: flex;
flex-direction: column;
gap: 8px; /* 增加间距 */
}
.top5-item {
display: flex;
align-items: center;
padding: 8px 12px; /* 增加内边距 */
background: rgba(0, 100, 200, 0.3); /* 增加背景不透明度 */
border: 1px solid rgba(0, 212, 255, 0.5); /* 增强边框 */
border-radius: 6px; /* 增大圆角 */
transition: all 0.3s ease;
}
.top5-item:hover {
background: rgba(0, 212, 255, 0.4); /* 增强悬停效果 */
border-color: rgba(0, 255, 255, 0.8);
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3); /* 添加悬停阴影 */
}
.top5-item .rank {
width: 22px; /* 增大尺寸 */
height: 22px;
background: linear-gradient(135deg, #00d4ff, #0099cc);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px; /* 增大字号 */
font-weight: bold;
margin-right: 12px; /* 增加右边距 */
box-shadow: 0 3px 10px rgba(0, 212, 255, 0.5); /* 增强阴影 */
}
.top5-item .province {
flex: 1;
color: #ffffff;
font-size: 13px; /* 增大字号 */
font-weight: 500;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7); /* 增强文字阴影 */
}
.top5-item .count {
color: #00d4ff;
font-size: 13px; /* 增大字号 */
font-weight: bold;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.8), 1px 1px 3px rgba(0, 0, 0, 0.7); /* 增强文字效果 */
}
/* 移除养殖场标签样式 */
/* .farm-label { ... } */
</style>