Files
datav---Cattle-Industry/src/components/Map3D.vue
2025-11-26 17:31:42 +08:00

2075 lines
76 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 // 移除弹窗组件注册
},
emits: ['province-click'], // 添加省份点击事件
setup(props, { emit }) {
let baseEarth = null;
// 移除养殖场标记相关变量
// let farmMarkers = []; // 存储养殖场标记
// let selectedFarm = null; // 当前选中的养殖场
// 移除弹窗状态管理
// const showPopup = ref(false);
// const selectedFarmData = ref(null);
// 全国牛源地TOP3数据单位
const cattleSourceTop5 = ref([
{ province: '内蒙古', count: 1032 },
{ province: '新疆', count: 849 },
{ province: '四川', count: 811 },
]);
// 重置
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 = () => { ... };
// 创建标签 - 优化省份名称标注位置和样式
// 标签注册表名称到CSS2DObject的映射
const labelRegistry = new Map();
const createdLabels = [];
// 全局标签缩放因子:统一缩小标签字号(例如 0.85 = 缩小15%
const LABEL_SIZE_FACTOR = 0.85;
// 省份简称映射(默认取首字作为后备)
const provinceAbbrMap = {
'北京市': '京', '天津市': '津', '上海市': '沪', '重庆市': '渝',
'河北省': '冀', '山西省': '晋', '辽宁省': '辽', '吉林省': '吉', '黑龙江省': '黑',
'江苏省': '苏', '浙江省': '浙', '安徽省': '皖', '福建省': '闽', '江西省': '赣',
'山东省': '鲁', '河南省': '豫', '湖北省': '鄂', '湖南省': '湘', '广东省': '粤',
'海南省': '琼', '四川省': '川', '贵州省': '黔', '云南省': '滇', '陕西省': '陕',
'甘肃省': '甘', '青海省': '青', '内蒙古自治区': '蒙', '广西壮族自治区': '桂',
'西藏自治区': '藏', '宁夏回族自治区': '宁', '新疆维吾尔自治区': '新',
'香港特别行政区': '港', '澳门特别行政区': '澳', '台湾省': '台'
};
const initLabel = (properties, scene, provinceObj = null) => {
// 标签地理中心(优先使用数据中心,缺失时回退到包围盒中心)
const centerCandidate = properties.center || properties.centroid || properties.cp;
let posVec = null;
if (Array.isArray(centerCandidate)) {
posVec = new THREE.Vector3(centerCandidate[0], centerCandidate[1], 1.2);
} else if (centerCandidate && typeof centerCandidate.x === 'number' && typeof centerCandidate.y === 'number') {
posVec = new THREE.Vector3(centerCandidate.x, centerCandidate.y, 1.2);
} else if (provinceObj) {
try {
const boxForCenter = new THREE.Box3().setFromObject(provinceObj);
const center = boxForCenter.getCenter(new THREE.Vector3());
posVec = new THREE.Vector3(center.x, center.y, 1.2);
} catch (e) {
return false;
}
} else {
return false;
}
// 创建省份名称标签(默认只显示简称/图标,悬停显示全称)
const fullName = properties.name;
const abbr = provinceAbbrMap[fullName] || (typeof fullName === 'string' ? fullName[0] : '');
const initialHtml = `<span class="abbr">${abbr}</span><span class="full">${fullName}</span>`;
const label = create2DTag(initialHtml, 'province-name-label');
// 字体与可读性增强
label.element.style.fontFamily = "Microsoft YaHei, SimHei, Arial, sans-serif";
label.element.style.webkitTextStroke = '0.8px rgba(0,0,0,0.6)';
label.element.style.textShadow = '1px 1px 2px rgba(0,0,0,0.6), 0 0 6px rgba(0, 212, 255, 0.5)';
// 基于省份面积自适应字号
let fontSizePx = 14; // 默认字号
if (provinceObj) {
const box = new THREE.Box3().setFromObject(provinceObj);
const size = box.getSize(new THREE.Vector3());
const areaApprox = size.x * size.y;
// 将面积映射到字号范围 [12, 22]
const minArea = 20; // 小省份的近似面积阈值
const maxArea = 600; // 大省份的近似面积阈值
const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
const t = clamp((areaApprox - minArea) / (maxArea - minArea), 0, 1);
fontSizePx = Math.round(12 + t * (22 - 12));
}
// 应用全局缩放因子,统一缩小标签
fontSizePx = Math.max(10, Math.round(fontSizePx * LABEL_SIZE_FACTOR));
label.element.style.fontSize = fontSizePx + 'px';
scene.add(label);
// 初始位置:略微抬高避免与边界贴合
// 默认显示简称内容(已在 initialHtml 中),设置坐标
label.show(initialHtml, posVec.clone());
// 青海省标签向左微调,避免与区域重叠
if (fullName === '青海省') {
const pos = posVec.clone();
pos.x -= 2; // 微调量,可根据实际效果再行调整
label.position.copy(pos);
}
// 注册标签用于交互联动与碰撞处理
labelRegistry.set(properties.name, label);
createdLabels.push({
name: properties.name,
label,
center: posVec.clone(),
fontSizePx
});
};
// 省份牛品种和存栏量数据 - 扩展为所有省份
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 center = properties.center || properties.centroid || properties.cp;
if (!center) {
console.warn(`省份 ${provinceName} 缺少坐标信息`);
return;
}
// 此处不再创建任何 Three.js 圆圈或光柱,仅依赖上方 initLabel 创建的省份名称标签
// 如果需要在部分省份单独微调位置,可在此添加偏移逻辑:
// 例如:
// const label = labelRegistry.get(provinceName);
// if (label) label.position.set(center[0] + dx, center[1] + dy, 1.2);
};
// 创建悬浮信息提示框
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, province);
// 为每个省份创建气泡标记
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.baseCameraDistance = cameraDistance;
// 添加背景,修饰元素
// this.rotatingApertureMesh = initRotatingAperture(this.scene, width); // 注释掉旋转光圈
// this.rotatingPointMesh = initRotatingPoint(this.scene, width - 2); // 注释掉旋转点
// initCirclePoint(this.scene, width); // 注释掉圆形点
initSceneBg(this.scene, width);
// 解决标签重叠并根据初始缩放调整标签大小
setTimeout(() => {
this.resolveLabelCollisions();
this.updateLabelScale();
}, 100);
// 将组添加到场景中
// 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();
// 监听缩放与旋转变化,动态缩放标签
if (this.controls) {
this.controls.addEventListener('change', () => {
this.updateLabelScale();
// 缩放/旋转时也重新做简单的重叠修正
this.resolveLabelCollisions(true);
});
}
// 窗口尺寸变化时,重算标签缩放与轻量碰撞
window.addEventListener('resize', () => {
setTimeout(() => {
this.updateLabelScale();
this.resolveLabelCollisions(true);
}, 100);
});
// 初始化地图拖拽平移交互PC与移动端
this.initDragPan(earthGroupBound);
// 养殖场标记将在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);
}
}
// 根据相机距离动态调整标签缩放
updateLabelScale() {
try {
if (!this.camera || createdLabels.length === 0) return;
const camPos = this.camera.position.clone();
const center = new THREE.Vector3(...centerXY, 0);
const dist = camPos.distanceTo(center);
const base = this.baseCameraDistance || dist;
// 相机越近,标签越小;相机越远,标签越大(做轻微反向缩放)
const scale = Math.max(0.75, Math.min(1.4, base / dist));
createdLabels.forEach(({ label, fontSizePx }) => {
const size = Math.round(fontSizePx * scale);
label.element.style.fontSize = size + 'px';
});
} catch (e) {}
}
// 粗略的标签碰撞检测与位置微调
resolveLabelCollisions(isRealtime = false) {
if (!this.camera || !this.renderer || createdLabels.length === 0) return;
const rects = [];
const toScreen = (pos) => {
const p = pos.clone().project(this.camera);
const w = this.renderer.domElement.width || this.options.width;
const h = this.renderer.domElement.height || this.options.height;
return {
x: (p.x + 1) * 0.5 * w,
y: (1 - (p.y + 1) * 0.5) * h
};
};
// 采集当前屏幕矩形
createdLabels.forEach(({ label, center, name }) => {
const screen = toScreen(center);
const bbox = label.element.getBoundingClientRect();
rects.push({
name,
label,
center,
x: screen.x - bbox.width / 2,
y: screen.y - bbox.height / 2,
w: bbox.width,
h: bbox.height
});
});
// 简单冲突检测:若重叠则按网格步进偏移
const moved = new Set();
const step = isRealtime ? 0.15 : 0.3; // 实时偏移更小
for (let i = 0; i < rects.length; i++) {
for (let j = i + 1; j < rects.length; j++) {
const a = rects[i];
const b = rects[j];
const overlap = !(a.x + a.w < b.x || b.x + b.w < a.x || a.y + a.h < b.y || b.y + b.h < a.y);
if (overlap) {
// 沿着x方向微调一个向左一个向右
if (!moved.has(a.name)) {
a.center.x -= step;
a.label.position.copy(a.center);
moved.add(a.name);
}
if (!moved.has(b.name)) {
b.center.x += step;
b.label.position.copy(b.center);
moved.add(b.name);
}
}
}
}
}
// 初始化省份悬停和点击交互功能
initProvinceHoverInteraction() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredProvince = null;
// 检测设备类型
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// 创建高亮材质
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 clickHighlightTopMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff88,
transparent: true,
opacity: 0.9
});
const clickHighlightSideMaterial = new THREE.MeshBasicMaterial({
color: 0x00cc66,
transparent: true,
opacity: 0.9
});
// 获取鼠标/触摸位置的通用函数
const getPointerPosition = (event) => {
const rect = this.container.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
// 触摸事件
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
// 鼠标事件
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: ((clientX - rect.left) / rect.width) * 2 - 1,
y: -((clientY - rect.top) / rect.height) * 2 + 1,
clientX,
clientY
};
};
// 处理省份交互的通用函数
const handleProvinceInteraction = (event, isClick = false) => {
const pointer = getPointerPosition(event);
mouse.x = pointer.x;
mouse.y = pointer.y;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(this.mapGroup.children, true);
if (!isClick) {
// 悬停处理:恢复之前高亮的省份
if (hoveredProvince) {
hoveredProvince.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = mesh.userData.originalMaterial;
}
});
// 恢复标签样式
if (hoveredProvince.userData && hoveredProvince.userData.name) {
const prevLabel = labelRegistry.get(hoveredProvince.userData.name);
if (prevLabel) prevLabel.element.classList.remove('province-name-label-hover');
}
hoveredProvince = null;
}
}
// 检查是否悬停/点击在省份上
if (intersects.length > 0) {
// 选择首个命中省份网格,避免边界线/其他对象抢占命中
let intersectedObject = null;
for (let i = 0; i < intersects.length; i++) {
const obj = intersects[i].object;
if (obj && obj.userData && obj.userData.type === 'province') {
intersectedObject = obj;
break;
}
}
if (intersectedObject && intersectedObject.userData.type === 'province') {
// 找到省份组
let provinceGroup = intersectedObject.parent;
while (provinceGroup && provinceGroup.userData.type !== 'provinceGroup') {
provinceGroup = provinceGroup.parent;
}
if (provinceGroup) {
if (isClick && provinceGroup.userData.name) {
// 点击处理
const provinceName = provinceGroup.userData.name;
// 添加点击视觉反馈效果
provinceGroup.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = [clickHighlightTopMaterial, clickHighlightSideMaterial];
// 0.3秒后恢复原始材质
setTimeout(() => {
mesh.material = mesh.userData.originalMaterial;
}, 300);
}
});
// 创建点击反馈提示
this.showClickFeedback({ clientX: pointer.clientX, clientY: pointer.clientY }, provinceName);
// 触发省份点击事件
emit('province-click', provinceName);
console.log(`点击了省份: ${provinceName}`);
} else if (!isClick) {
// 悬停处理
hoveredProvince = provinceGroup;
// 高亮整个省份
provinceGroup.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = [highlightTopMaterial, highlightSideMaterial];
}
});
// 高亮对应标签
const label = labelRegistry.get(provinceGroup.userData.name);
if (label) {
label.element.classList.add('province-name-label-hover');
// 悬停时保证字号不低于14px
try {
const currentSize = parseInt(window.getComputedStyle(label.element).fontSize) || 14;
if (currentSize < 14) {
label.element.style.fontSize = '14px';
}
} catch (e) {}
}
// 改变鼠标样式(仅桌面端)
if (!isMobile) {
this.container.style.cursor = 'pointer';
}
}
}
}
} else if (!isClick) {
// 恢复默认鼠标样式(仅桌面端)
if (!isMobile) {
this.container.style.cursor = 'default';
}
// 清除标签高亮
if (hoveredProvince && hoveredProvince.userData && hoveredProvince.userData.name) {
const label = labelRegistry.get(hoveredProvince.userData.name);
if (label) {
label.element.classList.remove('province-name-label-hover');
}
}
}
};
// 鼠标移动事件(桌面端)
const onMouseMove = (event) => {
if (!isMobile) {
handleProvinceInteraction(event, false);
}
};
// 点击/触摸事件(桌面端和移动端)
const onPointerClick = (event) => {
// 防止事件冒泡
event.preventDefault();
event.stopPropagation();
handleProvinceInteraction(event, true);
};
// 触摸开始事件(移动端长按触发悬浮效果)
let touchHoverTimer = null;
const onTouchStart = (event) => {
if (isMobile) {
clearTimeout(touchHoverTimer);
const startEvent = event;
touchHoverTimer = setTimeout(() => {
handleProvinceInteraction(startEvent, false);
}, 500);
}
};
// 触摸结束事件(移动端清除悬停效果)
const onTouchEnd = (event) => {
if (isMobile) {
clearTimeout(touchHoverTimer);
}
if (isMobile && hoveredProvince) {
hoveredProvince.children.forEach(mesh => {
if (mesh.userData.type === 'province') {
mesh.material = mesh.userData.originalMaterial;
}
});
const label = labelRegistry.get(hoveredProvince.userData.name);
if (label) label.element.classList.remove('province-name-label-hover');
hoveredProvince = null;
}
};
// 添加事件监听器
if (!isMobile) {
// 桌面端事件
this.container.addEventListener('mousemove', onMouseMove);
this.container.addEventListener('click', onPointerClick);
} else {
// 移动端事件
this.container.addEventListener('touchstart', onTouchStart, { passive: false });
this.container.addEventListener('touchend', onTouchEnd, { passive: false });
this.container.addEventListener('touchstart', onPointerClick, { passive: false });
}
// 存储事件处理函数以便后续清理
this.provinceHoverHandler = onMouseMove;
this.provinceClickHandler = onPointerClick;
this.provinceTouchStartHandler = onTouchStart;
this.provinceTouchEndHandler = onTouchEnd;
this.isMobileDevice = isMobile;
}
// 显示点击反馈提示
showClickFeedback(event, provinceName) {
const feedback = document.createElement('div');
feedback.className = 'province-click-feedback';
feedback.textContent = `正在跳转到 ${provinceName} 预警监测`;
// 设置位置
feedback.style.left = `${event.clientX - 100}px`;
feedback.style.top = `${event.clientY - 50}px`;
document.body.appendChild(feedback);
// 0.6秒后移除
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 600);
}
getDataRenderMap() {}
destroy() {
// 清理省份悬停和点击事件监听器
if (this.container) {
if (this.provinceHoverHandler) {
this.container.removeEventListener('mousemove', this.provinceHoverHandler);
}
if (this.provinceClickHandler) {
this.container.removeEventListener('click', this.provinceClickHandler);
}
if (this.provinceTouchStartHandler) {
this.container.removeEventListener('touchstart', this.provinceTouchStartHandler);
}
if (this.provinceTouchEndHandler) {
this.container.removeEventListener('touchend', this.provinceTouchEndHandler);
}
}
}
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
}
// 地图拖拽平移交互:支持鼠标与触摸,阻尼与惯性、边界弹性
initDragPan(groupBound) {
const container = this.container;
if (!container || !this.controls || !this.renderer) return;
const size = groupBound.size;
const center = new THREE.Vector3(...centerXY, 0);
// 平移边界(以中心为基准的最大偏移量)
const maxOffsetX = size.x * 0.35; // 可根据视觉需求微调
const maxOffsetY = size.y * 0.35;
const elasticRatio = 0.1; // 10% 过拖弹性区域
const elasticX = maxOffsetX * (1 + elasticRatio);
const elasticY = maxOffsetY * (1 + elasticRatio);
// 拖拽状态
this.panState = {
dragging: false,
lastPointer: { x: 0, y: 0 },
velocity: new THREE.Vector2(0, 0),
friction: 0.92,
spring: 0.08,
maxOffsetX,
maxOffsetY,
elasticX,
elasticY,
trailCanvas: null,
trailCtx: null,
trailAlpha: 0.8
};
// 轨迹反馈画布
const setupTrail = () => {
if (this.panState.trailCanvas) return;
const cvs = document.createElement('canvas');
cvs.width = container.clientWidth || this.options.width;
cvs.height = container.clientHeight || this.options.height;
cvs.style.position = 'absolute';
cvs.style.left = '0';
cvs.style.top = '0';
cvs.style.zIndex = '9999';
cvs.style.pointerEvents = 'none';
container.appendChild(cvs);
this.panState.trailCanvas = cvs;
this.panState.trailCtx = cvs.getContext('2d');
this.panState.trailCtx.strokeStyle = 'rgba(0, 212, 255, 0.6)';
this.panState.trailCtx.lineWidth = 2;
this.panState.trailCtx.lineJoin = 'round';
this.panState.trailCtx.lineCap = 'round';
};
const resizeTrail = () => {
if (!this.panState.trailCanvas) return;
this.panState.trailCanvas.width = container.clientWidth || this.options.width;
this.panState.trailCanvas.height = container.clientHeight || this.options.height;
};
window.addEventListener('resize', resizeTrail);
// 屏幕像素位移 -> 世界坐标位移基于相机距离与FOV近似计算
const screenDeltaToWorld = (dx, dy) => {
const dist = this.camera.position.distanceTo(center);
const fovRad = (this.camera.fov || 45) * Math.PI / 180;
const viewHeight = 2 * Math.tan(fovRad / 2) * dist;
const canvasH = this.renderer.domElement.height || this.options.height;
const canvasW = this.renderer.domElement.width || this.options.width;
const worldPerPixelY = viewHeight / canvasH;
const worldPerPixelX = worldPerPixelY * (canvasW / canvasH);
return new THREE.Vector2(dx * worldPerPixelX, -dy * worldPerPixelY);
};
// 应用平移并做边界与弹性处理
const applyPan = (deltaWorld) => {
const target = this.controls.target;
const cam = this.controls.object;
target.x += deltaWorld.x; target.y += deltaWorld.y;
cam.position.x += deltaWorld.x; cam.position.y += deltaWorld.y;
// 边界与弹性:在弹性范围外施加弹簧回弹
const ox = target.x - center.x;
const oy = target.y - center.y;
// X轴
if (Math.abs(ox) > elasticX) {
const sign = Math.sign(ox);
const desired = center.x + sign * elasticX;
const back = (desired - target.x) * this.panState.spring;
target.x += back; cam.position.x += back;
this.panState.velocity.x *= 0.8;
} else if (Math.abs(ox) > maxOffsetX) {
const sign = Math.sign(ox);
const desired = center.x + sign * maxOffsetX;
const back = (desired - target.x) * this.panState.spring;
target.x += back; cam.position.x += back;
}
// Y轴
if (Math.abs(oy) > elasticY) {
const sign = Math.sign(oy);
const desired = center.y + sign * elasticY;
const back = (desired - target.y) * this.panState.spring;
target.y += back; cam.position.y += back;
this.panState.velocity.y *= 0.8;
} else if (Math.abs(oy) > maxOffsetY) {
const sign = Math.sign(oy);
const desired = center.y + sign * maxOffsetY;
const back = (desired - target.y) * this.panState.spring;
target.y += back; cam.position.y += back;
}
};
// 暴露给渲染循环使用
this.applyPan = applyPan;
// 事件绑定
const onPointerDown = (e) => {
this.panState.dragging = true;
if (this.controls) this.controls.enabled = false; // 避免与OrbitControls冲突
setupTrail();
const p = ('touches' in e) ? e.touches[0] : e;
this.panState.lastPointer.x = p.clientX;
this.panState.lastPointer.y = p.clientY;
if (this.panState.trailCtx) {
const ctx = this.panState.trailCtx;
ctx.clearRect(0,0,this.panState.trailCanvas.width,this.panState.trailCanvas.height);
ctx.beginPath(); ctx.moveTo(p.clientX, p.clientY);
}
};
const onPointerMove = (e) => {
if (!this.panState.dragging) return;
if ('touches' in e) { try { e.preventDefault(); } catch(err){} }
const p = ('touches' in e) ? e.touches[0] : e;
const dx = p.clientX - this.panState.lastPointer.x;
const dy = p.clientY - this.panState.lastPointer.y;
this.panState.lastPointer.x = p.clientX;
this.panState.lastPointer.y = p.clientY;
const deltaWorld = screenDeltaToWorld(dx, dy);
this.applyPan(deltaWorld);
// 更新速度(用于惯性)
this.panState.velocity.copy(deltaWorld);
// 轨迹绘制
if (this.panState.trailCtx) {
const ctx = this.panState.trailCtx;
ctx.lineTo(p.clientX, p.clientY); ctx.stroke();
}
};
const onPointerUp = () => {
this.panState.dragging = false;
if (this.controls) this.controls.enabled = true;
// 轨迹在惯性阶段逐渐淡出
this.panState.trailAlpha = 0.8;
};
// 绑定PC与移动端事件
container.addEventListener('mousedown', onPointerDown);
window.addEventListener('mousemove', onPointerMove);
window.addEventListener('mouseup', onPointerUp);
container.addEventListener('touchstart', onPointerDown, { passive: false });
window.addEventListener('touchmove', onPointerMove, { passive: false });
window.addEventListener('touchend', onPointerUp, { passive: false });
// 保存以便销毁
this.dragPanHandlers = { onPointerDown, onPointerMove, onPointerUp, resizeTrail };
}
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.panState && !this.panState.dragging) {
// 应用速度
if (Math.abs(this.panState.velocity.x) > 1e-5 || Math.abs(this.panState.velocity.y) > 1e-5) {
this.applyPan(this.panState.velocity.clone());
// 阻尼
this.panState.velocity.multiplyScalar(this.panState.friction);
// 速度阈值
if (this.panState.velocity.length() < 1e-4) {
this.panState.velocity.set(0,0);
}
}
// 轨迹淡出并清除
if (this.panState.trailCtx && this.panState.trailAlpha > 0) {
const ctx = this.panState.trailCtx;
ctx.globalAlpha = this.panState.trailAlpha;
ctx.fillStyle = 'rgba(0,0,0,0.06)';
ctx.fillRect(0,0,this.panState.trailCanvas.width,this.panState.trailCanvas.height);
ctx.globalAlpha = 1.0;
this.panState.trailAlpha -= 0.05;
if (this.panState.trailAlpha <= 0) {
ctx.clearRect(0,0,this.panState.trailCanvas.width,this.panState.trailCanvas.height);
}
}
}
// 粒子上升
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实例运行完成');
// 重新挂载/初始化 CSS2DRendererinitRenderer 会清空容器,需要在其后恢复)
setTimeout(() => {
const containerEl = document.getElementById('app-32-map');
if (baseEarth && containerEl) {
if (baseEarth.css2dRender && baseEarth.css2dRender.domElement) {
// 重新设置尺寸与样式并挂载回容器
const w = Math.max(baseEarth.options.width || containerEl.offsetWidth || 800, 800);
const h = Math.max(baseEarth.options.height || containerEl.offsetHeight || 600, 600);
baseEarth.css2dRender.setSize(w, h);
baseEarth.css2dRender.domElement.style.position = 'absolute';
baseEarth.css2dRender.domElement.style.left = '0px';
baseEarth.css2dRender.domElement.style.top = '0px';
baseEarth.css2dRender.domElement.style.zIndex = '10000';
baseEarth.css2dRender.domElement.style.pointerEvents = 'none';
baseEarth.css2dRender.domElement.style.overflow = 'visible';
containerEl.appendChild(baseEarth.css2dRender.domElement);
} else {
// 若不存在则重新创建
baseEarth.css2dRender = initCSS2DRender({
width: Math.max(containerEl.offsetWidth || 800, 800),
height: Math.max(containerEl.offsetHeight || 600, 600)
}, containerEl);
baseEarth.css2dRender.domElement.style.zIndex = '10000';
baseEarth.css2dRender.domElement.style.pointerEvents = 'none';
}
}
}, 300);
// 移除养殖场标记初始化
// 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 {
position: fixed;
top: 80px; /* 紧贴顶部标题栏,高度与 .dashboard-header 保持一致 */
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: calc(100vh - 80px);
overflow: hidden;
z-index: 0; /* 低于侧边栏与标题 */
}
#app-32-map {
width: 100%;
height: 100%;
}
/* 响应式设计 - 适应不同屏幕尺寸 */
@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 {
width: 100%;
height: 100%;
}
.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: 14px; /* 默认字号,悬停时提升 */
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: 0; /* 默认隐藏:不显示标签内容,悬停时再展开 */
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: 0; /* 简称状态下按内容宽度渲染(一个字宽度) */
transition: opacity 0.25s ease, transform 0.25s ease, color 0.25s ease, padding 0.25s ease, min-width 0.25s ease; /* 添加过渡效果 */
opacity: 0; /* 默认不显示标签 */
transform: scale(0.96); /* 默认略缩小,悬停时放大 */
}
/* 默认仅显示简称,完整名称隐藏 */
.province-name-label .abbr {
opacity: 1;
display: inline-block;
transition: opacity 0.3s ease, transform 0.3s ease;
/* 在默认字体基础上缩小1px保持清晰可读 */
font-size: calc(1em - 1px);
transform: scale(0.92);
line-height: 1.1;
}
.province-name-label .full {
opacity: 0;
display: block; /* 默认不参与布局宽度计算 */
position: absolute; /* 绝对定位,避免影响容器宽度 */
left: 0;
top: 0;
transform: translateY(2px) scale(0.98);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
/* 省份名称标签悬停高亮样式 */
.province-name-label-hover {
color: #00ffdd;
border-color: rgba(0, 255, 200, 0.8);
box-shadow: 0 0 18px rgba(0, 255, 200, 0.6);
transform: scale(1.08);
font-size: 16px; /* 悬停时提升字号至少14px以上 */
padding: 8px 16px; /* 全称时更大的内边距,提升可读性 */
min-width: 96px; /* 全称显示更宽的标签 */
opacity: 1; /* 悬停淡入显示 */
}
.province-name-label-hover .abbr {
opacity: 0;
position: absolute;
transform: translateY(-2px) scale(0.92);
}
.province-name-label-hover .full {
opacity: 1;
position: static; /* 参与布局,让容器宽度扩展为全称宽度 */
transform: translateY(0) scale(1.06);
}
/* 小屏或低分辨率下适度进一步缩小,保持视觉平衡 */
@media (max-width: 1366px) {
.province-name-label .abbr { font-size: calc(1em - 2px); }
}
/* 省份标签样式 - 气泡标记上的标签 */
.province-label {
font-size: 11px; /* 整体缩小以保持界面平衡 */
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: 2px 6px; /* 缩小内边距 */
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; /* 添加过渡效果 */
}
/* 省份点击动画效果 */
@keyframes provinceClickPulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 省份悬停效果增强 */
@keyframes provinceHoverGlow {
0% {
box-shadow: 0 0 5px rgba(0, 212, 255, 0.3);
}
50% {
box-shadow: 0 0 20px rgba(0, 212, 255, 0.6);
}
100% {
box-shadow: 0 0 5px rgba(0, 212, 255, 0.3);
}
}
/* 点击反馈提示 - 响应式优化 */
.province-click-feedback {
position: absolute;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.9), rgba(0, 204, 102, 0.8));
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
pointer-events: none;
z-index: 10001;
animation: clickFeedbackShow 0.6s ease-out;
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.4);
border: 1px solid rgba(0, 255, 136, 0.6);
white-space: nowrap;
}
/* 移动端优化 */
@media screen and (max-width: 768px) {
.province-click-feedback {
font-size: 12px;
padding: 6px 12px;
border-radius: 16px;
}
}
/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
.province-click-feedback {
font-size: 13px;
padding: 8px 14px;
animation-duration: 0.8s;
}
}
@keyframes clickFeedbackShow {
0% {
opacity: 0;
transform: scale(0.5) translateY(20px);
}
50% {
opacity: 1;
transform: scale(1.1) translateY(-10px);
}
100% {
opacity: 0;
transform: scale(1) translateY(-30px);
}
}
.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>