diff --git a/src/App.vue b/src/App.vue
index 81c4a36..c6f6fb2 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -2,19 +2,69 @@
import { ref } from 'vue'
import Home from './components/Home.vue'
import Alert from './components/Alert.vue'
+import Price from './components/Price.vue'
// 当前激活的页面
const currentPage = ref('home')
+// 当前选中的省份参数
+const selectedProvince = ref('')
+
// 页面组件映射
const pageComponents = {
home: Home,
- alert: Alert
+ alert: Alert,
+ price: Price
}
// 切换页面函数
-const switchPage = (page) => {
+const switchPage = (page, provinceParam = '') => {
currentPage.value = page
+ if (provinceParam) {
+ selectedProvince.value = provinceParam
+ }
+}
+
+// 提供给子组件的跳转函数
+const navigateToWarningMonitor = (provinceName) => {
+ // 省份名称到代码的映射
+ const provinceCodeMap = {
+ '北京市': 'BJ',
+ '天津市': 'TJ',
+ '河北省': 'HE',
+ '山西省': 'SX',
+ '内蒙古自治区': 'NM',
+ '辽宁省': 'LN',
+ '吉林省': 'JL',
+ '黑龙江省': 'HL',
+ '上海市': 'SH',
+ '江苏省': 'JS',
+ '浙江省': 'ZJ',
+ '安徽省': 'AH',
+ '福建省': 'FJ',
+ '江西省': 'JX',
+ '山东省': 'SD',
+ '河南省': 'HA',
+ '湖北省': 'HB',
+ '湖南省': 'HN',
+ '广东省': 'GD',
+ '广西壮族自治区': 'GX',
+ '海南省': 'HI',
+ '重庆市': 'CQ',
+ '四川省': 'SC',
+ '贵州省': 'GZ',
+ '云南省': 'YN',
+ '西藏自治区': 'XZ',
+ '陕西省': 'SN',
+ '甘肃省': 'GS',
+ '青海省': 'QH',
+ '宁夏回族自治区': 'NX',
+ '新疆维吾尔自治区': 'XJ'
+ }
+
+ const provinceCode = provinceCodeMap[provinceName] || provinceName
+ // 跳转到价格行情页
+ switchPage('price', provinceCode)
}
@@ -39,6 +89,9 @@ const switchPage = (page) => {
首页
+
+ 价格行情
+
预警监测
@@ -54,7 +107,11 @@ const switchPage = (page) => {
-
+
diff --git a/src/components/Alert.vue b/src/components/Alert.vue
index 04558c8..fc873f0 100644
--- a/src/components/Alert.vue
+++ b/src/components/Alert.vue
@@ -246,14 +246,36 @@
@@ -964,38 +1020,23 @@ export default {
margin-top: 0; /* 移除负边距 */
}
-/* 不同品种牛单价最低省份展示模块 */
-.lowest-price-panel {
- background: rgba(0, 20, 40, 0.8);
- border: 1px solid #00d4ff;
- border-radius: 8px;
- padding: 15px;
- /* margin-bottom: 15px; */
- height: 28%; /* 从32%调整到28%,为地图腾出更多空间 */
-width: 730px;
-margin-left: 580px;
-}
+
.lowest-price-panel h3 {
color: #00d4ff;
font-size: 16px;
margin: 0 0 15px 0;
- text-align: center;
+ text-align: left;
font-weight: bold;
}
-.price-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 10px;
- height: calc(100% - 40px);
-}
+
.price-item {
background: rgba(0, 255, 255, 0.1);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 6px;
- padding: 10px; /* 增加内边距 */
+ padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -1044,7 +1085,6 @@ margin-left: 580px;
top: 0px;
left: 10px;
right: 10px;
- bottom: 0px;
border: 2px solid #00d4ff;
border-radius: 8px;
pointer-events: none;
@@ -1087,9 +1127,9 @@ margin-left: 580px;
display: flex;
flex-direction: column;
background: rgba(0, 20, 40, 0.3);
- /* border: 1px solid rgba(0, 212, 255, 0.2); */ /* 去掉边框 */
+
border-radius: 4px;
- padding: 10px;
+
}
/* 上下布局时的图表区域样式 */
@@ -1101,9 +1141,9 @@ margin-left: 580px;
color: #00d4ff;
font-size: 14px;
margin: 0 0 10px 0;
- text-align: center;
+ text-align: left;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
- padding-bottom: 5px;
+
}
.sales-section .chart,
@@ -1123,47 +1163,22 @@ margin-left: 580px;
background: rgba(7, 59, 68, 0.15);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
- margin-bottom: 15px;
+ margin-bottom: 5px;
backdrop-filter: blur(5px);
position: relative;
- overflow: hidden; /* 防止内容溢出 */
+ overflow: hidden;
box-shadow:
0 0 8px rgba(0, 212, 255, 0.2),
inset 0 0 8px rgba(0, 212, 255, 0.05);
}
-/* 面板内部发光边框 */
-/* .panel::before {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- right: 2px;
- bottom: 2px;
- border: 1px solid rgba(0, 212, 255, 0.1);
- border-radius: 4px;
- pointer-events: none;
-} */
-/* 面板角落装饰 */
-/* .panel::after {
- content: '';
- position: absolute;
- top: -1px;
- left: -1px;
- width: 20px;
- height: 20px;
- border-top: 2px solid #00d4ff;
- border-left: 2px solid #00d4ff;
- border-radius: 6px 0 0 0;
-} */
.panel-header {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
- /* padding: 10px 15px 8px 15px; */
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
margin-bottom: 10px;
}
@@ -1180,12 +1195,10 @@ margin-left: 580px;
}
.panel-header h3 {
- /* margin-bottom: 5px; */
color: #ffffff;
font-size: 16px;
padding-bottom: 5px;
font-weight: bold;
- /* text-shadow: 0 0 8px rgba(255, 255, 255, 0.5); */
position: relative;
z-index: 1;
}
@@ -1203,19 +1216,7 @@ margin-left: 580px;
box-shadow: 0 0 6px rgba(0, 212, 255, 0.8);
}
-/* 地区选择下拉框样式 */
-.region-select,
-.breed-selector {
- background: rgba(0, 20, 40, 0.8);
- border: 1px solid #00d4ff;
- border-radius: 4px;
- color: #ffffff;
- padding: 4px 8px;
- font-size: 12px;
- outline: none;
- cursor: pointer;
- transition: all 0.3s ease;
-}
+
.region-select:hover,
.breed-selector:hover {
@@ -1233,9 +1234,10 @@ margin-left: 580px;
/* 存栏总数统计样式 */
.livestock-panel .echarts-container {
display: flex;
- flex-direction: column;
- height: 250px;
- padding: 0 10px 10px 10px;
+ flex-direction: row;
+ gap: 10px; /* 两图间距约16px */
+ height:230px;
+ padding: 0 5px 5px 5px;
}
.total-display {
@@ -1261,48 +1263,95 @@ margin-left: 580px;
height: 350px;
}
-/* 出售统计样式 */
-.sales-panel .sales-chart {
- height: 280px;
- padding: 10px 0px 0px 0px;
-}
-
-.sales-panel .sales-chart .chart {
- width: 100%;
- height: 78%;
-}
-
-/* 牛只耳标佩戴统计样式 */
-.ear-tag-panel .ear-tag-stats {
- display: flex;
- gap: 15px;
- height: 280px;
- padding: 8px 15px 15px 15px;
-}
-
-.flip-cards {
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-width: 100px;
- padding-top: 20px;
-}
-
-.flip-card {
- background-color: transparent;
- width: 100px;
- height: 90px;
- perspective: 1000px;
-}
-
-.flip-card-inner {
- position: relative;
- width: 100%;
+/* 新增:存栏率的两个图表容器 */
+.livestock-pie {
+ flex: 0 0 40%;
height: 100%;
- text-align: center;
- transition: transform 0.6s;
- transform-style: preserve-3d;
}
+.livestock-bar {
+ flex: 1;
+ height: 100%;
+}
+
+/* 统一图例样式,置于两图下方 */
+.livestock-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px 10px;
+
+ align-items: center;
+}
+.livestock-legend .legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.livestock-legend .legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+ box-shadow: 0 0 6px rgba(0, 212, 255, 0.5);
+}
+.livestock-legend .legend-label {
+ color: #eaf7ff;
+ font-size: 12px;
+}
+
+/* 小屏幕下纵向排列 */
+@media (max-width: 768px) {
+ .livestock-panel .echarts-container {
+ flex-direction: column;
+ height: auto;
+ }
+ .livestock-pie,
+ .livestock-bar {
+ width: 100%;
+ height: 280px;
+ }
+}
+
+/* 出栏率统计样式 */
+.slaughter-panel {
+ height: 330px;
+}
+.slaughter-panel .echarts-container {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 16px;
+ height: 200px;
+}
+.slaughter-pie {
+ flex: 0 0 42%;
+ height: 100%;
+}
+.slaughter-bar {
+ flex: 1;
+ height: 100%;
+}
+.slaughter-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px 10px;
+ align-items: center;
+ margin-top: 8px;
+}
+.slaughter-legend .legend-item { display: flex; align-items: center; gap: 6px; }
+.slaughter-legend .legend-color { width: 12px; height: 12px; border-radius: 2px; box-shadow: 0 0 6px rgba(0, 212, 255, 0.5); }
+.slaughter-legend .legend-label { color: #eaf7ff; font-size: 12px; }
+.validation-message { margin-top: 6px; color: #ffcf6e; font-size: 12px; }
+
+@media (max-width: 768px) {
+ .slaughter-panel { height: auto; }
+ .slaughter-panel .echarts-container { flex-direction: column; height: auto; }
+ .slaughter-pie, .slaughter-bar { width: 100%; height: 280px; }
+}
+
+
+
+
+
+
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
@@ -1347,16 +1396,7 @@ margin-left: 580px;
height:93%;
}
-/* 防疫统计样式 */
- .epidemic-panel .epidemic-content {
- height: 200px;
- padding: 0 15px 15px 15px;
-}
-.epidemic-chart {
- width: 100%;
- height: 100%;
-}
/* 品种单价排行榜样式 */
.price-panel .price-content {
@@ -1397,7 +1437,7 @@ margin-left: 580px;
.detail-row {
color: #fff;
- padding: 6px 0;
+ /* padding: 6px 0; */
border-bottom: 1px dashed rgba(132, 172, 240, 0.15);
}
@@ -1408,7 +1448,7 @@ margin-left: 580px;
/* 销售额柱状图模块样式 */
.sales-revenue-panel {
- margin-top: 20px;
+ margin-top: 5px;
height: 280px; /* 缩小高度 */
background: rgba(0, 20, 40, 0.3);
border: 1px solid rgba(0, 212, 255, 0.2);
@@ -1422,58 +1462,7 @@ margin-left: 580px;
margin-top: 15px; /* 添加上边距,将图表往下移 */
}
-/* 牛只参保统计样式 */
-.cattle-insurance-panel .insurance-stats {
- display: flex;
- align-items: center;
- height: 220px;
- padding: 10px 15px 15px 15px;
-}
-.insurance-circle {
- position: relative;
- width: 120px;
- height: 120px;
- margin-right: 20px;
- flex-shrink: 0;
- align-self: flex-start;
- margin-top: 15px;
-}
-
-.insurance-chart {
- width: 100%;
- height: 100%;
-}
-
-.insurance-center {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
- z-index: 10;
-}
-
-.insurance-number {
- font-size: 16px;
- font-weight: bold;
- color: #00ffff;
- margin-bottom: 4px;
-}
-
-.insurance-label {
- font-size: 10px;
- color: #cccccc;
- line-height: 1.2;
-}
-
-.insurance-companies {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 100%;
-}
.company-item {
display: flex;
@@ -1502,4 +1491,39 @@ margin-left: 580px;
flex: 1;
line-height: 1.2;
}
+
+/* 存栏率模块工具栏与数据说明 */
+.panel-tools {
+ display: flex;
+ gap: 8px;
+ margin-left: 10px;
+}
+
+.tool-btn {
+ background: rgba(0, 212, 255, 0.15);
+ border: 1px solid rgba(0, 212, 255, 0.4);
+ color: #ffffff;
+ border-radius: 6px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.tool-btn:hover {
+ background: rgba(0, 212, 255, 0.25);
+}
+
+.data-notes {
+ margin-top: 10px;
+ min-height: 10%;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ color: #00d4ff;
+ font-size: 12px;
+}
+
+.data-source {
+ color: rgba(255, 255, 255, 0.7);
+}
\ No newline at end of file
diff --git a/src/components/Map3D.vue b/src/components/Map3D.vue
index f72caaf..bfd3c33 100644
--- a/src/components/Map3D.vue
+++ b/src/components/Map3D.vue
@@ -47,7 +47,8 @@ export default {
components: {
// FarmPopup // 移除弹窗组件注册
},
- setup() {
+ emits: ['province-click'], // 添加省份点击事件
+ setup(props, { emit }) {
let baseEarth = null;
// 移除养殖场标记相关变量
// let farmMarkers = []; // 存储养殖场标记
@@ -332,23 +333,88 @@ export default {
// 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');
+ // 标签注册表:名称到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 = `${abbr}${fullName}`;
+ 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);
-
- // 调整标签位置,避免与气泡标记重叠
- label.show(properties.name, new THREE.Vector3(...labelCenter, 1.2));
-
- // console.log(`标签创建并显示成功: ${properties.name}`);
+ // 初始位置:略微抬高避免与边界贴合
+ // 默认显示简称内容(已在 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
+ });
};
// 省份牛品种和存栏量数据 - 扩展为所有省份
@@ -525,71 +591,23 @@ export default {
}
};
- // 创建省份气泡标记 - 修改为为所有省份创建标记点(排除港澳台)
+ // 仅创建省份名称文本标签(替换原圆圈标记)
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}气泡标记和标签创建成功`);
+ // 此处不再创建任何 Three.js 圆圈或光柱,仅依赖上方 initLabel 创建的省份名称标签
+ // 如果需要在部分省份单独微调位置,可在此添加偏移逻辑:
+ // 例如:
+ // const label = labelRegistry.get(provinceName);
+ // if (label) label.position.set(center[0] + dx, center[1] + dy, 1.2);
};
// 创建悬浮信息提示框
@@ -812,9 +830,8 @@ export default {
};
this.mapGroup.add(province);
- // 创建标点和标签
-
- initLabel(properties, this.scene);
+ // 创建标点和标签(传入省份对象用于面积估算)
+ initLabel(properties, this.scene, province);
// 为每个省份创建气泡标记
createProvinceBubble(properties, this.scene);
@@ -836,12 +853,19 @@ export default {
// 计算合适的相机距离,让地图显示得更大一些
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);
@@ -851,6 +875,23 @@ export default {
// 初始化省份悬停交互功能
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()完成后初始化
@@ -878,13 +919,85 @@ export default {
// 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,
@@ -897,28 +1010,79 @@ export default {
opacity: 0.8
});
- const onMouseMove = (event) => {
+ // 创建点击高亮材质
+ 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();
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+ 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 (hoveredProvince) {
- hoveredProvince.children.forEach(mesh => {
- if (mesh.userData.type === 'province') {
- mesh.material = mesh.userData.originalMaterial;
+ 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;
+ hoveredProvince = null;
+ }
}
- // 检查是否悬停在省份上
+ // 检查是否悬停/点击在省份上
if (intersects.length > 0) {
- const intersectedObject = intersects[0].object;
- if (intersectedObject.userData.type === 'province') {
+ // 选择首个命中省份网格,避免边界线/其他对象抢占命中
+ 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') {
@@ -926,37 +1090,175 @@ export default {
}
if (provinceGroup) {
- hoveredProvince = provinceGroup;
- // 高亮整个省份
- provinceGroup.children.forEach(mesh => {
- if (mesh.userData.type === 'province') {
- mesh.material = [highlightTopMaterial, highlightSideMaterial];
+ 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) {}
}
- });
-
- // 改变鼠标样式
- this.container.style.cursor = 'pointer';
+
+ // 改变鼠标样式(仅桌面端)
+ if (!isMobile) {
+ this.container.style.cursor = 'pointer';
+ }
+ }
}
}
- } else {
- // 恢复默认鼠标样式
- this.container.style.cursor = 'default';
+ } 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;
}
};
// 添加事件监听器
- this.container.addEventListener('mousemove', onMouseMove);
+ 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.provinceHoverHandler && this.container) {
- this.container.removeEventListener('mousemove', this.provinceHoverHandler);
+ // 清理省份悬停和点击事件监听器
+ 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() {
@@ -1015,6 +1317,163 @@ export default {
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();
@@ -1076,6 +1535,31 @@ export default {
});
}
}
+ // 惯性与阻尼更新(拖拽松开后的滑动与边界弹性回弹)
+ 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++) {
@@ -1137,10 +1621,33 @@ export default {
baseEarth.run();
// console.log('Earth实例运行完成');
- // 将CSS2D渲染器赋值给baseEarth实例,确保渲染循环中能正确访问
- if (baseEarth && baseEarth.css2dRender) {
- // CSS2D渲染器已经在CurrentEarth类中初始化了
- }
+ // 重新挂载/初始化 CSS2DRenderer(initRenderer 会清空容器,需要在其后恢复)
+ 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);
@@ -1311,35 +1818,85 @@ export default {
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-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: 13px; /* 稍微增大字号 */
+ 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: 3px 8px; /* 增加内边距 */
+ padding: 2px 6px; /* 缩小内边距 */
border-radius: 6px; /* 增大圆角 */
border: 1px solid rgba(0, 212, 255, 0.7); /* 增强边框 */
backdrop-filter: blur(6px); /* 增强背景模糊 */
@@ -1350,7 +1907,84 @@ export default {
transition: all 0.3s ease; /* 添加过渡效果 */
}
-/* 全国牛源地TOP5列表样式 - 优化配色方案 */
+/* 省份点击动画效果 */
+@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; /* 定位到底部 */
diff --git a/src/components/Price.vue b/src/components/Price.vue
new file mode 100644
index 0000000..08fe533
--- /dev/null
+++ b/src/components/Price.vue
@@ -0,0 +1,552 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ row.date }}
+
{{ row.region }}
+
{{ row.breed }}
+
{{ row.price.toFixed(2) }}{{ unit }}
+
+
+ {{ row.delta.toFixed(2) }}{{ unit }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 今日批发均价
+ 相比昨日 {{ trendText }} {{ yesterdayDelta.toFixed(2) }} {{ trendSymbol }}
+
+
+ ¥
+ {{ avgToday.toFixed(2) }}
+ {{ unit }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file