hejianhao
2025-04-21 c08d0ebace5e9f20eb442ad7cb1db05d61ecbd0d
src/components/MapPanel.vue
@@ -1,11 +1,13 @@
<template>
  <div class="center-panel">
    <div class="map-container" ref="mapContainer"></div>
    <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',
@@ -13,183 +15,291 @@
    return {
      map: null,
      markers: [],
      infoWindow: null
      currentMakers: [],
      infoWindow: null,
      updateTimer: null,
      markerObjects: {} // Store marker objects by ID
    }
  },
  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() {
    this.$nextTick(() => {
      this.initMap()
    this.fetchMapData()
    this.startUpdateTimer()
  },
  beforeDestroy() {
    window.removeEventListener('resize', () => {
      this.map && this.map.resize()
    })
    if (this.updateTimer) {
      clearInterval(this.updateTimer)
    }
  },
  methods: {
    async initMap() {
      try {
        const AMap = await AMapLoader.load({
          key: '526e04b30ceba8f217c5def5a92392f9',
          version: '2.0',
          plugins: ['AMap.ToolBar', 'AMap.Scale']
        })
        this.map = new AMap.Map(this.$refs.mapContainer, {
          zoom: 16,
          center: [91.1172, 29.6487],
          viewMode: '3D',
          pitch: 35,
          mapStyle: 'amap://styles/normal',
          features: ['bg', 'road', 'building', 'point'],
          buildingAnimation: true
        })
        this.map.addControl(new AMap.ToolBar({
          position: 'RB'
        }))
        this.map.addControl(new AMap.Scale({
          position: 'RB'
        }))
        this.infoWindow = new AMap.InfoWindow({
          offset: new AMap.Pixel(0, -30),
          closeWhenClickMap: true,
          autoMove: true,
          anchor: 'bottom-center'
        })
        this.map.on('click', () => {
          if (this.infoWindow) {
            this.infoWindow.close()
          }
        })
        this.addMarkers()
      } catch (error) {
        console.error('地图加载失败:', error)
      }
    startUpdateTimer() {
      this.updateTimer = setInterval(() => {
        this.fetchMapData()
      }, 3000)
    },
    addMarkers() {
      const houses = [
        {
          position: [91.1172, 29.6487],
          title: '新城大道房源',
          status: 'waiting',
          content: `
            <div class="info-window" data-status="waiting">
              <div class="info-title">房屋状态:待租出</div>
              <div class="info-content">
                <p>房屋编号:新城大道</p>
                <p>房屋状态:待租出</p>
                <p>租赁面积:1000/2000㎡</p>
                <p>本季租金:500/2000元/月</p>
              </div>
            </div>
          `
        },
        {
          position: [91.1272, 29.6587],
          title: '拉萨路房源',
          status: 'rented',
          content: `
            <div class="info-window" data-status="rented">
              <div class="info-title">房屋状态:已租出</div>
              <div class="info-content">
                <p>房屋编号:拉萨路</p>
                <p>房屋状态:已租出</p>
                <p>租赁面积:800/1500㎡</p>
                <p>本季租金:400/1800元/月</p>
              </div>
            </div>
          `
        }
      ]
      houses.forEach(house => {
        const circles = []
        const baseRadius = 15
        const numCircles = 3
        const centerCircle = new AMap.Circle({
          center: house.position,
          radius: baseRadius / 2,
          fillColor: house.status === 'waiting' ? '#ff9800' : '#4CAF50',
          fillOpacity: 0.8,
          strokeWeight: 0,
          zIndex: numCircles + 2,
          bubble: true,
          cursor: 'pointer'
        })
        for (let i = 0; i < numCircles; i++) {
          circles.push(new AMap.Circle({
            center: house.position,
            radius: baseRadius * (i + 1),
            fillColor: house.status === 'waiting' ? '#ff9800' : '#4CAF50',
            fillOpacity: 0.3 - (i * 0.05),
            strokeColor: house.status === 'waiting' ? '#ff9800' : '#4CAF50',
            strokeWeight: 2,
            strokeOpacity: 0.5 - (i * 0.08),
            zIndex: numCircles - i,
            bubble: true,
            cursor: 'pointer'
          }))
        }
        const clickHandler = () => {
          if (this.infoWindow) {
            this.infoWindow.setContent(house.content)
            this.infoWindow.open(this.map, house.position)
          }
        }
        centerCircle.on('click', clickHandler)
        circles.forEach(circle => circle.on('click', clickHandler))
        this.map.add([centerCircle, ...circles])
        this.markers.push({ center: centerCircle, rings: circles })
        let phase = 0
        const animate = () => {
          phase += 0.15
          const scale = 1 + 0.6 * Math.sin(phase)
          circles.forEach((circle, index) => {
            const currentRadius = baseRadius * (index + 1)
            circle.setRadius(currentRadius * scale)
            const baseOpacity = 0.3 - (index * 0.05)
            const opacityScale = 1 + 0.5 * Math.sin(phase)
            circle.setOptions({
              fillOpacity: baseOpacity * opacityScale,
              strokeOpacity: (0.5 - (index * 0.08)) * opacityScale
            })
    fetchMapData() {
      getHouseMapDistribution().then(res => {
        if (res.code === 200) {
          const newData = res.data.map(item => {
            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 ? '#618CE9' : item.houseStatus == 2 ? '#FDAE03' : item.houseStatus == 3 ? '#F64E4F' : '#0FBE6B'
            }
            return item
          })
          requestAnimationFrame(animate)
          if (!this.map) {
            this.currentMakers = JSON.parse(JSON.stringify(newData))
            this.markers = newData
            this.$nextTick(() => {
              this.initMap()
              window.addEventListener('resize', () => {
                this.map && this.map.resize()
              })
            })
          } else {
            this.updateMarkers(newData)
          }
        }
        animate()
      })
    }
    },
    updateMarkers(newData) {
      // Update existing markers and add new ones
      newData.forEach(newMarker => {
        const markerId = `${newMarker.longitude}-${newMarker.latitude}`
        const existingMarker = this.markerObjects[markerId]
        if (existingMarker) {
          // Update existing marker content
          const content = this.generateMarkerContent(newMarker)
          existingMarker.setContent(content)
        } else {
          // Create new marker
          const content = this.generateMarkerContent(newMarker)
          const markerObj = new AMap.Marker({
            position: newMarker.position,
            content: content,
            anchor: 'center',
            offset: new AMap.Pixel(0, 0),
            zIndex: 100
          })
          markerObj.on('click', () => {
            this.showInfoWindow(markerObj, newMarker.info)
          })
          markerObj.setMap(this.map)
          this.markerObjects[markerId] = markerObj
        }
      })
      // Remove markers that no longer exist
      Object.keys(this.markerObjects).forEach(markerId => {
        const [longitude, latitude] = markerId.split('-')
        const markerExists = newData.some(marker =>
          marker.longitude === parseFloat(longitude) &&
          marker.latitude === parseFloat(latitude)
        )
        if (!markerExists) {
          this.markerObjects[markerId].setMap(null)
          delete this.markerObjects[markerId]
        }
      })
      this.currentMakers = JSON.parse(JSON.stringify(newData))
      this.markers = newData
    },
    generateMarkerContent(marker) {
      return `
        <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>
      `
    },
    async initMap() {
      const map = await AMapLoader.load({
        key: '526e04b30ceba8f217c5def5a92392f9',
        version: '2.0',
        plugins: ['AMap.MarkerClusterer']
      })
      this.map = new map.Map('map-container', {
        zoom: 14,
        center: [91.172119, 29.652941],
        mapStyle: 'amap://styles/normal'
      })
      this.map.on('click', () => {
        this.markers = this.currentMakers
        this.$nextTick(() => {
          this.map.clearMap()
          this.addMapMarkers()
        })
      })
      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>
<style scoped lang="scss">
.center-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: relative;
  height: 100%;
  min-height: 0;
}
.map-container {
  border-radius: 8px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
  width: 100%;
  height: 100vh;
  height: 100%;
  flex: 1;
  position: relative;
}
.info-window {
  padding: 10px;
  background: rgba(146, 146, 146, 0.5);
  background: rgba(146, 146, 146, 0.7);
  border-radius: 8px;
  z-index: 10003;
  min-width: 200px;
@@ -198,7 +308,7 @@
.info-title {
  font-size: 16px;
  font-weight: bold;
  color: #fff;
  color: #fff !important;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid #eee;
@@ -215,11 +325,19 @@
}
.info-window[data-status="waiting"] .info-title {
  color: #fff;
  color: #2196F3;
}
.info-window[data-status="rented"] .info-title {
  color: #fff;
  color: #FF9800;
}
.info-window[data-status="repairing"] .info-title {
  color: #F44336;
}
.info-window[data-status="overdue"] .info-title {
  color: #39C5BB;
}
.amap-info {
@@ -228,11 +346,123 @@
}
.amap-info-content {
  background: rgba(146, 146, 146, 0.5);
  background: rgba(146, 146, 146, 0.7);
  padding: 0 !important;
}
.amap-info-sharp {
  display: none !important;
}
</style>
.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>