From 3bfc51c184e772081c4945d7c77c5e46a4f38947 Mon Sep 17 00:00:00 2001 From: dengyuxin Date: Fri, 7 Nov 2025 14:04:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=9C=B0=E5=9B=BE=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=92=8C=E6=B7=BB=E5=8A=A0=E5=9C=B0=E5=8C=BA-?= =?UTF-8?q?=E5=93=81=E7=A7=8D=E6=95=B0=E6=8D=AE=E5=B1=95=E7=A4=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 63 ++- src/components/Alert.vue | 26 +- src/components/Home.vue | 1146 +++++++++++++++++++------------------- src/components/Map3D.vue | 898 ++++++++++++++++++++++++----- src/components/Price.vue | 552 ++++++++++++++++++ 5 files changed, 1987 insertions(+), 698 deletions(-) create mode 100644 src/components/Price.vue 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 @@ + + + + + \ No newline at end of file