509 lines
13 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>
</div>
</template>
<script setup>
2026-04-02 16:35:45 +08:00
import { ref, onMounted, onUnmounted } from "vue";
import axios from 'axios';
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;
// 重庆地图 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';
let geoJsonData;
2026-03-31 18:10:34 +08:00
2026-04-03 18:08:42 +08:00
if (isDev) {
// 本地测试用
const response = await fetch(GEOJSON_URL)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
geoJsonData = await response.json()
} else {
// 部署用
const response = await axios.get('/aliyun-geo/bound/500000_full.json');
geoJsonData = response.data;
if (!geoJsonData) {
throw new Error('地图数据为空');
}
}
2026-04-02 16:35:45 +08:00
2026-03-31 18:10:34 +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 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-02 16:35:45 +08:00
zoom: 7,
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) => ({
fillColor: "#1890ff",
weight: 1.5,
opacity: 0.6,
color: "#40a9ff",
fillOpacity: 0.15,
}),
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 });
});
// 鼠标悬停效果
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",
});
});
// 添加 popup
2026-03-31 18:10:34 +08:00
layer.bindPopup(`<div class="map-popup">
<strong>${feature.properties.name}</strong>
2026-04-02 16:35:45 +08:00
</div>`);
}
},
});
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 label = window.L.divIcon({
className: "district-label",
html: `<div class="label-content">${feature.properties.name}</div>`,
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 });
});
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-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>