1378 lines
37 KiB
Vue
Raw Normal View History

2026-03-31 18:10:34 +08:00
<template>
<div class="chongqing-map-container">
<div ref="mapContainer" class="map-container"></div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span class="loading-text">地图加载中...</span>
</div>
<div v-if="error" class="error-overlay">
<span class="error-text">{{ error }}</span>
<button class="retry-btn" @click="loadMapData">重试</button>
</div>
<TongnanCenterCardDialog
ref="tongnanCenterCardDialog"
:visible.sync="visible"
:value="value"
:z-index="zIndex"
:width="width"
></TongnanCenterCardDialog>
<tunnelInfoDialog
v-model:visible="tunnelDialogVisible"
:data="tunnelDialogData"
/>
<mapInfoDialog
v-model:visible="mapInfoDialogVisible"
:type="mapInfoDialogType"
:data="mapInfoDialogData"
/>
<centerInfoCard
:visible="centerCardVisible"
:title="centerCardTitle"
:dataList="centerCardDataList"
@close="closeCenterCard"
@itemClick="handleCenterCardItemClick"
@click="handleCenterCardClick"
/>
2026-03-31 18:10:34 +08:00
</div>
</template>
<script setup>
import {
ref,
onMounted,
onUnmounted,
watch,
defineExpose,
h,
render,
} from "vue";
import axios from "axios";
import { request } from "@/utils/request";
import TongnanCenterCardDialog from "../Dialog/tongnanCenterCardDialog.vue";
2026-03-31 18:10:34 +08:00
import projectIcon from "../../../assets/MaMap_img/项目@2x.png";
import bridgeIcon from "../../../assets/MaMap_img/桥梁icon@2x.png";
import tunnelIcon from "../../../assets/MaMap_img/蓝色@2x1.png";
import tunnelLineIcon from "../../../assets/MaMap_img/线路icon定位@2x.png";
import tunnelIcon2 from "../../../assets/MaMap_img/隧洞icon@2x.png";
import rescueTeamIcon from "../../../assets/MaMap_img/队伍icon@2x.png";
import tunnelInfoDialog from "../Dialog/tunnelInfoDialog.vue";
import mapInfoDialog from "../Dialog/mapInfoDialog.vue";
import centerInfoCard from "../Dialog/centerInfoCard.vue";
2026-04-02 16:35:45 +08:00
const mapContainer = ref(null);
const loading = ref(false);
const error = ref(null);
let mapInstance = null;
let geoJsonLayer = null;
2026-03-31 18:10:34 +08:00
const props = defineProps({
activeIndex: {
type: Number,
default: -1,
},
2026-04-09 14:53:44 +08:00
dateRange: {
type: Array,
default: () => [],
},
});
2026-04-02 16:35:45 +08:00
// 定义 emits
const emit = defineEmits([
"districtClick",
"openTongnanTeam",
"openResponseSituation",
"openTongnanResponsible",
]);
2026-04-02 16:35:45 +08:00
// 当前选中的区县
const selectedDistrict = ref(null);
let selectedLayer = null;
// 隧道信息弹窗
const tunnelDialogVisible = ref(false);
const tunnelDialogData = ref({});
// 地图信息弹窗
const mapInfoDialogVisible = ref(false);
const mapInfoDialogType = ref("project");
const mapInfoDialogData = ref({});
// 中心信息卡片弹窗
const centerCardVisible = ref(false);
const centerCardTitle = ref("调度统计");
const centerCardDataList = ref([]);
// 地图上显示的区县卡片标记
let countyCardMarkers = [];
// 打开地图信息弹窗
const openMapInfoDialog = (type, data) => {
mapInfoDialogType.value = type;
mapInfoDialogData.value = data;
mapInfoDialogVisible.value = true;
};
// 清除地图上的区县卡片标记
const clearCountyCardMarkers = () => {
countyCardMarkers.forEach((marker) => {
// 清理 Vue 组件
if (marker._vueContainer) {
render(null, marker._vueContainer);
}
if (mapInstance) {
mapInstance.removeLayer(marker);
}
});
countyCardMarkers = [];
};
// 在地图上显示区县卡片
const showCountyCardsOnMap = (dataList) => {
if (!mapInstance || !geoJsonLayer) {
console.warn("地图未初始化,无法显示区县卡片");
return;
}
// 清除之前的卡片标记
clearCountyCardMarkers();
if (!dataList || dataList.length === 0) {
return;
}
// 简化区县名称
const simplifyName = (name) => {
return name
.replace("土家族苗族自治县", "")
.replace("苗族土家族自治县", "")
.replace("自治县", "")
.replace("区", "")
.replace("县", "");
};
// 遍历数据列表,为每个区县创建卡片
dataList.forEach((item) => {
const countyName = item.countyName || item.name;
if (!countyName) return;
const targetName = simplifyName(countyName);
// 查找对应的区县图层
let targetLayer = null;
geoJsonLayer.eachLayer((layer) => {
const layerName = layer.feature?.properties?.name || "";
if (simplifyName(layerName) === targetName) {
targetLayer = layer;
}
});
if (targetLayer) {
// 获取区县的中心点
const bounds = targetLayer.getBounds();
const center = bounds.getCenter();
// 创建一个容器元素用于挂载 Vue 组件
const container = document.createElement("div");
container.className = "county-card-wrapper";
// 使用 Vue 的 h 函数创建组件虚拟节点
const vnode = h(centerInfoCard, {
visible: true,
title: countyName,
dataList: [item],
item: item,
onClose: () => {
closeCenterCard();
},
onItemClick: (clickedItem) => {
handleCenterCardItemClick(clickedItem);
},
});
// 渲染组件到容器
render(vnode, container);
// 创建自定义图标,使用渲染后的 HTML
const customIcon = window.L.divIcon({
className: "county-card-icon",
html: container.innerHTML,
iconSize: [150, 40],
iconAnchor: [110, 60],
});
// 创建标记
const marker = window.L.marker(center, {
icon: customIcon,
interactive: true,
});
// 添加点击事件到 marker
marker.on("click", () => {
console.log("centerInfoCard clicked, county:", countyName);
// 调用处理函数
handleCenterCardItemClick(item);
// 移动地图到该位置
mapInstance.setView(center, 10);
// 触发点击事件
emit("districtClick", {
name: countyName,
data: item,
2026-04-13 14:54:48 +08:00
type: item.type,
});
});
marker.addTo(mapInstance);
countyCardMarkers.push(marker);
// 保存容器引用以便后续清理
marker._vueContainer = container;
}
});
// 调整地图视角以显示所有卡片
if (countyCardMarkers.length > 0) {
const group = new window.L.featureGroup(countyCardMarkers);
mapInstance.fitBounds(group.getBounds().pad(0.2));
}
};
// 获取公路类型文本
const getRoadTypeText = (roadType) => {
const roadTypeMap = {
national: "国省道",
rural: "农村公路",
};
return roadTypeMap[roadType] || roadType;
};
// 打开中心信息卡片弹窗
const openCenterCard = (data) => {
centerCardDataList.value = data.dataList || [];
centerCardTitle.value = data.title || "调度统计";
centerCardVisible.value = true;
// 在地图上显示区县卡片
showCountyCardsOnMap(data.dataList);
};
// 关闭中心信息卡片弹窗
const closeCenterCard = () => {
centerCardVisible.value = false;
clearCountyCardMarkers();
};
// 处理中心卡片项点击
const handleCenterCardItemClick = (item) => {
console.log("点击了卡片项:", item);
// 根据区县名称定位地图
if (item.countyName || item.name) {
const countyName = item.countyName || item.name;
locateToDistrict(countyName);
}
};
// 打开隧道信息弹窗(兼容旧代码)
const openTunnelDialog = (data) => {
openMapInfoDialog("tunnel", data);
};
2026-04-09 14:53:44 +08:00
// 格式化日期时间为接口所需格式
const formatDateTime = (date) => {
if (!date) return "";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
const seconds = String(d.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 获取时间参数
const getTimeParams = () => {
if (props.dateRange && props.dateRange.length === 2) {
return {
start: formatDateTime(props.dateRange[0]),
end: formatDateTime(props.dateRange[1]),
};
}
// 默认时间范围当月1号到当前时间
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
return {
start: `${year}-${month}-01 00:00:00`,
end: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`,
};
};
// 受影响对象数据
const affectedCountyData = ref({
byName: {},
sortedList: [],
});
// 获取受影响对象数据
const getAffectedCountyData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
2026-04-09 14:53:44 +08:00
url: "/snow-ops-platform/weather-warning/affected-county",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
console.log("受影响对象数据:", res);
if (res.code === "00000" && res.data) {
// 统计各区县预警数量
const warningStats = countWarningsByCounty(res.data);
console.log("区县预警统计:", warningStats);
affectedCountyData.value = warningStats;
2026-04-09 14:53:44 +08:00
getAffectedProjectData();
getAffectedTunnelData();
getAffectedBridgeData();
getAffectedRoadSectionData();
// getEmergencyForceData();
2026-04-09 14:53:44 +08:00
loadMapData();
}
} catch (error) {
console.error("获取受影响对象数据失败:", error);
}
};
const affectedBridgeData = ref([]);
// 获取受影响桥梁数据
const getAffectedBridgeData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
2026-04-09 14:53:44 +08:00
url: "/snow-ops-platform/weather-warning/affected-object/bridge",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
if (res.code === "00000" && res.data) {
res.data.forEach((item) => {
item.COORDINATE_POINT = [item.GL1_QLJD, item.GL1_QLWD];
});
}
affectedBridgeData.value = res.data;
addProjectMarkers(res.data, bridgeIcon, "bridge");
} catch (error) {
console.error("获取受影响桥梁数据失败:", error);
return [];
}
};
const affectedTunnelData = ref([]);
const tunnelInfoDialogRef = ref(null);
// 获取受影响隧道数据
const getAffectedTunnelData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
2026-04-09 14:53:44 +08:00
url: "/snow-ops-platform/weather-warning/affected-object/tunnel",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
console.log("受影响隧道数据:", res);
if (res.code === "00000" && res.data) {
res.data.forEach((item) => {
item.COORDINATE_POINT = [
Number(item.GL1_SDJD2),
Number(item.GL1_SDWD2),
];
});
tunnelInfoDialogRef.value = res.data;
console.log("受影响隧道数据:", res.data);
addProjectMarkers(res.data, tunnelIcon2, "tunnel");
}
return [];
} catch (error) {
console.error("获取受影响隧道数据失败:", error);
return [];
}
};
const affectedProjectData = ref([]);
// 获取受影响项目数据
const getAffectedProjectData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
2026-04-09 14:53:44 +08:00
url: "/snow-ops-platform/weather-warning/affected-object/project",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
console.log("受影响项目数据:", res);
if (res.code === "00000" && res.data) {
console.log("项目数据条数:", res.data.length);
// 解析坐标数据
const parsedData = res.data.map((item) => {
const newItem = { ...item };
if (item.COORDINATE_POINT) {
console.log("原始坐标:", item.COORDINATE_POINT);
newItem.COORDINATE_POINT = item.COORDINATE_POINT.substring(
6,
item.COORDINATE_POINT.length - 1,
)
.split(" ")
.reverse();
console.log("解析后坐标:", newItem.COORDINATE_POINT);
}
return newItem;
});
affectedProjectData.value = parsedData;
// 在地图上添加项目标记
console.log("开始添加项目标记...");
addProjectMarkers(parsedData, projectIcon, "project");
} else {
console.warn("没有获取到项目数据或返回码错误:", res);
}
return res.data || [];
} catch (error) {
console.error("获取受影响项目数据失败:", error);
return [];
}
};
const affectedRoadSectionData = ref([]);
// 获取受影响路段数据
const getAffectedRoadSectionData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
2026-04-09 14:53:44 +08:00
url: "/snow-ops-platform/weather-warning/affected-object/road-section",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
console.log("受影响路段数据:", res);
if (res.code === "00000" && res.data) {
res.data.forEach((item) => {
item.COORDINATE_POINT = JSON.parse(item.STARTPOINT);
});
affectedRoadSectionData.value = res.data;
// 在地图上添加项目标记
console.log("开始添加项目标记...");
addProjectMarkers(affectedRoadSectionData.value, tunnelLineIcon, "road");
}
return [];
} catch (error) {
console.error("获取受影响路段数据失败:", error);
return [];
}
};
const emergencyForceData = ref([]);
// 获取应急力量数据
const getEmergencyForceData = async () => {
try {
2026-04-09 14:53:44 +08:00
const timeParams = getTimeParams();
const res = await request({
url: "/snow-ops-platform/yhYjll/list",
method: "GET",
2026-04-09 14:53:44 +08:00
params: timeParams,
});
console.log("应急力量数据:", res);
2026-04-09 14:53:44 +08:00
if (res.code === "00000" && res.data) {
// 解析坐标数据
2026-04-09 14:53:44 +08:00
res.data.forEach((item) => {
if (item.gl1Lx == 1 || item.gl1Lx == 2) {
item.COORDINATE_POINT = [item.gl1Lng, item.gl1Lat];
console.log("解析后坐标:", item.COORDINATE_POINT);
}
});
2026-04-09 14:53:44 +08:00
emergencyForceData.value = res.data;
// 在地图上添加应急力量标记
2026-04-09 14:53:44 +08:00
console.log("开始添加应急力量标记...", res.data);
addProjectMarkers(res.data, rescueTeamIcon, "emergency");
} else {
console.warn("没有获取到应急力量数据或返回码错误:", res);
}
return res.data || [];
} catch (error) {
console.error("获取应急力量数据失败:", error);
return [];
}
};
let projectMarkers = []; // 存储项目标记
// 清除项目标记
const clearProjectMarkers = () => {
projectMarkers.forEach((marker) => {
if (mapInstance) {
mapInstance.removeLayer(marker);
}
});
clearCountyCardMarkers();
projectMarkers = [];
// 关闭所有弹窗
tunnelDialogVisible.value = false;
mapInfoDialogVisible.value = false;
centerCardVisible.value = false;
};
// 在地图上添加项目标记
const addProjectMarkers = (data, iconUrl, type = "project") => {
console.log(
"addProjectMarkers 被调用, mapInstance:",
!!mapInstance,
"数据条数:",
data?.length,
"类型:",
type,
);
if (!mapInstance) {
console.warn("mapInstance 未初始化,无法添加标记");
return;
}
if (!data || data.length === 0) {
console.warn("没有数据,无法添加标记");
return;
}
// // 清除之前的标记
// clearProjectMarkers();
// 创建自定义图标
const projectIconObj = window.L.icon({
iconUrl: iconUrl,
2026-04-09 14:53:44 +08:00
iconSize: [35, 35],
iconAnchor: [10, 10],
popupAnchor: [0, -10],
});
// 遍历数据添加标记
data.forEach((item) => {
if (item.COORDINATE_POINT && item.COORDINATE_POINT.length === 2) {
// COORDINATE_POINT 格式: [经度, 纬度]
// Leaflet 需要: [纬度, 经度]
const lng = item.COORDINATE_POINT[0];
const lat = item.COORDINATE_POINT[1];
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
if (!isNaN(latNum) && !isNaN(lngNum)) {
const marker = window.L.marker([latNum, lngNum], {
icon: projectIconObj,
});
// 点击 marker 打开对应类型的信息弹窗
marker.on("click", () => {
openMapInfoDialog(type, item);
});
marker.addTo(mapInstance);
projectMarkers.push(marker);
} else {
console.warn("无效的坐标:", item.COORDINATE_POINT, item);
}
} else {
console.warn("缺少坐标数据:", item);
}
});
console.log(`已添加 ${projectMarkers.length} 个项目标记`);
};
// 确定主要预警颜色(出现最多的预警等级)
const getMainWarningColor = (levels) => {
if (!levels || Object.keys(levels).length === 0) return "#1890ff"; // 默认蓝色
// 预警等级优先级:红色 > 橙色 > 黄色 > 蓝色
const priority = ["红色", "橙色", "黄色", "蓝色"];
// 按数量排序
const sortedLevels = Object.entries(levels)
.map(([level, count]) => ({ level, count }))
.sort((a, b) => b.count - a.count);
// 如果有多个等级数量相同,按优先级排序
const maxCount = sortedLevels[0].count;
const maxLevels = sortedLevels.filter((item) => item.count === maxCount);
if (maxLevels.length === 1) {
return getColorByLevel(maxLevels[0].level);
} else {
// 按优先级选择
for (const level of priority) {
if (maxLevels.some((item) => item.level === level)) {
return getColorByLevel(level);
}
}
return getColorByLevel(maxLevels[0].level);
}
};
// 统计各区县的预警数量(按预警等级区分)
const countWarningsByCounty = (data) => {
if (!data || !Array.isArray(data)) return {};
const stats = {};
data.forEach((item) => {
// 根据实际数据结构调整字段名
const rawCountyName =
item.countyName || item.name || item.qxmc || item.gl1Qxmc;
const riskLevel =
item.riskLevel || item.level || item.warningLevel || item.gl1Yjdj;
// 简化区县名称
const countyName = simplifyDistrictName(rawCountyName);
if (countyName) {
if (!stats[countyName]) {
stats[countyName] = {
total: 0,
levels: {},
};
}
// 统计总数
stats[countyName].total += 1;
// 按预警等级统计
const level = riskLevel || "未知";
if (stats[countyName].levels[level]) {
stats[countyName].levels[level] += 1;
} else {
stats[countyName].levels[level] = 1;
}
}
});
// 合并渝北区和江北区为两江新区
if (stats["渝北区"] || stats["江北区"]) {
const yubeiData = stats["渝北区"] || {
total: 0,
levels: {},
};
const jiangbeiData = stats["江北区"] || {
total: 0,
levels: {},
};
// 合并总数
const total = yubeiData.total + jiangbeiData.total;
// 合并各等级预警数量
const levels = { ...yubeiData.levels };
Object.entries(jiangbeiData.levels).forEach(([level, count]) => {
if (levels[level]) {
levels[level] += count;
} else {
levels[level] = count;
}
});
// 创建两江新区数据
stats["两江新区"] = {
total,
levels,
};
// 删除渝北区和江北区
delete stats["渝北区"];
delete stats["江北区"];
}
// 转换为数组格式并按总数排序(数量从大到小)
const sortedList = Object.entries(stats)
.map(([name, data]) => ({
name,
total: data.total,
levels: data.levels,
}))
.sort((a, b) => b.total - a.total);
return {
byName: stats, // 对象格式:{ 区县名: { total: 总数, levels: { 等级: 数量 } } }
sortedList: sortedList, // 数组格式:[{ name: 区县名, total: 总数, levels: { 等级: 数量 } }]
};
};
2026-04-02 16:35:45 +08:00
// 重庆地图 GeoJSON API 地址 - 使用最新版本
const GEOJSON_URL =
"https://geo.datav.aliyun.com/areas_v3/bound/500000_full.json";
2026-03-31 18:10:34 +08:00
// 加载地图数据
const loadMapData = async () => {
2026-04-02 16:35:45 +08:00
loading.value = true;
error.value = null;
2026-03-31 18:10:34 +08:00
try {
2026-04-02 16:35:45 +08:00
// 初始化地图
2026-04-03 18:08:42 +08:00
// 检查URL参数中是否有Map=dev
const urlParams = new URLSearchParams(window.location.search);
const isDev = urlParams.get("Map") === "dev";
2026-04-03 18:08:42 +08:00
let geoJsonData;
2026-04-03 18:08:42 +08:00
if (isDev) {
// 本地测试用
const response = await fetch(GEOJSON_URL);
2026-04-03 18:08:42 +08:00
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
2026-04-03 18:08:42 +08:00
}
geoJsonData = await response.json();
2026-04-03 18:08:42 +08:00
} else {
// 部署用
const response = await axios.get("/aliyun-geo/bound/500000_full.json");
2026-04-03 18:08:42 +08:00
geoJsonData = response.data;
if (!geoJsonData) {
throw new Error("地图数据为空");
2026-04-03 18:08:42 +08:00
}
}
2026-04-02 16:35:45 +08:00
// // 处理行政区划变更:渝北区和江北区合并为两江新区
processDistrictMerge(geoJsonData);
initMap(geoJsonData);
2026-03-31 18:10:34 +08:00
} catch (err) {
2026-04-02 16:35:45 +08:00
console.error("加载地图数据失败:", err);
error.value = "地图数据加载失败,请检查网络连接";
2026-03-31 18:10:34 +08:00
} finally {
2026-04-02 16:35:45 +08:00
loading.value = false;
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
};
// 处理行政区划合并
const processDistrictMerge = (geoJsonData) => {
if (!geoJsonData || !geoJsonData.features) return;
// 查找渝北区和江北区的特征
const yubeiIndex = geoJsonData.features.findIndex(
(f) => f.properties && f.properties.name === "渝北区",
);
const jiangbeiIndex = geoJsonData.features.findIndex(
(f) => f.properties && f.properties.name === "江北区",
);
// 如果两个区都存在,合并为两江新区
if (yubeiIndex !== -1 && jiangbeiIndex !== -1) {
const yubeiFeature = geoJsonData.features[yubeiIndex];
const jiangbeiFeature = geoJsonData.features[jiangbeiIndex];
// 创建两江新区的特征
const liangjiangFeature = JSON.parse(JSON.stringify(yubeiFeature));
liangjiangFeature.properties.name = "两江新区";
liangjiangFeature.properties.adcode = "500006"; // 使用新的行政区划代码
// 如果是 MultiPolygon合并两个区的坐标
if (
yubeiFeature.geometry.type === "MultiPolygon" &&
jiangbeiFeature.geometry.type === "MultiPolygon"
) {
liangjiangFeature.geometry.coordinates = [
...yubeiFeature.geometry.coordinates,
...jiangbeiFeature.geometry.coordinates,
];
} else if (
yubeiFeature.geometry.type === "Polygon" &&
jiangbeiFeature.geometry.type === "Polygon"
) {
liangjiangFeature.geometry.type = "MultiPolygon";
liangjiangFeature.geometry.coordinates = [
[yubeiFeature.geometry.coordinates],
[jiangbeiFeature.geometry.coordinates],
];
}
// 移除原来的两个区
geoJsonData.features.splice(Math.max(yubeiIndex, jiangbeiIndex), 1);
geoJsonData.features.splice(Math.min(yubeiIndex, jiangbeiIndex), 1);
// 添加合并后的两江新区
geoJsonData.features.push(liangjiangFeature);
console.log("已合并渝北区和江北区为两江新区");
}
};
// 计算区县中心点
const getCentroid = (coordinates) => {
let sumLat = 0;
let sumLng = 0;
let count = 0;
const processCoordinates = (coords) => {
if (typeof coords[0] === "number") {
// 单个坐标点 [lng, lat]
sumLng += coords[0];
sumLat += coords[1];
count++;
} else if (Array.isArray(coords[0])) {
// 坐标数组
coords.forEach(processCoordinates);
}
};
processCoordinates(coordinates);
return count > 0 ? [sumLat / count, sumLng / count] : null;
};
2026-03-31 18:10:34 +08:00
// 简化区县名称
const simplifyDistrictName = (name) => {
const nameMap = {
酉阳土家族苗族自治县: "酉阳县",
秀山土家族苗族自治县: "秀山县",
彭水苗族土家族自治县: "彭水县",
石柱土家族自治县: "石柱县",
};
return nameMap[name] || name;
};
// 根据预警等级获取颜色
const getColorByLevel = (level) => {
const colorMap = {
2026-04-09 14:53:44 +08:00
红色预警: "#912210",
橙色预警: "#BA6527",
黄色预警: "#A47109",
蓝色预警: "#185A91",
未知: "#1890ff",
};
return colorMap[level] || "#1890ff";
};
2026-03-31 18:10:34 +08:00
// 初始化地图
const initMap = (geoJsonData) => {
2026-04-02 16:35:45 +08:00
if (!mapContainer.value) return;
2026-03-31 18:10:34 +08:00
try {
// 清除旧地图实例
if (mapInstance) {
2026-04-02 16:35:45 +08:00
mapInstance.remove();
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
2026-03-31 18:10:34 +08:00
// 创建地图实例
mapInstance = new window.L.Map(mapContainer.value, {
center: [29.563, 106.551], // 重庆中心坐标
2026-04-09 14:53:44 +08:00
zoom: 8,
2026-04-02 16:35:45 +08:00
minZoom: 6,
2026-03-31 18:10:34 +08:00
maxZoom: 18,
zoomControl: false,
2026-04-02 16:35:45 +08:00
attributionControl: false,
});
// 添加瓦片图层 - 使用深色样式
2026-04-03 18:08:42 +08:00
// const tileLayer = new window.L.TileLayer(
// "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
// {
// attribution:
// '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
// subdomains: "abcd",
// maxZoom: 19,
// },
// );
// mapInstance.addLayer(tileLayer);
2026-04-02 16:35:45 +08:00
// 添加 GeoJSON 图层
geoJsonLayer = new window.L.GeoJSON(geoJsonData, {
style: (feature) => {
// const districtName = feature.properties.name;
const districtName = simplifyDistrictName(feature.properties.name);
2026-04-09 14:53:44 +08:00
let fillColor = "#132C44"; // 默认深蓝色背景
let fillOpacity = 0.4; // 默认透明度更高(更透明)
// 如果有预警统计数据,应用主要预警颜色
if (
affectedCountyData.value &&
affectedCountyData.value.byName &&
affectedCountyData.value.byName[districtName]
) {
const districtData = affectedCountyData.value.byName[districtName];
// console.log(districtData.levels);
fillColor = getMainWarningColor(districtData.levels);
2026-04-09 14:53:44 +08:00
fillOpacity = 1; // 有预警时稍微不透明一些
}
return {
fillColor: fillColor,
2026-04-09 14:53:44 +08:00
weight: 0.5,
opacity: 0.6,
color: "#00d4ff",
fillOpacity: fillOpacity,
};
},
2026-03-31 18:10:34 +08:00
onEachFeature: (feature, layer) => {
if (feature.properties && feature.properties.name) {
2026-04-02 16:35:45 +08:00
// 区县点击事件
// layer.on("click", (e) => {
// // 清除之前选中的样式
// if (selectedLayer) {
// selectedLayer.setStyle({
// fillColor: "#1890ff",
// fillOpacity: 0.15,
// weight: 1.5,
// color: "#40a9ff",
// });
// }
// // 设置当前选中样式
// layer.setStyle({
// fillColor: "#ff4d4f",
// fillOpacity: 0.6,
// weight: 2.5,
// color: "#ff7875",
// });
// selectedLayer = layer;
// selectedDistrict.value = feature.properties.name;
// // 触发事件
// emit("districtClick", {
// name: feature.properties.name,
// feature: feature,
// latlng: e.latlng,
// });
// // 平滑移动到点击位置
// mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 });
// });
2026-04-02 16:35:45 +08:00
// 鼠标悬停效果
// layer.on("mouseover", () => {
// layer.setStyle({
// fillColor: "#1890ff",
// fillOpacity: 0.4,
// weight: 2,
// color: "#69c0ff",
// });
// });
// layer.on("mouseout", () => {
// layer.setStyle({
// fillColor: "#1890ff",
// fillOpacity: 0.15,
// weight: 1.5,
// color: "#40a9ff",
// });
// });
2026-04-02 16:35:45 +08:00
// 添加 popup
// layer.bindPopup(`<div class="map-popup">
// <strong>${feature.properties.name}</strong>
// </div>`);
2026-04-02 16:35:45 +08:00
}
},
});
mapInstance.addLayer(geoJsonLayer);
// 添加区县标签
geoJsonData.features.forEach((feature) => {
if (feature.properties && feature.properties.name) {
let centroid;
if (feature.geometry.type === "Polygon") {
centroid = getCentroid(feature.geometry.coordinates);
} else if (feature.geometry.type === "MultiPolygon") {
// 取第一个多边形的中心
centroid = getCentroid(feature.geometry.coordinates[0]);
}
if (centroid) {
const displayName = simplifyDistrictName(feature.properties.name);
2026-04-02 16:35:45 +08:00
const label = window.L.divIcon({
className: "district-label",
html: `<div class="label-content">${displayName}</div>`,
2026-04-02 16:35:45 +08:00
iconSize: [80, 30],
iconAnchor: [40, 15],
});
const marker = window.L.marker(centroid, { icon: label });
// marker.on("click", (e) => {
// // 清除之前选中的样式
// if (selectedLayer) {
// selectedLayer.setStyle({
// fillColor: "#1E3A8A",
// fillOpacity: 0.3,
// weight: 2,
// });
// }
// // 找到对应的区县图层并设置样式
// geoJsonLayer.eachLayer((layer) => {
// if (
// layer.feature &&
// layer.feature.properties.name === feature.properties.name
// ) {
// layer.setStyle({
// fillColor: "#ff4d4f",
// fillOpacity: 0.6,
// weight: 2.5,
// color: "#ff7875",
// });
// selectedLayer = layer;
// selectedDistrict.value = feature.properties.name;
// }
// });
// // 触发事件
// emit("districtClick", {
// name: feature.properties.name,
// feature: feature,
// latlng: e.latlng,
// });
// // 平滑移动到点击位置
// mapInstance.panTo(e.latlng, { animate: true, duration: 0.5 });
// });
2026-04-02 16:35:45 +08:00
mapInstance.addLayer(marker);
2026-03-31 18:10:34 +08:00
}
}
2026-04-02 16:35:45 +08:00
});
2026-03-31 18:10:34 +08:00
// 调整视图以适应重庆边界
2026-04-02 16:35:45 +08:00
mapInstance.fitBounds(geoJsonLayer.getBounds());
2026-03-31 18:10:34 +08:00
} catch (err) {
2026-04-02 16:35:45 +08:00
console.error("初始化地图失败:", err);
error.value = "地图初始化失败";
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
};
2026-03-31 18:10:34 +08:00
// 监听 activeIndex 变化
watch(
() => props.activeIndex,
async (newVal) => {
console.log("activeIndex 变化:", newVal);
switch (newVal) {
case 0:
await getAffectedProjectData();
break;
case 1:
await getAffectedTunnelData();
break;
case 3:
await getAffectedBridgeData();
break;
case 4:
await getAffectedRoadSectionData();
break;
2026-04-09 14:53:44 +08:00
case 5:
await getEmergencyForceData();
break;
default:
break;
}
},
{ immediate: false },
);
2026-04-09 14:53:44 +08:00
// 监听 dateRange 变化,重新加载数据
watch(
() => props.dateRange,
async (newVal, oldVal) => {
console.log("dateRange 变化:", newVal, oldVal);
// 先重新加载受影响区县数据
await getAffectedCountyData();
// 根据当前 activeIndex 重新加载对应数据
switch (props.activeIndex) {
case 0:
await getAffectedProjectData();
break;
case 1:
await getAffectedTunnelData();
break;
case 3:
await getAffectedBridgeData();
break;
case 4:
await getEmergencyForceData();
break;
default:
break;
2026-04-09 14:53:44 +08:00
}
},
{ deep: true },
);
2026-03-31 18:10:34 +08:00
// 组件挂载时加载地图
onMounted(() => {
// 获取受影响对象数据
getAffectedCountyData();
2026-04-02 16:35:45 +08:00
// 检查 Leaflet 是否已加载
if (typeof window.L === "undefined") {
// 动态加载 Leaflet CSS 和 JS
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
link.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
link.crossOrigin = "";
document.head.appendChild(link);
const script = document.createElement("script");
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
script.integrity = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
script.crossOrigin = "";
script.onload = loadMapData;
document.head.appendChild(script);
2026-03-31 18:10:34 +08:00
} else {
2026-04-02 16:35:45 +08:00
loadMapData();
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
});
2026-03-31 18:10:34 +08:00
// 组件卸载时清理资源
onUnmounted(() => {
if (mapInstance) {
2026-04-02 16:35:45 +08:00
mapInstance.remove();
mapInstance = null;
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
});
// 根据区县名称定位地图
const locateToDistrict = (countyName) => {
if (!mapInstance || !geoJsonLayer) {
console.warn("地图未初始化,无法定位");
return;
}
// 简化区县名称
const simplifyName = (name) => {
return name
.replace("土家族苗族自治县", "")
.replace("苗族土家族自治县", "")
.replace("自治县", "")
.replace("区", "")
.replace("县", "");
};
const targetName = simplifyName(countyName);
console.log("定位到区县:", countyName, "简化后:", targetName);
// 查找对应的区县图层
let targetLayer = null;
geoJsonLayer.eachLayer((layer) => {
const layerName = layer.feature?.properties?.name || "";
if (simplifyName(layerName) === targetName) {
targetLayer = layer;
}
});
if (targetLayer) {
// 获取区县的中心点
const bounds = targetLayer.getBounds();
const center = bounds.getCenter();
// 移动地图到该中心点并放大
mapInstance.setView(center, 10);
// 高亮显示该区县
if (selectedLayer) {
geoJsonLayer.resetStyle(selectedLayer);
}
2026-04-13 14:54:48 +08:00
// targetLayer.setStyle({
// fillColor: "#ff7a00",
// fillOpacity: 0.6,
// weight: 3,
// color: "#ff4d4f",
// });
selectedLayer = targetLayer;
console.log("已定位到区县:", countyName, "中心点:", center);
} else {
console.warn("未找到区县:", countyName);
}
};
// 暴露方法给父组件调用
defineExpose({
getEmergencyForceData,
clearProjectMarkers,
openCenterCard,
locateToDistrict,
});
2026-03-31 18:10:34 +08:00
</script>
2026-04-02 16:35:45 +08:00
<style lang="scss">
// 区县标签样式(需要全局样式,因为 Leaflet 动态添加元素)
.district-label {
background: transparent !important;
border: none !important;
.label-content {
background: transparent;
color: #fff;
padding: vw(4) vw(8);
border-radius: vw(2);
font-weight: 500;
text-align: center;
white-space: nowrap;
text-shadow:
0 1px 3px rgba(0, 0, 0, 0.8),
0 0 2px rgba(0, 0, 0, 0.5);
letter-spacing: 0.5px;
cursor: pointer;
width: fit-content;
transition: all 0.3s;
&:hover {
color: #40a9ff;
text-shadow: 0 0 10px rgba(64, 169, 255, 0.8);
transform: scale(1.05);
}
}
}
</style>
2026-03-31 18:10:34 +08:00
<style lang="scss" scoped>
2026-04-02 16:35:45 +08:00
// 视频屏幕自适应 - 基于视口宽度动态调整
@function vw($px) {
@return calc($px / 1920 * 100vw);
}
2026-03-31 18:10:34 +08:00
.chongqing-map-container {
width: 100%;
height: 100%;
position: relative;
}
.map-container {
width: 100%;
height: 100%;
}
.loading-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
}
.loading-spinner {
2026-04-02 16:35:45 +08:00
width: vw(40);
height: vw(40);
border: vw(4) solid #3b82f6;
border-top: vw(4) solid transparent;
2026-03-31 18:10:34 +08:00
border-radius: 50%;
animation: spin 1s linear infinite;
2026-04-02 16:35:45 +08:00
margin-bottom: vw(10);
2026-03-31 18:10:34 +08:00
}
.loading-text {
color: #fff;
2026-04-02 16:35:45 +08:00
font-size: vw(14);
2026-03-31 18:10:34 +08:00
}
.error-text {
color: #ff6b6b;
2026-04-02 16:35:45 +08:00
font-size: vw(14);
margin-bottom: vw(10);
2026-03-31 18:10:34 +08:00
}
.retry-btn {
2026-04-02 16:35:45 +08:00
background: #3b82f6;
2026-03-31 18:10:34 +08:00
color: white;
border: none;
2026-04-02 16:35:45 +08:00
padding: vw(8) vw(16);
border-radius: vw(4);
2026-03-31 18:10:34 +08:00
cursor: pointer;
2026-04-02 16:35:45 +08:00
font-size: vw(12);
2026-03-31 18:10:34 +08:00
&:hover {
2026-04-02 16:35:45 +08:00
background: #2563eb;
2026-03-31 18:10:34 +08:00
}
}
@keyframes spin {
2026-04-02 16:35:45 +08:00
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
// Leaflet 地图样式覆盖
2026-03-31 18:10:34 +08:00
:deep(.leaflet-container) {
background: #0f1c2e !important;
.leaflet-popup {
left: -20px !important;
}
2026-04-02 16:35:45 +08:00
2026-03-31 18:10:34 +08:00
.leaflet-popup-content-wrapper {
2026-04-02 16:35:45 +08:00
background: rgba(24, 144, 255, 0.95);
border-radius: vw(4);
padding: vw(6) vw(12);
min-width: vw(80);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
2026-03-31 18:10:34 +08:00
.map-popup {
color: #fff;
2026-04-02 16:35:45 +08:00
font-size: vw(12);
text-align: center;
margin: 0;
padding: 0;
font-weight: 500;
2026-03-31 18:10:34 +08:00
}
}
2026-04-02 16:35:45 +08:00
2026-03-31 18:10:34 +08:00
.leaflet-popup-tip {
2026-04-02 16:35:45 +08:00
background: rgba(24, 144, 255, 0.95);
2026-03-31 18:10:34 +08:00
}
2026-04-02 16:35:45 +08:00
.leaflet-popup-content {
width: max-content !important;
margin: vw(10) 10px vw(10) 0;
line-height: 1.3;
}
}
// 区县高亮样式
:deep(.leaflet-interactive) {
transition: all 0.3s;
cursor: pointer;
2026-03-31 18:10:34 +08:00
}
.project-popup {
width: vw(120);
height: vw(20);
color: #fff;
font-size: vw(12);
text-align: center;
margin: 0;
padding: 15px;
font-weight: 500;
}
// 区县卡片样式
:deep(.county-card-icon) {
background: transparent !important;
border: none !important;
.center-info-card-container {
width: 100%;
min-width: 150px;
}
.center-info-card {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(24, 144, 255, 0.5);
filter: brightness(1.1);
}
}
}
2026-04-02 16:35:45 +08:00
</style>