| <template> | 
|   <div class="center-panel"> | 
|     <div id="map-container" class="map-container"></div> | 
|   </div> | 
| </template> | 
|   | 
| <script> | 
| import AMapLoader from '@amap/amap-jsapi-loader' | 
| import { mapState } from 'vuex' | 
| import { getHouseMapDistribution } from './service' | 
|   | 
| export default { | 
|   name: 'MapPanel', | 
|   data() { | 
|     return { | 
|       map: null, | 
|       markers: [], | 
|       currentMakers: [], | 
|       infoWindow: null | 
|     } | 
|   }, | 
|   watch: { | 
|     mapMarkerStatus(newVal) { | 
|       if (newVal === 'all') { | 
|         this.markers = this.currentMakers | 
|         this.$nextTick(() => { | 
|           this.map.clearMap() | 
|           this.addMapMarkers() | 
|         }) | 
|       } else { | 
|         let arr = this.currentMakers.filter(item => item.info.statusText == newVal) | 
|         this.markers = arr | 
|         this.$nextTick(() => { | 
|           this.map.clearMap() | 
|           this.addMapMarkers() | 
|         }) | 
|       } | 
|   | 
|     } | 
|   }, | 
|   computed: { | 
|     ...mapState([ | 
|       'mapMarkerStatus' | 
|     ]), | 
|   }, | 
|   mounted() { | 
|     getHouseMapDistribution().then(res => { | 
|       if (res.code === 200) { | 
|         res.data.map((item, index) => { | 
|           item.position = [item.longitude, item.latitude] | 
|           item.info = { | 
|             address: item.houseAddress, | 
|             name: item.houseName, | 
|             status: item.houseStatus, | 
|             rentStatus: item.rentStatus, | 
|             statusText: item.houseStatus == 1 ? '待出租' : item.houseStatus == 2 ? '已出租' : item.houseStatus == 3 ? '维修中' : '欠费', | 
|             tenant: item.tenant, | 
|             rent: item.rent, | 
|             color: item.houseStatus == 1 ? '#FDAE03' : item.houseStatus == 2 ? '#618CE9' : item.houseStatus == 3 ? '#F64E4F' : '#0FBE6B' | 
|           } | 
|           return item | 
|         }) | 
|         this.currentMakers = JSON.parse(JSON.stringify(res.data)) | 
|         this.markers = res.data | 
|       } | 
|       this.$nextTick(() => { | 
|         this.initMap() | 
|         window.addEventListener('resize', () => { | 
|           this.map && this.map.resize() | 
|         }) | 
|       }) | 
|     }) | 
|   | 
|   }, | 
|   beforeDestroy() { | 
|     window.removeEventListener('resize', () => { | 
|       this.map && this.map.resize() | 
|     }) | 
|   }, | 
|   methods: { | 
|     async initMap() { | 
|       const map = await AMapLoader.load({ | 
|         key: '67968c82f27c7e2cb9f40c1a9aa3042b', | 
|         version: '2.0', | 
|         plugins: ['AMap.MarkerClusterer'] | 
|       }) | 
|   | 
|       this.map = new map.Map('map-container', { | 
|         zoom: 13, | 
|         center: [91.172119, 29.652941], | 
|         mapStyle: 'amap://styles/normal' | 
|       }) | 
|   | 
|       this.addMapMarkers() | 
|     }, | 
|     addMapMarkers() { | 
|       this.markers.forEach(marker => { | 
|         const content = ` | 
|           <div class="marker-container"> | 
|             <svg width="120" height="120" viewBox="0 0 120 120"> | 
|               <defs> | 
|                 <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"> | 
|                   <feGaussianBlur stdDeviation="3" result="coloredBlur"/> | 
|                   <feMerge> | 
|                     <feMergeNode in="coloredBlur"/> | 
|                     <feMergeNode in="SourceGraphic"/> | 
|                   </feMerge> | 
|                 </filter> | 
|               </defs> | 
|               <circle class="ripple" cx="60" cy="60" r="12" fill="none" stroke="${marker.info.color}" stroke-width="3" style="opacity: 0.4"> | 
|                 <animate attributeName="r" from="12" to="45" dur="3s" begin="0s" repeatCount="indefinite" /> | 
|                 <animate attributeName="opacity" from="0.8" to="0" dur="3s" begin="0s" repeatCount="indefinite" /> | 
|               </circle> | 
|               <circle class="ripple" cx="60" cy="60" r="12" fill="none" stroke="${marker.info.color}" stroke-width="3" style="opacity: 0.4"> | 
|                 <animate attributeName="r" from="12" to="45" dur="3s" begin="1s" repeatCount="indefinite" /> | 
|                 <animate attributeName="opacity" from="0.8" to="0" dur="3s" begin="1s" repeatCount="indefinite" /> | 
|               </circle> | 
|               <circle class="ripple" cx="60" cy="60" r="12" fill="none" stroke="${marker.info.color}" stroke-width="3" style="opacity: 0.4"> | 
|                 <animate attributeName="r" from="12" to="45" dur="3s" begin="2s" repeatCount="indefinite" /> | 
|                 <animate attributeName="opacity" from="0.8" to="0" dur="3s" begin="2s" repeatCount="indefinite" /> | 
|               </circle> | 
|               <circle class="marker" cx="60" cy="60" r="8" fill="${marker.info.color}" filter="url(#glow)"> | 
|                 <animate attributeName="r" values="8;10;8" dur="2s" repeatCount="indefinite" /> | 
|                 <animate attributeName="fill-opacity" values="1;0.8;1" dur="2s" repeatCount="indefinite" /> | 
|               </circle> | 
|             </svg> | 
|           </div> | 
|         ` | 
|   | 
|         const markerObj = new AMap.Marker({ | 
|           position: marker.position, | 
|           content: content, | 
|           anchor: 'center', | 
|           offset: new AMap.Pixel(0, 0), | 
|           zIndex: 100 | 
|         }) | 
|   | 
|         markerObj.on('click', () => { | 
|           this.showInfoWindow(markerObj, marker.info) | 
|         }) | 
|   | 
|         markerObj.setMap(this.map) | 
|       }) | 
|     }, | 
|     showInfoWindow(marker, info) { | 
|       const content = ` | 
|         <div class="map-info-card" style="min-width:300px !important;color:#fff;background-color: rgba(146, 146, 146, 0.7) !important;padding: 10px;border-radius: 12px;box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);border-radius:0px 35px 35px 0px;"> | 
|           <div class="info-body"> | 
|             <div class="info-item">房屋地址:${info.address}</div> | 
|             <div class="info-item">房屋名称:${info.name}</div> | 
|             <div class="info-item">房屋状态:<span class="status-tag" style="background: rgba(${info.color.replace(/[^\d,]/g, '')}, 0.1); color: ${info.color};">${info.statusText}</span></div> | 
|             <div class="info-item">租户:${info.tenant}</div> | 
|             <div class="info-item"> | 
|               租金状态:${info.rentStatus} | 
|             </div> | 
|             <div class="info-item"> | 
|               本季租金:${info.rent} | 
|             </div> | 
|           </div> | 
|         </div> | 
|       ` | 
|   | 
|       const infoWindow = new AMap.InfoWindow({ | 
|         content: content, | 
|         offset: new AMap.Pixel(0, -20), | 
|         closeWhenClickMap: true, | 
|         anchor: 'bottom-center', | 
|         isCustom: true | 
|       }) | 
|   | 
|       infoWindow.open(this.map, marker.getPosition()) | 
|     }, | 
|   } | 
| } | 
| </script> | 
|   | 
| <style scoped lang="scss"> | 
| .center-panel { | 
|   flex: 1; | 
|   display: flex; | 
|   flex-direction: column; | 
|   position: relative; | 
|   height: 100%; | 
|   min-height: 0; | 
| } | 
|   | 
| .map-container { | 
|   width: 100%; | 
|   height: 100%; | 
|   flex: 1; | 
|   position: relative; | 
| } | 
|   | 
| .info-window { | 
|   padding: 10px; | 
|   background: rgba(146, 146, 146, 0.7); | 
|   border-radius: 8px; | 
|   z-index: 10003; | 
|   min-width: 200px; | 
| } | 
|   | 
| .info-title { | 
|   font-size: 16px; | 
|   font-weight: bold; | 
|   color: #fff !important; | 
|   margin-bottom: 12px; | 
|   padding-bottom: 8px; | 
|   border-bottom: 1px solid #eee; | 
| } | 
|   | 
| .info-content { | 
|   font-size: 12px; | 
|   color: #fff; | 
| } | 
|   | 
| .info-content p { | 
|   margin: 8px 0; | 
|   line-height: 1.4; | 
| } | 
|   | 
| .info-window[data-status="waiting"] .info-title { | 
|   color: #2196F3; | 
| } | 
|   | 
| .info-window[data-status="rented"] .info-title { | 
|   color: #FF9800; | 
| } | 
|   | 
| .info-window[data-status="repairing"] .info-title { | 
|   color: #F44336; | 
| } | 
|   | 
| .info-window[data-status="overdue"] .info-title { | 
|   color: #39C5BB; | 
| } | 
|   | 
| .amap-info { | 
|   z-index: 10003 !important; | 
|   display: block !important; | 
| } | 
|   | 
| .amap-info-content { | 
|   background: rgba(146, 146, 146, 0.7); | 
|   padding: 0 !important; | 
| } | 
|   | 
| .amap-info-sharp { | 
|   display: none !important; | 
| } | 
|   | 
| .marker-container { | 
|   width: 120px; | 
|   height: 120px; | 
|   transform: translate(-50%, -50%); | 
|   pointer-events: all; | 
|   filter: drop-shadow(0 4px 12px rgba(79, 217, 255, 0.4)); | 
|   | 
| } | 
|   | 
| svg { | 
|   width: 100%; | 
|   height: 100%; | 
| } | 
|   | 
| .ripple { | 
|   transform-origin: center; | 
|   stroke-width: 3; | 
| } | 
|   | 
| .marker { | 
|   filter: drop-shadow(0 0 8px #4fd9ff); | 
| } | 
|   | 
| .map-info-card { | 
|   border: 1px solid rgba(79, 217, 255, 0.2); | 
|   overflow: hidden; | 
|   width: 300px !important; | 
|   backdrop-filter: blur(12px); | 
|   | 
|   .info-header { | 
|     padding: 12px 15px; | 
|     border-bottom: 1px solid rgba(79, 217, 255, 0.15); | 
|   | 
|     .info-title { | 
|       color: #fff; | 
|       font-size: 15px; | 
|       font-weight: 500; | 
|       position: relative; | 
|       padding-left: 12px; | 
|   | 
|       &::before { | 
|         content: ''; | 
|         position: absolute; | 
|         left: 0; | 
|         top: 50%; | 
|         transform: translateY(-50%); | 
|         width: 3px; | 
|         height: 14px; | 
|         background: linear-gradient(to bottom, #4fd9ff, #568aea); | 
|         border-radius: 2px; | 
|       } | 
|     } | 
|   } | 
|   | 
|   .info-body { | 
|     padding: 15px; | 
|     background: rgba(15, 19, 37, 0.95); | 
|   | 
|     .info-item { | 
|       font-size: 14px; | 
|       color: rgba(255, 255, 255, 0.9); | 
|       margin-bottom: 12px; | 
|       line-height: 1.5; | 
|   | 
|       &:last-child { | 
|         margin-bottom: 0; | 
|       } | 
|   | 
|       .status-tag { | 
|         display: inline-block; | 
|         padding: 2px 8px; | 
|         border-radius: 4px; | 
|         font-size: 13px; | 
|       } | 
|   | 
|       .rent-status { | 
|         display: inline-flex; | 
|         align-items: center; | 
|         gap: 8px; | 
|         margin-top: 4px; | 
|         width: 100%; | 
|   | 
|         .progress-bar { | 
|           flex: 1; | 
|           height: 4px; | 
|           background: rgba(255, 255, 255, 0.1); | 
|           border-radius: 2px; | 
|           overflow: hidden; | 
|           position: relative; | 
|   | 
|           .progress { | 
|             position: absolute; | 
|             left: 0; | 
|             top: 0; | 
|             height: 100%; | 
|             border-radius: 2px; | 
|             transition: width 0.3s ease; | 
|           } | 
|         } | 
|   | 
|         .rent-text { | 
|           font-size: 13px; | 
|           color: rgba(255, 255, 255, 0.9); | 
|           white-space: nowrap; | 
|           min-width: 80px; | 
|           text-align: right; | 
|         } | 
|       } | 
|     } | 
|   } | 
| } | 
| </style> |