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>
|
2026-04-08 15:34:49 +08:00
|
|
|
|
<TongnanCenterCardDialog
|
|
|
|
|
|
ref="tongnanCenterCardDialog"
|
|
|
|
|
|
:visible.sync="visible"
|
|
|
|
|
|
:value="value"
|
|
|
|
|
|
:z-index="zIndex"
|
|
|
|
|
|
:width="width"
|
|
|
|
|
|
></TongnanCenterCardDialog>
|
2026-03-31 18:10:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-04-02 16:35:45 +08:00
|
|
|
|
import { ref, onMounted, onUnmounted } from "vue";
|
2026-04-08 15:34:49 +08:00
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
import { request } from "@/utils/request";
|
|
|
|
|
|
import TongnanCenterCardDialog from "@/views/RiskWarning/Dialog/tongnanCenterCardDialog.vue";
|
2026-03-31 18:10:34 +08:00
|
|
|
|
|
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
|
|
|
|
|
2026-04-02 16:35:45 +08:00
|
|
|
|
// 定义 emits
|
|
|
|
|
|
const emit = defineEmits(["districtClick"]);
|
|
|
|
|
|
|
|
|
|
|
|
// 当前选中的区县
|
|
|
|
|
|
const selectedDistrict = ref(null);
|
|
|
|
|
|
let selectedLayer = null;
|
|
|
|
|
|
|
2026-04-08 15:34:49 +08:00
|
|
|
|
// 受影响对象数据
|
|
|
|
|
|
const affectedCountyData = ref({
|
|
|
|
|
|
byName: {},
|
|
|
|
|
|
sortedList: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取受影响对象数据
|
|
|
|
|
|
const getAffectedCountyData = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await request({
|
|
|
|
|
|
url: "snow-ops-platform/weather-warning/affected-county",
|
|
|
|
|
|
method: "GET",
|
|
|
|
|
|
params: {
|
|
|
|
|
|
start: "2025-03-03 12:33:00",
|
|
|
|
|
|
end: "2025-07-30 12:33:00",
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log("受影响对象数据:", res);
|
|
|
|
|
|
if (res.code === "00000" && res.data) {
|
|
|
|
|
|
// 统计各区县预警数量
|
|
|
|
|
|
const warningStats = countWarningsByCounty(res.data);
|
|
|
|
|
|
console.log("区县预警统计:", warningStats);
|
|
|
|
|
|
affectedCountyData.value = warningStats;
|
|
|
|
|
|
loadMapData();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("获取受影响对象数据失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 确定主要预警颜色(出现最多的预警等级)
|
|
|
|
|
|
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 getColorByLevel = (level) => {
|
|
|
|
|
|
const colorMap = {
|
|
|
|
|
|
红色预警: "#FF4D4F",
|
|
|
|
|
|
橙色预警: "#EC7345",
|
|
|
|
|
|
黄色预警: "#FBC23C",
|
|
|
|
|
|
蓝色预警: "#3799FC",
|
|
|
|
|
|
未知: "#1890ff",
|
|
|
|
|
|
};
|
|
|
|
|
|
return colorMap[level] || "#1890ff";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 统计各区县的预警数量(按预警等级区分)
|
|
|
|
|
|
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);
|
2026-04-08 15:34:49 +08:00
|
|
|
|
const isDev = urlParams.get("Map") === "dev";
|
|
|
|
|
|
|
2026-04-03 18:08:42 +08:00
|
|
|
|
let geoJsonData;
|
2026-04-08 15:34:49 +08:00
|
|
|
|
|
2026-04-03 18:08:42 +08:00
|
|
|
|
if (isDev) {
|
|
|
|
|
|
// 本地测试用
|
2026-04-08 15:34:49 +08:00
|
|
|
|
const response = await fetch(GEOJSON_URL);
|
2026-04-03 18:08:42 +08:00
|
|
|
|
if (!response.ok) {
|
2026-04-08 15:34:49 +08:00
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
2026-04-03 18:08:42 +08:00
|
|
|
|
}
|
2026-04-08 15:34:49 +08:00
|
|
|
|
geoJsonData = await response.json();
|
2026-04-03 18:08:42 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 部署用
|
2026-04-08 15:34:49 +08:00
|
|
|
|
const response = await axios.get("/aliyun-geo/bound/500000_full.json");
|
2026-04-03 18:08:42 +08:00
|
|
|
|
geoJsonData = response.data;
|
|
|
|
|
|
if (!geoJsonData) {
|
2026-04-08 15:34:49 +08:00
|
|
|
|
throw new Error("地图数据为空");
|
2026-04-03 18:08:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 16:35:45 +08:00
|
|
|
|
|
|
|
|
|
|
// // 处理行政区划变更:渝北区和江北区合并为两江新区
|
|
|
|
|
|
processDistrictMerge(geoJsonData);
|
2026-04-08 15:34:49 +08:00
|
|
|
|
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
|
|
|
|
|
2026-04-08 15:34:49 +08:00
|
|
|
|
// 简化区县名称
|
|
|
|
|
|
const simplifyDistrictName = (name) => {
|
|
|
|
|
|
const nameMap = {
|
|
|
|
|
|
酉阳土家族苗族自治县: "酉阳县",
|
|
|
|
|
|
秀山土家族苗族自治县: "秀山县",
|
|
|
|
|
|
彭水苗族土家族自治县: "彭水县",
|
|
|
|
|
|
石柱土家族自治县: "石柱县",
|
|
|
|
|
|
};
|
|
|
|
|
|
return nameMap[name] || name;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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-08 15:34:49 +08:00
|
|
|
|
zoom: 6,
|
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:
|
|
|
|
|
|
// '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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, {
|
2026-04-08 15:34:49 +08:00
|
|
|
|
style: (feature) => {
|
|
|
|
|
|
const districtName = feature.properties.name;
|
|
|
|
|
|
let fillColor = "#132C44"; // 默认
|
|
|
|
|
|
// 如果有预警统计数据,应用主要预警颜色
|
|
|
|
|
|
if (
|
|
|
|
|
|
affectedCountyData.value &&
|
|
|
|
|
|
affectedCountyData.value.byName &&
|
|
|
|
|
|
affectedCountyData.value.byName[districtName]
|
|
|
|
|
|
) {
|
|
|
|
|
|
const districtData = affectedCountyData.value.byName[districtName];
|
|
|
|
|
|
console.log(districtData.levels);
|
|
|
|
|
|
fillColor = getMainWarningColor(districtData.levels);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
fillColor: fillColor,
|
|
|
|
|
|
weight: 1.5,
|
|
|
|
|
|
opacity: 0.6,
|
|
|
|
|
|
color: "#40a9ff",
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
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
|
|
|
|
// 区县点击事件
|
2026-04-08 15:34:49 +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
|
|
|
|
// 鼠标悬停效果
|
2026-04-08 15:34:49 +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
|
2026-04-08 15:34:49 +08:00
|
|
|
|
// 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) {
|
2026-04-08 15:34:49 +08:00
|
|
|
|
const displayName = simplifyDistrictName(feature.properties.name);
|
2026-04-02 16:35:45 +08:00
|
|
|
|
const label = window.L.divIcon({
|
|
|
|
|
|
className: "district-label",
|
2026-04-08 15:34:49 +08:00
|
|
|
|
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 });
|
2026-04-08 15:34:49 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
// 组件挂载时加载地图
|
|
|
|
|
|
onMounted(() => {
|
2026-04-08 15:34:49 +08:00
|
|
|
|
// 获取受影响对象数据
|
|
|
|
|
|
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
|
|
|
|
});
|
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%;
|
|
|
|
|
|
background: #0f1c2e;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
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
|
|
|
|
}
|
2026-04-02 16:35:45 +08:00
|
|
|
|
</style>
|