更新地图显示和添加地区-品种数据展示功能
This commit is contained in:
63
src/App.vue
63
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)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,6 +89,9 @@ const switchPage = (page) => {
|
||||
<div class="nav-item" :class="{ active: currentPage === 'home' }" @click="switchPage('home')">
|
||||
<span>首页</span>
|
||||
</div>
|
||||
<div class="nav-item" :class="{ active: currentPage === 'price' }" @click="switchPage('price')">
|
||||
<span>价格行情</span>
|
||||
</div>
|
||||
<div class="nav-item" :class="{ active: currentPage === 'alert' }" @click="switchPage('alert')">
|
||||
<span>预警监测</span>
|
||||
</div>
|
||||
@@ -54,7 +107,11 @@ const switchPage = (page) => {
|
||||
<!-- 主体内容区域 -->
|
||||
<main class="dashboard-main">
|
||||
<!-- 动态组件加载 -->
|
||||
<component :is="pageComponents[currentPage]" />
|
||||
<component
|
||||
:is="pageComponents[currentPage]"
|
||||
:selectedProvince="selectedProvince"
|
||||
@navigate-to-warning="navigateToWarningMonitor"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -246,14 +246,36 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'Alert',
|
||||
setup() {
|
||||
props: {
|
||||
selectedProvince: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const deviceChart = ref(null)
|
||||
|
||||
// 监听省份参数变化
|
||||
watch(() => props.selectedProvince, (newProvince) => {
|
||||
if (newProvince) {
|
||||
console.log(`预警监测页面接收到省份参数: ${newProvince}`)
|
||||
// 这里可以根据省份参数加载对应的数据
|
||||
loadProvinceData(newProvince)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 根据省份加载数据
|
||||
const loadProvinceData = (provinceCode) => {
|
||||
// 这里可以实现根据省份代码加载对应的预警数据
|
||||
console.log(`加载省份 ${provinceCode} 的预警数据`)
|
||||
// 可以调用API或更新本地数据
|
||||
}
|
||||
|
||||
// 设备统计数据
|
||||
const deviceData = ref({
|
||||
farms: ['养殖场A', '养殖场B', '养殖场C', '养殖场D', '养殖场E', '养殖场F'],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// 标签注册表:名称到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;
|
||||
}
|
||||
// 设置标签的显示内容和位置
|
||||
let labelCenter = properties.center || properties.centroid || properties.cp;
|
||||
// console.log(`创建标签: ${properties.name}, 位置:`, labelCenter);
|
||||
|
||||
// 创建省份名称标签(使用优化的样式类)
|
||||
var label = create2DTag(properties.name, 'province-name-label');
|
||||
// 创建省份名称标签(默认只显示简称/图标,悬停显示全称)
|
||||
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);
|
||||
}
|
||||
|
||||
// 调整标签位置,避免与气泡标记重叠
|
||||
label.show(properties.name, new THREE.Vector3(...labelCenter, 1.2));
|
||||
|
||||
// console.log(`标签创建并显示成功: ${properties.name}`);
|
||||
// 注册标签用于交互联动与碰撞处理
|
||||
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()完成后初始化
|
||||
|
||||
@@ -879,12 +920,84 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化省份悬停交互功能
|
||||
// 根据相机距离动态调整标签缩放
|
||||
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 (!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) {
|
||||
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,6 +1090,31 @@ export default {
|
||||
}
|
||||
|
||||
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 => {
|
||||
@@ -933,31 +1122,144 @@ export default {
|
||||
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 {
|
||||
// 恢复默认鼠标样式
|
||||
}
|
||||
}
|
||||
} 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.provinceHoverHandler && this.container) {
|
||||
// 清理省份悬停和点击事件监听器
|
||||
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();
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -1313,13 +1820,13 @@ export default {
|
||||
|
||||
/* 省份名称标签样式 - 优化显示效果和配色方案 */
|
||||
.province-name-label {
|
||||
font-size: 16px; /* 增大字号提高可读性 */
|
||||
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: 6px 12px; /* 增加内边距 */
|
||||
border-radius: 8px; /* 增大圆角 */
|
||||
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); /* 添加阴影效果 */
|
||||
@@ -1328,18 +1835,68 @@ export default {
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
min-width: 80px; /* 增加最小宽度 */
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
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; /* 定位到底部 */
|
||||
|
||||
552
src/components/Price.vue
Normal file
552
src/components/Price.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
||||
import VChart from 'vue-echarts'
|
||||
|
||||
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent])
|
||||
|
||||
export default {
|
||||
name: 'Price',
|
||||
components: { VChart },
|
||||
props: {
|
||||
selectedProvince: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const provinceNameMap = {
|
||||
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 provinceName = computed(() => provinceNameMap[props.selectedProvince] || props.selectedProvince || '未知地区')
|
||||
|
||||
// 省份基础价(元/斤),用于生成示例数据
|
||||
const basePriceMap = {
|
||||
NX: 6.8, BJ: 7.2, TJ: 6.9, HI: 7.0, CQ: 6.7, HE: 6.6, SD: 6.9, HB: 6.8
|
||||
}
|
||||
|
||||
// 牛品种列表(示例)
|
||||
const breedListMap = {
|
||||
default: ['西门塔尔牛', '利木赞牛', '夏洛来牛', '本地黄牛'],
|
||||
NX: ['西门塔尔牛', '本地黄牛', '海福特牛'],
|
||||
BJ: ['利木赞牛', '安格斯牛', '夏洛来牛'],
|
||||
HI: ['本地黄牛', '安格斯牛'],
|
||||
CQ: ['本地黄牛', '西门塔尔牛']
|
||||
}
|
||||
|
||||
const unit = ref('元/个')
|
||||
const days = ref([])
|
||||
const priceSeries = ref([])
|
||||
// 省内各地区牛数据列表
|
||||
const regionRows = ref([])
|
||||
const selectedRow = ref(null)
|
||||
|
||||
const todayPrice = computed(() => (priceSeries.value[priceSeries.value.length - 1] ?? 0))
|
||||
const max7d = computed(() => (priceSeries.value.length ? Math.max(...priceSeries.value) : 0))
|
||||
const min7d = computed(() => (priceSeries.value.length ? Math.min(...priceSeries.value) : 0))
|
||||
const avg7d = computed(() => {
|
||||
if (!priceSeries.value.length) return 0
|
||||
const sum = priceSeries.value.reduce((acc, v) => acc + v, 0)
|
||||
return +(sum / priceSeries.value.length).toFixed(2)
|
||||
})
|
||||
|
||||
// 当日均价(以省内各地区列表的当日价格求平均;无列表则回退为趋势当日值)
|
||||
const avgToday = computed(() => {
|
||||
if (selectedRow.value && selectedRow.value.price) {
|
||||
return +(selectedRow.value.price).toFixed(2)
|
||||
}
|
||||
if (regionRows.value.length) {
|
||||
const sum = regionRows.value.reduce((acc, r) => acc + (r.price || 0), 0)
|
||||
return +(sum / regionRows.value.length).toFixed(2)
|
||||
}
|
||||
return todayPrice.value
|
||||
})
|
||||
|
||||
const breeds = computed(() => breedListMap[props.selectedProvince] || breedListMap.default)
|
||||
|
||||
// 日期范围与重量类别筛选
|
||||
const range = ref(7) // 7/30/60
|
||||
const weight = ref('normal') // normal/400/500
|
||||
const setRange = (v) => { range.value = v; genData(selectedRow.value?.id) }
|
||||
const setWeight = (w) => { weight.value = w; genData(selectedRow.value?.id) }
|
||||
|
||||
const isUnknownRegion = (name) => {
|
||||
const n = (name ?? '').toString()
|
||||
return n === '未知地区' || n.startsWith('未知地区 ')
|
||||
}
|
||||
|
||||
const chartOption = computed(() => ({
|
||||
title: { text: `${provinceName.value} 牛价趋势`, left: 'center', textStyle: { color: '#00d4ff' } },
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, top: 60, bottom: 40 },
|
||||
xAxis: { type: 'category', data: days.value, axisLabel: { color: '#cfefff' }, axisLine: { lineStyle: { color: '#2e6ba8' } } },
|
||||
yAxis: { type: 'value', axisLabel: { color: '#cfefff' }, splitLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } } },
|
||||
series: [{
|
||||
name: '牛价', type: 'line', smooth: true, data: priceSeries.value,
|
||||
lineStyle: { color: '#00d4ff', width: 2 },
|
||||
itemStyle: { color: '#00ffcc' },
|
||||
areaStyle: { color: 'rgba(0,212,255,0.15)' }
|
||||
}]
|
||||
}))
|
||||
|
||||
// 生成省内地区牛数据列表(示例,固定为10行)
|
||||
const genRegionRows = () => {
|
||||
const today = new Date()
|
||||
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||
const regionTypes = [
|
||||
'省会城区','市辖区','回族自治县','张北县','市郊区',
|
||||
'开发区','交易市场','牧区','新区','工业园区'
|
||||
]
|
||||
const regions = regionTypes.map(t => `${provinceName.value} ${t}`)
|
||||
const base = basePriceMap[props.selectedProvince] ?? 6.8
|
||||
regionRows.value = regions.slice(0, 10).map((r, idx) => {
|
||||
const d = new Date(today)
|
||||
const fluct = (Math.sin(idx) * 0.08) + (Math.random() * 0.12 - 0.06)
|
||||
const breed = (breeds.value[idx % breeds.value.length])
|
||||
const price = +((base + fluct)).toFixed(2)
|
||||
const delta = +((Math.random() * 0.6 - 0.3)).toFixed(2)
|
||||
return {
|
||||
date: fmt(d),
|
||||
breed,
|
||||
region: r,
|
||||
price,
|
||||
delta,
|
||||
up: delta > 0,
|
||||
id: `${props.selectedProvince}-${idx}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const genData = (regionId = null) => {
|
||||
// 根据筛选生成最近 N 天日期与价格数据(可按地区与重量微调)
|
||||
const baseBase = basePriceMap[props.selectedProvince] ?? 6.8
|
||||
const regionOffset = regionId ? (parseInt(regionId.split('-')[1]) || 0) * 0.03 : 0
|
||||
const weightOffset = weight.value === '400' ? 0.15 : (weight.value === '500' ? 0.25 : 0)
|
||||
const base = baseBase + regionOffset + weightOffset
|
||||
const today = new Date()
|
||||
const d = []
|
||||
const p = []
|
||||
const count = range.value
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
const dt = new Date(today)
|
||||
dt.setDate(today.getDate() - i)
|
||||
d.push(`${dt.getMonth() + 1}-${String(dt.getDate()).padStart(2, '0')}`)
|
||||
// 价格在基础价附近小幅波动
|
||||
const fluct = (Math.sin(i) * 0.08) + (Math.random() * 0.12 - 0.06)
|
||||
p.push(+((base + fluct)).toFixed(2))
|
||||
}
|
||||
days.value = d
|
||||
priceSeries.value = p
|
||||
}
|
||||
|
||||
const handleRowClick = (row) => {
|
||||
selectedRow.value = row
|
||||
genData(row?.id)
|
||||
}
|
||||
|
||||
const yesterdayDelta = computed(() => {
|
||||
if (!priceSeries.value.length || priceSeries.value.length < 2) return 0
|
||||
const n = priceSeries.value.length
|
||||
return +(priceSeries.value[n - 1] - priceSeries.value[n - 2]).toFixed(2)
|
||||
})
|
||||
const trendText = computed(() => {
|
||||
const d = yesterdayDelta.value
|
||||
if (Math.abs(d) < 0.01) return '平稳'
|
||||
return d > 0 ? '上涨' : '下跌'
|
||||
})
|
||||
const trendSymbol = computed(() => {
|
||||
const d = yesterdayDelta.value
|
||||
if (Math.abs(d) < 0.01) return '-'
|
||||
return d > 0 ? '↑' : '↓'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
genRegionRows()
|
||||
genData()
|
||||
})
|
||||
|
||||
return { unit, provinceName, breeds, todayPrice, max7d, min7d, avg7d, avgToday, days, priceSeries, chartOption, regionRows, selectedRow, handleRowClick, isUnknownRegion, range, weight, setRange, setWeight, yesterdayDelta, trendText, trendSymbol }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="price-page">
|
||||
<div class="price-header">
|
||||
<h2><span :class="{ highlight: isUnknownRegion(provinceName) }">{{ provinceName }}</span> 牛价行情</h2>
|
||||
<!-- <div class="unit">单位:<span>{{ unit }}</span></div> -->
|
||||
</div>
|
||||
|
||||
<div class="price-content">
|
||||
<section class="left-info">
|
||||
<div class="card region-card">
|
||||
<div class="panel-header">
|
||||
<div class="diamond-icon"></div>
|
||||
<h3>省内各地区牛数据列表</h3>
|
||||
</div>
|
||||
<div class="region-table">
|
||||
<div class="table-header">
|
||||
<div class="th">时间</div>
|
||||
<div class="th">地区</div>
|
||||
<div class="th">品类</div>
|
||||
<div class="th">价格({{ unit }})</div>
|
||||
<div class="th">涨跌</div>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div class="table-row" v-for="row in regionRows" :key="row.id" @click="handleRowClick(row)" :class="{ active: selectedRow && selectedRow.id === row.id }">
|
||||
<div class="td">{{ row.date }}</div>
|
||||
<div class="td">{{ row.region }}</div>
|
||||
<div class="td">{{ row.breed }}</div>
|
||||
<div class="td">{{ row.price.toFixed(2) }}{{ unit }}</div>
|
||||
<div class="td">
|
||||
<span :class="row.delta === 0 ? 'change-zero' : (row.up ? 'change-up' : 'change-down')">
|
||||
{{ row.delta.toFixed(2) }}{{ unit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="right-stats">
|
||||
<div class="card detail-card">
|
||||
<div class="panel-header">
|
||||
<div class="diamond-icon"></div>
|
||||
<h3>
|
||||
<span :class="{ highlight: isUnknownRegion(selectedRow ? selectedRow.region : provinceName) }">
|
||||
{{ (selectedRow ? selectedRow.region : provinceName) }}
|
||||
</span>
|
||||
价格详情
|
||||
</h3>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="today-price-line">
|
||||
<div class="labels">
|
||||
<span class="label">今日批发均价</span>
|
||||
<span class="compare">相比昨日 <span :class="yesterdayDelta === 0 ? 'change-zero' : (yesterdayDelta > 0 ? 'change-up' : 'change-down')">{{ trendText }} {{ yesterdayDelta.toFixed(2) }}</span> <span class="symbol">{{ trendSymbol }}</span></span>
|
||||
</div>
|
||||
<div class="price-display">
|
||||
<span class="currency">¥</span>
|
||||
<span class="value">{{ avgToday.toFixed(2) }}</span>
|
||||
<span class="unit">{{ unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<div class="weight-group">
|
||||
<button :class="['filter-btn', { active: weight === 'normal' }]" @click="setWeight('normal')">通货</button>
|
||||
<button :class="['filter-btn', { active: weight === '400' }]" @click="setWeight('400')">400斤</button>
|
||||
<button :class="['filter-btn', { active: weight === '500' }]" @click="setWeight('500')">500斤</button>
|
||||
</div>
|
||||
<div class="range-group">
|
||||
<button :class="['filter-btn', { active: range === 7 }]" @click="setRange(7)">近7天</button>
|
||||
<button :class="['filter-btn', { active: range === 30 }]" @click="setRange(30)">近30天</button>
|
||||
<button :class="['filter-btn', { active: range === 60 }]" @click="setRange(60)">近60天</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-chart class="trend-chart" :option="chartOption" autoresize />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.price-page {
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: #011819; /* 与预警监测页背景一致 */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.price-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background:
|
||||
rgba(0, 212, 255, 0.05),
|
||||
rgba(0, 212, 255, 0.03),
|
||||
rgba(0, 212, 255, 0.02);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.price-page > * { position: relative; z-index: 1; }
|
||||
|
||||
.price-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(0,212,255,0.25);
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.price-header h2 {
|
||||
font-size: 18px;
|
||||
color: #eaf7ff;
|
||||
}
|
||||
|
||||
.unit span {
|
||||
color: #00ffcc;
|
||||
}
|
||||
|
||||
.price-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1.6fr; /* 加宽左侧列表区域 */
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(7, 59, 68, 0.15); /* 参考预警监测 .panel 背景 */
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #00d4ff;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #bfe9ff;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 统一标题为预警监测页风格 */
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.highlight { color: #00d4ff; }
|
||||
|
||||
.diamond-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #00d4ff;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.breed-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.breed-item {
|
||||
background: rgba(0,212,255,0.12);
|
||||
border: 1px solid rgba(0,212,255,0.35);
|
||||
color: #eaf7ff;
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.price-today .value {
|
||||
color: #00ffcc;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.price-today .unit {
|
||||
color: #cfefff;
|
||||
}
|
||||
|
||||
.right-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(0,212,255,0.25);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.today-price-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.today-price-line .labels { color: #cfefff; font-size: 14px; }
|
||||
.today-price-line .price-display { display: flex; align-items: baseline; gap: 6px; }
|
||||
.today-price-line .currency { color: #ff7a28; font-size: 22px; font-weight: 700; }
|
||||
.today-price-line .value { color: #ff7a28; font-size: 32px; font-weight: 800; }
|
||||
.today-price-line .unit { color: #ff7a28; font-size: 14px; font-weight: 600; }
|
||||
.today-price-line .symbol { color: #cfefff; margin-left: 6px; }
|
||||
|
||||
.filters { display: flex; justify-content: space-between; align-items: center; }
|
||||
.filter-btn {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(0,212,255,0.35);
|
||||
color: #eaf7ff;
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.filter-btn.active { background: rgba(0,212,255,0.2); border-color: #00d4ff; color: #ffffff; }
|
||||
.filter-btn:hover { background: rgba(0,212,255,0.12); }
|
||||
.weight-group, .range-group { display: flex; align-items: center; }
|
||||
|
||||
.region-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.table-header, .table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.6fr 1.2fr 1fr 1fr; /* 时间略窄,地区更宽,价格与涨跌等宽 */
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
padding: 18px 0; /* 增加上下内边距,提高行高 */
|
||||
min-height: 60px; /* 提高最小高度,列表更舒展 */
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
color: #eaf7ff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease, border-left-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:nth-child(even) { background: rgba(0, 212, 255, 0.05); }
|
||||
.table-row:nth-child(odd) { background: rgba(255, 255, 255, 0.02); }
|
||||
|
||||
.table-row:hover {
|
||||
background: rgba(0, 212, 255, 0.12);
|
||||
border-left: 2px solid #00d4ff;
|
||||
}
|
||||
|
||||
.table-row.active {
|
||||
background: rgba(0, 212, 255, 0.22);
|
||||
border-left: 3px solid #00d4ff;
|
||||
box-shadow: inset 0 0 12px rgba(0, 212, 255, 0.35), 0 0 8px rgba(0, 212, 255, 0.25);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.table-row.active .td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 涨跌颜色(左侧列表也复用) */
|
||||
.change-up { color: #00e676; font-weight: 600; }
|
||||
.change-down { color: #ff5252; font-weight: 600; }
|
||||
|
||||
.th { font-size: 14px; }
|
||||
.td { font-size: 14px; }
|
||||
|
||||
/* 仅将左侧“省内各地区牛数据列表”模块文字居中 */
|
||||
.region-card .card-title { text-align: center; }
|
||||
.region-card .region-table { text-align: center; }
|
||||
.region-card .th, .region-card .td { text-align: center; }
|
||||
|
||||
.td .main { color: #eaf7ff; }
|
||||
.td .sub { color: #9ec7d9; font-size: 12px; margin-top: 2px; }
|
||||
|
||||
.change-zero { color: #b9cdd8; }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(0,212,255,0.25);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-item .label { color: #cfefff; font-size: 13px; }
|
||||
.stat-item .value { color: #eaffff; font-size: 22px; font-weight: bold; }
|
||||
.stat-item .unit { color: #9ed7ff; font-size: 12px; }
|
||||
|
||||
.trend-chart { width: 100%; height: 320px; }
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.trend-chart { height: 280px; }
|
||||
.table-header, .table-row { grid-template-columns: 0.9fr 1.3fr 1fr 0.9fr 0.9fr; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.price-content { grid-template-columns: 1fr; }
|
||||
.table-header, .table-row { grid-template-columns: 1fr 1.2fr 1fr 0.9fr 0.9fr; }
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.th, .td { font-size: 13px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user