| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- <template>
- <div class="map-panel">
- <div ref="mapRef" class="leaflet-map"></div>
- <div class="map-toolbar">
- <a-space>
- <a-switch v-model:checked="stationVisible" size="small" />
- <span>场站点</span>
- </a-space>
- </div>
- </div>
- </template>
- <script setup>
- import {nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue';
- const jiangmenStations = [
- {name: '江门站', lat: 22.5789, lng: 113.0819, type: 'solar', status: 'normal', power: 128.6},
- {name: '江门站-光伏侧', lat: 22.66, lng: 112.96, type: 'solar', status: 'normal', power: 86.2},
- {name: '江门站-储能侧', lat: 22.48, lng: 113.18, type: 'solar', status: 'warning', power: 154.8}
- ];
- const props = defineProps({
- stations: {
- type: Array,
- default: () => [
- {name: '甘肃酒泉风电场', lat: 39.74, lng: 98.51, type: 'wind', status: 'normal', power: 128.6},
- {name: '宁夏中卫光伏站', lat: 37.52, lng: 105.18, type: 'solar', status: 'normal', power: 86.2},
- {name: '陕西榆林风光站', lat: 38.29, lng: 109.74, type: 'hybrid', status: 'warning', power: 154.8},
- {name: '青海海南光伏站', lat: 36.28, lng: 100.62, type: 'solar', status: 'normal', power: 72.4},
- {name: '内蒙古鄂尔多斯风电场', lat: 39.61, lng: 109.78, type: 'wind', status: 'normal', power: 190.3}
- ]
- },
- useFallbackStations: {
- type: Boolean,
- default: true
- }
- });
- const mapRef = ref(null);
- const stationVisible = ref(true);
- let map;
- let provinceLayer;
- let labelLayer;
- let stationLayer;
- const ensureLeaflet = () => {
- if (window.L) {
- return Promise.resolve(window.L);
- }
- return new Promise((resolve, reject) => {
- const existingCss = document.querySelector('link[data-leaflet-css]');
- if (!existingCss) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.href = '/weather/leaflet/leaflet.css';
- link.dataset.leafletCss = 'true';
- document.head.appendChild(link);
- }
- const existingScript = document.querySelector('script[data-leaflet-js]');
- if (existingScript) {
- existingScript.addEventListener('load', () => resolve(window.L), {once: true});
- existingScript.addEventListener('error', reject, {once: true});
- return;
- }
- const script = document.createElement('script');
- script.src = '/weather/leaflet/leaflet.js';
- script.dataset.leafletJs = 'true';
- script.onload = () => resolve(window.L);
- script.onerror = reject;
- document.body.appendChild(script);
- });
- };
- const initMap = async () => {
- await nextTick();
- const L = await ensureLeaflet();
- if (!mapRef.value || map) {
- return;
- }
- map = L.map(mapRef.value, {
- center: [22.58, 113.08],
- zoom: 7,
- minZoom: 3,
- maxZoom: 10,
- zoomControl: false,
- attributionControl: false,
- preferCanvas: true
- });
- L.control.zoom({position: 'bottomright'}).addTo(map);
- await loadProvinceLayer(L);
- renderStations(L);
- setTimeout(() => map?.invalidateSize(), 120);
- };
- const loadProvinceLayer = async (L) => {
- try {
- const response = await fetch('/weather/cn-province.geojson');
- const geoJson = await response.json();
- provinceLayer = L.geoJSON(geoJson, {
- interactive: false,
- style: {
- color: '#9a8666',
- weight: 0.8,
- opacity: 0.78,
- fillColor: '#f3f1e9',
- fillOpacity: 0.76
- }
- }).addTo(map);
- labelLayer = L.layerGroup();
- geoJson.features.forEach((feature) => {
- const name = getFeatureName(feature);
- const position = getFeatureCenter(feature);
- if (!name || !position) {
- return;
- }
- L.marker(position, {
- icon: L.divIcon({
- className: 'province-name-label',
- html: `<span>${name}</span>`,
- iconSize: [0, 0],
- iconAnchor: [0, 0]
- }),
- interactive: false
- }).addTo(labelLayer);
- });
- labelLayer.addTo(map);
- } catch (error) {
- console.error('加载省界地图失败:', error);
- }
- };
- const getFeatureName = (feature) => {
- const properties = feature?.properties || {};
- return properties.name || properties.NAME || properties.NAME_CHN || properties.fullname;
- };
- const getFeatureCenter = (feature) => {
- const geometry = feature?.geometry;
- if (!geometry) {
- return null;
- }
- const rings = [];
- if (geometry.type === 'Polygon') {
- rings.push(geometry.coordinates?.[0] || []);
- } else if (geometry.type === 'MultiPolygon') {
- geometry.coordinates?.forEach((polygon) => rings.push(polygon?.[0] || []));
- }
- const ring = rings.sort((a, b) => b.length - a.length)[0];
- if (!ring?.length) {
- return null;
- }
- const bounds = ring.reduce(
- (result, point) => ({
- minLng: Math.min(result.minLng, point[0]),
- maxLng: Math.max(result.maxLng, point[0]),
- minLat: Math.min(result.minLat, point[1]),
- maxLat: Math.max(result.maxLat, point[1])
- }),
- {minLng: Infinity, maxLng: -Infinity, minLat: Infinity, maxLat: -Infinity}
- );
- return [(bounds.minLat + bounds.maxLat) / 2, (bounds.minLng + bounds.maxLng) / 2];
- };
- const renderStations = (L = window.L) => {
- if (!map || !L) {
- return;
- }
- if (stationLayer) {
- stationLayer.remove();
- }
- stationLayer = L.layerGroup();
- if (stationVisible.value) {
- const stations = getVisibleStations();
- stations.forEach((station) => {
- const marker = L.marker([station.lat, station.lng], {
- icon: L.divIcon({
- className: 'power-station-marker',
- html: `<div class="station-pin station-${station.type || 'hybrid'} ${
- station.status === 'warning' ? 'is-warning' : ''
- }"><span>${getStationIcon(station.type)}</span></div>`,
- iconSize: [30, 30],
- iconAnchor: [15, 30],
- popupAnchor: [0, -28]
- })
- });
- marker.bindTooltip(station.name, {
- direction: 'top',
- offset: [0, -24],
- opacity: 0.96
- });
- marker.bindPopup(`
- <div class="station-popup">
- <b>${station.name}</b>
- <span>类型:${getStationTypeName(station.type)}</span>
- <span>实时功率:${station.power || '--'} MW</span>
- <span>状态:${station.status === 'warning' ? '告警' : '正常'}</span>
- </div>
- `);
- marker.addTo(stationLayer);
- });
- if (stations.length) {
- const bounds = stations.map((station) => [station.lat, station.lng]);
- map.fitBounds(bounds, {padding: [32, 32], maxZoom: 8});
- }
- }
- stationLayer.addTo(map);
- };
- const getStationIcon = (type) => {
- const iconMap = {
- wind: '风',
- solar: '光',
- hybrid: '电'
- };
- return iconMap[type] || '电';
- };
- const getVisibleStations = () => {
- const sourceStations = props.useFallbackStations && isLegacyDefaultStations(props.stations) ? jiangmenStations : props.stations;
- const validStations = sourceStations
- .filter((station) => Number.isFinite(Number(station.lat)) && Number.isFinite(Number(station.lng)))
- .map((station, index) => ({
- ...station,
- lat: Number(station.lat),
- lng: Number(station.lng),
- type: station.type || 'hybrid',
- name: station.name || station.stationName || station.entityName || (index === 0 ? '江门站' : `江门站-${index + 1}`)
- }));
- if (validStations.length) {
- return validStations;
- }
- return props.useFallbackStations ? jiangmenStations : [];
- };
- const isLegacyDefaultStations = (stations) => {
- return stations.length === 5
- && Number(stations[0]?.lat) === 39.74
- && Number(stations[0]?.lng) === 98.51;
- };
- const getStationTypeName = (type) => {
- const typeMap = {
- wind: '风电场',
- solar: '光伏电站',
- hybrid: '风光场站'
- };
- return typeMap[type] || '新能源场站';
- };
- onMounted(initMap);
- onBeforeUnmount(() => {
- if (provinceLayer) {
- provinceLayer.remove();
- provinceLayer = null;
- }
- if (labelLayer) {
- labelLayer.remove();
- labelLayer = null;
- }
- if (stationLayer) {
- stationLayer.remove();
- stationLayer = null;
- }
- if (map) {
- map.remove();
- map = null;
- }
- });
- watch(stationVisible, () => renderStations());
- watch(
- () => props.stations,
- () => renderStations(),
- {deep: true}
- );
- </script>
- <style lang="less" scoped>
- .map-panel {
- position: relative;
- min-height: 420px;
- overflow: hidden;
- background: #eef1f4;
- border-radius: 6px;
- .leaflet-map {
- position: absolute;
- inset: 0;
- background: #f5f4ee;
- }
- .map-toolbar {
- position: absolute;
- z-index: 900;
- top: 14px;
- left: 14px;
- right: 14px;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- pointer-events: none;
- :deep(.ant-space) {
- height: 32px;
- padding: 0 10px;
- border: 1px solid rgba(192, 199, 209, 0.72);
- border-radius: 6px;
- color: #334155;
- background: rgba(255, 255, 255, 0.9);
- box-shadow: 0 6px 16px rgba(31, 43, 62, 0.1);
- font-size: 12px;
- pointer-events: auto;
- backdrop-filter: blur(6px);
- }
- }
- :deep(.leaflet-control-zoom) {
- overflow: hidden;
- border: 1px solid rgba(53, 64, 82, 0.18);
- border-radius: 6px;
- box-shadow: 0 8px 20px rgba(35, 48, 68, 0.12);
- }
- :deep(.leaflet-control-zoom a) {
- color: #263445;
- background: rgba(255, 255, 255, 0.94);
- }
- }
- :global(.power-station-marker) {
- border: none;
- background: transparent;
- }
- :global(.province-name-label) {
- border: none;
- background: transparent;
- pointer-events: none;
- }
- :global(.province-name-label span) {
- display: block;
- width: max-content;
- color: rgba(45, 55, 72, 0.72);
- font-size: 12px;
- font-weight: 600;
- text-shadow:
- 0 0 3px #fff,
- 0 0 3px #fff;
- transform: translate(-50%, -50%);
- white-space: nowrap;
- }
- :global(.power-station-marker .station-pin) {
- display: grid;
- place-items: center;
- width: 30px;
- height: 30px;
- border: 2px solid #fff;
- border-radius: 7px;
- color: #fff;
- background: #1da1f2;
- box-shadow: 0 8px 18px rgba(16, 92, 158, 0.28);
- font-size: 12px;
- font-weight: 700;
- }
- :global(.power-station-marker .station-pin::after) {
- position: absolute;
- left: 50%;
- bottom: -6px;
- width: 8px;
- height: 8px;
- border: 2px solid #fff;
- border-top: 0;
- border-left: 0;
- background: inherit;
- content: '';
- transform: translateX(-50%) rotate(45deg);
- }
- :global(.power-station-marker .station-wind) {
- background: #2d9cdb;
- }
- :global(.power-station-marker .station-solar) {
- background: #f5a623;
- }
- :global(.power-station-marker .station-hybrid) {
- background: #24a148;
- }
- :global(.power-station-marker .is-warning) {
- background: #f5222d;
- }
- :global(.station-popup) {
- display: grid;
- gap: 4px;
- min-width: 150px;
- color: #334155;
- }
- :global(.station-popup b) {
- color: #0f172a;
- }
- </style>
|