hejianhao
2025-03-26 04477a62f8966e9aabc31421bab138960eff323e
除地图外所有接口对接、样式调整
10个文件已修改
1个文件已删除
2个文件已添加
1273 ■■■■ 已修改文件
package.json 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BottomCharts.vue 222 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Header.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/LeftPanel.vue 256 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/MapPanel.vue 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RightPanel.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/service.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/DataScreen.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -13,8 +13,11 @@
    "echarts": "^5.6.0",
    "element-ui": "^2.15.14",
    "regenerator-runtime": "^0.14.1",
    "sass": "^1.86.0",
    "sass-loader": "^16.0.5",
    "vue": "^2.7.7",
    "vue-router": "^3.5.4"
    "vue-router": "^3.5.4",
    "vuex": "^4.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-legacy": "^2.3.1",
src/App.vue
@@ -1,8 +1,38 @@
<template>
  <div id="app">
    <div class="screen-adapter">
    <router-view />
  </div>
  </div>
</template>
<script>
export default {
  name: 'App',
  mounted() {
    this.handleScreenAdapter()
    window.addEventListener('resize', this.handleScreenAdapter)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleScreenAdapter)
  },
  methods: {
    handleScreenAdapter() {
      const designWidth = 1920
      const designHeight = 1080
      const scale = Math.min(
        window.innerWidth / designWidth,
        window.innerHeight / designHeight
      )
      const screenAdapter = document.querySelector('.screen-adapter')
      screenAdapter.style.transform = `scale(${scale})`
      screenAdapter.style.transformOrigin = 'left top'
      screenAdapter.style.width = `${window.innerWidth / scale}px`
      screenAdapter.style.height = `${window.innerHeight / scale}px`
    }
  }
}
</script>
<style>
* {
@@ -25,5 +55,11 @@
  width: 100%;
  height: 100%;
  overflow: hidden;
  margin: 0 !important;
  padding: 0 !important;
}
.screen-adapter {
  transform-origin: left top;
}
</style>
src/components/BottomCharts.vue
@@ -1,12 +1,32 @@
<template>
  <div class="bottom-charts">
    <div class="chart-container">
    <div class="chart-group">
      <div class="chart-title">收入趋势</div>
        <div class="chart-title">租金收入趋势图</div>
      <div class="income-chart" ref="incomeChart"></div>
    </div>
    <div class="chart-group">
      <div class="chart-title">租金收入趋势图</div>
        <div class="chart-title">租户数量趋势图</div>
      <div class="trend-chart" ref="rentTrendChart"></div>
      </div>
    </div>
    <div class="map-container">
      <div class="map-marker-item" @click="handleMapMarkerClick('已出租')" :class="{ active: activeStatus === '已出租' }">
        <div class="map-marker-item-icon"></div>
        <div class="map-marker-item-text">已出租</div>
      </div>
      <div class="map-marker-item" @click="handleMapMarkerClick('待出租')" :class="{ active: activeStatus === '待出租' }">
        <div class="map-marker-item-icon"></div>
        <div class="map-marker-item-text">待出租</div>
      </div>
      <div class="map-marker-item" @click="handleMapMarkerClick('维修中')" :class="{ active: activeStatus === '维修中' }">
        <div class="map-marker-item-icon"></div>
        <div class="map-marker-item-text">维修中</div>
      </div>
      <div class="map-marker-item" @click="handleMapMarkerClick('欠费')" :class="{ active: activeStatus === '欠费' }">
        <div class="map-marker-item-icon"></div>
        <div class="map-marker-item-text">欠费</div>
      </div>
    </div>
  </div>
</template>
@@ -16,12 +36,23 @@
export default {
  name: 'BottomCharts',
  props: {
    rentIncomeTrend: {
      type: Array,
      default: () => []
    },
    tenantCountTrend: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      charts: {
        income: null,
        rentTrend: null
      }
      },
      activeStatus: null
    }
  },
  mounted() {
@@ -30,30 +61,43 @@
    })
  },
  methods: {
    handleMapMarkerClick(status) {
      if (this.activeStatus === status) {
        this.activeStatus = null
        this.$store.commit('SET_MAP_MARKER_STATUS', 'all')
      } else {
        this.activeStatus = status
        this.$store.commit('SET_MAP_MARKER_STATUS', status)
      }
    },
    initCharts() {
      // 收入趋势图表
      // 租金收入趋势图表
      if (Object.keys(this.rentIncomeTrend).length > 0) {
      this.charts.income = echarts.init(this.$refs.incomeChart)
      this.charts.income.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
            left: '5%',
            right: '5%',
            bottom: '5%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
            trigger: 'axis',
            confine: true
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
            data: this.rentIncomeTrend.quarters,
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
              color: '#fff',
              interval: 0,
              rotate: 30
          }
        },
        yAxis: {
@@ -72,7 +116,7 @@
          }
        },
        series: [{
          data: [100, 400, 200, 300, 200, 400, 300],
            data: this.rentIncomeTrend.incomeData,
          type: 'line',
          smooth: true,
          symbol: 'circle',
@@ -102,30 +146,34 @@
          }
        }]
      })
      // 租金趋势图表
      }
      // 租户数量趋势图表
      if (this.tenantCountTrend.length > 0) {
      this.charts.rentTrend = echarts.init(this.$refs.rentTrendChart)
      this.charts.rentTrend.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
            left: '5%',
            right: '5%',
            bottom: '5%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
            trigger: 'axis',
            confine: true
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
            data: this.tenantCountTrend.map(item => item.date),
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
              color: '#fff',
              interval: 0,
              rotate: 30
          }
        },
        yAxis: {
@@ -144,17 +192,17 @@
          }
        },
        series: [{
          data: [300, 200, 400, 300, 500, 400, 300],
            data: this.tenantCountTrend.map(item => item.count),
          type: 'line',
          smooth: true,
          symbol: 'circle',
          symbolSize: 8,
          lineStyle: {
            color: '#4facfe',
              color: '#87F7C7',
            width: 2
          },
          itemStyle: {
            color: '#4facfe'
              color: '#87F7C7'
          },
          areaStyle: {
            color: {
@@ -165,40 +213,50 @@
              y2: 1,
              colorStops: [{
                offset: 0,
                color: 'rgba(79,172,254,0.3)'
                  color: 'rgba(135,247,195,0.3)'
              }, {
                offset: 1,
                color: 'rgba(79,172,254,0.1)'
                  color: 'rgba(135,247,195,0.1)'
              }]
            }
          }
        }]
      })
      }
      window.addEventListener('resize', () => {
        Object.values(this.charts).forEach(chart => {
          chart && chart.resize()
        })
      })
    }
    },
  }
}
</script>
<style scoped>
<style scoped lang="scss">
.bottom-charts {
  height: 200px;
  width: 700px;
  position: fixed;
  width: 100%;
  position: absolute;
  bottom: 0;
  left: 0;
  z-index: 1003;
  display: flex;
  gap: 10px;
  justify-content: space-between;
  padding: 0 20px;
  pointer-events: none;
}
.chart-container {
  display: flex;
  gap: 20px;
  flex-shrink: 0;
  width: 800px;
  pointer-events: auto;
}
.chart-group {
  flex: 1;
  width: 390px;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 10px;
@@ -213,5 +271,107 @@
.income-chart,
.trend-chart {
  height: calc(100% - 30px);
  width: 100%;
}
.map-container {
  color: #fff;
  display: flex;
  align-items: end;
  gap: 20px;
  margin-bottom: 35px;
  margin-right: 35px;
  pointer-events: auto;
}
.map-marker-item {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  .map-marker-item-icon {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    position: relative;
    &::before,
    &::after {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 100%;
      height: 100%;
      border-radius: 50%;
      opacity: 0;
    }
  }
  &.active {
    .map-marker-item-icon {
      &::before,
      &::after {
        animation: ripple 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
      }
      &::after {
        animation-delay: 0.75s;
      }
    }
  }
  &:nth-child(1) {
    .map-marker-item-icon {
      background-color: #4fd9ff;
      &::before,
      &::after {
        border: 2px solid #4fd9ff;
      }
    }
  }
  &:nth-child(2) {
    .map-marker-item-icon {
      background-color: #faad14;
      &::before,
      &::after {
        border: 2px solid #faad14;
      }
    }
  }
  &:nth-child(3) {
    .map-marker-item-icon {
      background-color: #ff4d4f;
      &::before,
      &::after {
        border: 2px solid #ff4d4f;
      }
    }
  }
  &:nth-child(4) {
    .map-marker-item-icon {
      background-color: #39c5bb;
      &::before,
      &::after {
        border: 2px solid #39c5bb;
      }
    }
  }
}
@keyframes ripple {
  0% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 0.8;
  }
  100% {
    transform: translate(-50%, -50%) scale(2.5);
    opacity: 0;
  }
}
</style> 
src/components/Header.vue
@@ -4,8 +4,9 @@
      <span class="time">{{ currentTime }}</span>
      <span class="date">{{ currentDate }}</span>
      <span class="weather">
        <i class="weather-icon"></i>
        {{ temperature }}°C
        <div class="weather-box"></div>
        <iframe allowtransparency="true" frameborder="0" width="100" height="36" scrolling="no"
          src="//tianqi.2345.com/plugin/widget/index.htm?s=3&z=3&t=1&v=0&d=3&bd=0&k=&f=ffffff&ltf=ffffff&htf=ffffff&q=1&e=1&a=1&c=55591&w=100&h=36&align=left"></iframe>
      </span>
    </div>
  </div>
@@ -60,22 +61,25 @@
.header {
  height: 120px;
  display: flex;
  position: fixed;
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  width: 100%;
  background: linear-gradient(to bottom, rgba(1, 85, 121, 0.8) 0%, rgba(1, 85, 121, 0.4) 40%, rgba(151, 190, 192, 0) 100%);
  z-index: 1000;
  justify-content: flex-end;
  align-items: center;
  padding: 10px 20px;
  backdrop-filter: blur(10px);
  pointer-events: none;
}
.time-weather {
  color: #fff;
  display: flex;
  gap: 20px;
  align-items: center;
  pointer-events: auto;
}
.time {
@@ -83,13 +87,16 @@
  font-weight: bold;
}
.date {
  color: #fff;
.weather {
  position: relative;
}
.weather {
  display: flex;
  align-items: center;
  gap: 5px;
.weather-box {
  position: absolute;
  top: 0;
  left: 0;
  width: 100px;
  height: 36px;
  z-index: 1000;
}
</style> 
src/components/LeftPanel.vue
@@ -9,13 +9,13 @@
        <div class="stat-item">
          <div class="stat-info">
            <div class="stat-label"> <i class="icon-area"></i> 房屋总面积</div>
            <div class="stat-value">{{ totalArea }}<span class="unit">m²</span></div>
            <div class="stat-value">{{ staticsData.houseTotalArea }}<span class="unit">m²</span></div>
          </div>
        </div>
        <div class="stat-item">
          <div class="stat-info">
            <div class="stat-label"> <i class="icon-area"></i>已出租面积</div>
            <div class="stat-value">{{ rentedArea }}<span class="unit">m²</span></div>
            <div class="stat-value">{{ staticsData.houseRentedArea }}<span class="unit">m²</span></div>
          </div>
        </div>
      </div>
@@ -23,107 +23,147 @@
        <div class="stat-group">
          <div class="stat-item mt-10">
            <div class="stat-info">
              <div class="stat-label"> <i class="icon-money"></i>今日已收租金</div>
              <div class="stat-value">{{ todayRent }}<span class="unit">万元</span></div>
              <div class="stat-label"> <i class="icon-money"></i>总计应收租金</div>
              <div class="stat-value">{{ staticsData.totalReceivableRent }}<span class="unit">万元</span></div>
            </div>
          </div>
          <div class="stat-item mt-10">
            <div class="stat-info">
              <div class="stat-label"><i class="icon-money"></i>今日已收租金</div>
              <div class="stat-value">{{ todayIncome }}<span class="unit">万元</span></div>
              <div class="stat-label"><i class="icon-money"></i>总计已收租金</div>
              <div class="stat-value">{{ staticsData.totalReceivedRent }}<span class="unit">万元</span></div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="chart-card" style="width: 350px;">
      <div class="chart-title">区域租金分析</div>
      <div class="area-chart" ref="areaRentChart"></div>
      <div class="chart-title">区域租金排名</div>
      <div class="chart-unit">单位(万元)</div>
      <div class="rent-rank-list" ref="scrollContainer" @mouseenter="stopScroll" @mouseleave="startScroll">
        <div class="rank-list-content" ref="scrollContent">
          <div class="rank-list-group">
            <div v-for="(item, index) in displayRank" :key="index" class="rent-rank-item">
              <div class="rank-name">{{ item.streetName }}</div>
              <div class="rank-progress">
                <div class="rank-bar">
                  <div class="rank-bar-inner" :style="{
                    width: (item.rentAmount / Math.max(...rentRank.map(i => i.rentAmount)) * 100) + '%',
                    background: getBarColor(item.rentAmount)
                  }"></div>
                </div>
                <div class="rank-value">{{ item.rentAmount }}</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'LeftPanel',
  props: {
    staticsData: {
      type: Object,
      default: () => { }
    },
    rentRank: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      totalArea: '386.5',
      rentedArea: '316.5',
      todayRent: '316.5',
      todayIncome: '124.5',
      charts: {
        areaRent: null
      scrollTimer: null,
      isScrolling: true,
      currentTranslate: 0,
      firstItemHeight: 0,
      animationFrameId: null,
      lastTimestamp: 0,
      displayRank: []
    }
  },
  watch: {
    rentRank: {
      immediate: true,
      handler(newVal) {
        if (newVal && newVal.length) {
          // 复制两组数据用于无缝滚动
          this.displayRank = [...newVal, ...newVal];
        }
      }
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initCharts()
    })
      const firstItem = this.$el.querySelector('.rent-rank-item');
      if (firstItem) {
        this.firstItemHeight = firstItem.offsetHeight + 8;
      }
      this.startScroll();
    });
  },
  beforeDestroy() {
    this.clearScrollTimer();
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId);
    }
  },
  methods: {
    initCharts() {
      this.charts.areaRent = echarts.init(this.$refs.areaRentChart)
      this.charts.areaRent.setOption({
        grid: {
          left: '20%',
          right: '5%',
          top: '10%',
          bottom: '10%'
        },
        xAxis: {
          type: 'value',
          axisLine: {
            show: false
          },
          splitLine: {
            show: false
          },
          axisLabel: {
            color: '#fff'
    startScroll() {
      this.isScrolling = true;
      if (!this.animationFrameId) {
        this.lastTimestamp = performance.now();
        this.startAutoScroll();
          }
        },
        yAxis: {
          type: 'category',
          data: ['新城大道', '拉萨路', '新城大道', '拉萨路', '新城大道', '新城大道', '拉萨路', '新城大道', '拉萨路', '新城大道'],
          axisLine: {
            lineStyle: {
              color: '#fff'
    stopScroll() {
      this.isScrolling = false;
    },
    clearScrollTimer() {
      if (this.animationFrameId) {
        cancelAnimationFrame(this.animationFrameId);
        this.animationFrameId = null;
            }
          },
          axisLabel: {
            color: '#fff'
    startAutoScroll() {
      const animate = (timestamp) => {
        if (!this.isScrolling) {
          this.lastTimestamp = timestamp;
          this.animationFrameId = requestAnimationFrame(animate);
          return;
          }
        },
        series: [{
          type: 'bar',
          data: [1900, 1850, 1800, 1750, 1700, 1600, 1500, 1400, 1300, 1200],
          barWidth: '30%',
          itemStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 1,
              y2: 0,
              colorStops: [{
                offset: 0,
                color: '#0ff'
              }, {
                offset: 1,
                color: '#0ff'
              }]
            }
          }
        }]
      })
      window.addEventListener('resize', () => {
        this.charts.areaRent && this.charts.areaRent.resize()
      })
        const deltaTime = timestamp - this.lastTimestamp;
        const speed = 0.03;
        const step = speed * deltaTime;
        const content = this.$refs.scrollContent;
        if (!content || !this.firstItemHeight) {
          this.animationFrameId = requestAnimationFrame(animate);
          return;
        }
        this.currentTranslate -= step;
        // 当滚动到第一组数据的末尾时,重置位置到第二组数据的开始
        const halfHeight = this.firstItemHeight * this.rentRank.length;
        if (Math.abs(this.currentTranslate) >= halfHeight) {
          this.currentTranslate = 0;
        }
        content.style.transform = `translateY(${this.currentTranslate}px)`;
        this.lastTimestamp = timestamp;
        this.animationFrameId = requestAnimationFrame(animate);
      };
      this.animationFrameId = requestAnimationFrame(animate);
    },
    getBarColor(value) {
      if (value == 0) return 'transparent';
      return '#87F7C7';
    }
  }
}
@@ -136,7 +176,7 @@
  position: fixed;
  top: 0;
  left: 0;
  height: calc(100vh);
  height: 100%;
  background: linear-gradient(to right, rgba(1, 85, 121, 0.8) 0%, rgba(1, 85, 121, 0.8) 20%, rgba(151, 190, 192, 0));
  z-index: 1002;
  flex-direction: column;
@@ -177,6 +217,7 @@
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.icon-area,
@@ -205,6 +246,10 @@
  font-size: 24px;
  font-weight: bold;
  color: #0ff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
.unit {
@@ -219,15 +264,76 @@
  border-radius: 8px;
  padding: 15px;
  min-height: 300px;
  max-height: calc(100vh - 600px);
  overflow: hidden;
  margin-bottom: 20px;
}
.chart-title {
  font-size: 16px;
  margin-bottom: 15px;
  color: #fff;
}
.area-chart {
  height: calc(100% - 30px);
.chart-unit {
  font-size: 12px;
  margin-bottom: 15px;
}
.rent-rank-list {
  height: calc(100% - 60px);
  overflow: hidden;
  padding: 10px 0;
  position: relative;
  max-height: calc(100% - 60px);
}
.rank-list-content {
  position: relative;
  transition: none;
  transform: translateY(0);
  will-change: transform;
}
.rank-list-group {
  position: relative;
}
.rent-rank-item {
  display: flex;
  flex-direction: column;
  padding: 8px 0;
}
.rank-name {
  color: #fff;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rank-progress {
  display: flex;
  align-items: center;
  gap: 10px;
}
.rank-bar {
  flex: 1;
  height: 8px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.rank-bar-inner {
  height: 100%;
  transition: all 0.3s ease;
}
.rank-value {
  color: #fff;
  font-size: 14px;
  min-width: 45px;
  text-align: right;
}
</style> 
src/components/MapPanel.vue
@@ -1,11 +1,12 @@
<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'
export default {
  name: 'MapPanel',
@@ -16,175 +17,207 @@
      infoWindow: null
    }
  },
  watch: {
    mapMarkerStatus(newVal) {
      console.log(newVal)
    }
  },
  computed: {
    ...mapState([
      'mapMarkerStatus'
    ]),
  },
  mounted() {
    this.$nextTick(() => {
      this.initMap()
      window.addEventListener('resize', () => {
        this.map && this.map.resize()
      })
    })
  },
  beforeDestroy() {
    window.removeEventListener('resize', () => {
      this.map && this.map.resize()
    })
  },
  methods: {
    async initMap() {
      try {
        const AMap = await AMapLoader.load({
          key: '526e04b30ceba8f217c5def5a92392f9',
      const map = await AMapLoader.load({
        key: '67968c82f27c7e2cb9f40c1a9aa3042b',
          version: '2.0',
          plugins: ['AMap.ToolBar', 'AMap.Scale']
        plugins: ['AMap.MarkerClusterer']
        })
        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 = new map.Map('map-container', {
        zoom: 13,
        center: [91.172119, 29.652941],
        mapStyle: 'amap://styles/normal'
        })
        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)
      this.addMapMarkers()
    },
    addMapMarkers() {
      const markers = [
        {
          position: [91.172119, 29.652941],
          info: {
            address: '西花部汾罚藏医院',
            name: '名称名称名称',
            status: 'vacant',
            statusText: '待出租',
            tenant: '张三',
            currentRent: 1600,
            totalRent: 20000,
            quarterlyPaid: 500,
            quarterlyRent: 2000,
            color: '#faad14'
      }
    },
    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: '拉萨路房源',
          position: [91.182119, 29.662941],
          info: {
            address: '拉萨市城关区江苏路15号',
            name: '泊江大道B栋',
          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>
          `
            statusText: '已出租',
            tenant: '-',
            currentRent: 3000,
            totalRent: 15000,
            quarterlyPaid: 1000,
            quarterlyRent: 4500,
            color: '#4fd9ff'
          }
        },
        {
          position: [91.162119, 29.642941],
          info: {
            address: '拉萨市城关区北京中路8号',
            name: '新城大道C区',
            status: 'maintenance',
            statusText: '维修中',
            tenant: '-',
            currentRent: 0,
            totalRent: 16000,
            quarterlyPaid: 0,
            quarterlyRent: 5400,
            color: '#ff4d4f'
          }
        },
        {
          position: [91.192119, 29.672941],
          info: {
            address: '拉萨市城关区夺底路33号',
            name: '高江路D座',
            status: 'overdue',
            statusText: '欠费',
            tenant: '西藏联通',
            currentRent: 12000,
            totalRent: 20000,
            quarterlyPaid: 4000,
            quarterlyRent: 6600,
            color: '#39c5bb'
          }
        }
      ]
      houses.forEach(house => {
        const circles = []
        const baseRadius = 15
        const numCircles = 3
      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 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'
        const markerObj = new AMap.Marker({
          position: marker.position,
          content: content,
          anchor: 'center',
          offset: new AMap.Pixel(0, 0),
          zIndex: 100
        })
        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
            })
        markerObj.on('click', () => {
          this.showInfoWindow(markerObj, marker.info)
          })
          requestAnimationFrame(animate)
        }
        animate()
        markerObj.setMap(this.map)
      })
    }
    },
    showInfoWindow(marker, info) {
      const content = `
        <div class="map-info-card" style="color:#000;background-color: #fff !important;padding: 10px;border-radius: 12px;box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);">
          <div class="info-header">
            <div class="info-title">房屋详情</div>
          </div>
          <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.currentRent}/${info.totalRent}
            </div>
            <div class="info-item">
              本季租金:${info.quarterlyPaid}/${info.quarterlyRent}
            </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 {
@@ -215,11 +248,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 {
@@ -235,4 +276,116 @@
.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;
  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> 
src/components/RightPanel.vue
@@ -3,18 +3,18 @@
    <div class="right-panel-top">
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本月新增用户</div>
          <div class="data-title">本月新增租户数</div>
          <div class="data-tabs">
            <div class="tab-value">{{ newUsers }}<span class="unit">户</span></div>
            <div class="tab-value">{{ staticsData.newTenantCount }}<span class="unit">户</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
      </div>
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本季度交租率</div>
          <div class="data-title">总计租户数</div>
          <div class="data-tabs">
            <div class="tab-value">{{ quarterlyRate }}<span class="unit">元</span></div>
            <div class="tab-value">{{ staticsData.totalTenantCount }}<span class="unit">户</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
@@ -23,18 +23,18 @@
    <div class="right-panel-top1">
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本月新增用户</div>
          <div class="data-title">本季度已交租金</div>
          <div class="data-tabs">
            <div class="tab-value">{{ newUsers }}<span class="unit">万元</span></div>
            <div class="tab-value">{{ staticsData.totalRentPaid }}<span class="unit">万元</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
      </div>
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本季度交租率</div>
          <div class="data-title">本季度应交租金</div>
          <div class="data-tabs">
            <div class="tab-value">{{ quarterlyRate }}<span class="unit">万元</span></div>
            <div class="tab-value">{{ staticsData.totalRentShould }}<span class="unit">万元</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
@@ -44,11 +44,18 @@
      <div class="status-title">实时租赁数据</div>
      <div class="status-items-container">
        <div class="status-items">
          <div v-for="(item, index) in rentList" :key="index" class="status-item">
            <span class="area-name">{{ item.area }}</span>
          <div v-for="(item, index) in realTimeRentData" :key="'original-' + index" class="status-item">
            <span class="area-name">{{ item.streetName }}</span>
            <span class="area-name">{{ item.roomName }}</span>
            <div class="status-actions">
              <span class="action-btn">启用中</span>
              <span class="action-btn">合同签署</span>
              <span class="action-btn" :style="{ backgroundColor: item.leaseStatus == 2 ? '#FFB822' : item.leaseStatus == 1 ? '#66BAF8' : '#FF6B6B' }">{{ ['待出租', '已出租', '维修中'][item.leaseStatus - 1] }}</span>
            </div>
          </div>
          <div v-for="(item, index) in realTimeRentData" :key="'duplicate-' + index" class="status-item">
            <span class="area-name">{{ item.streetName }}</span>
            <span class="area-name">{{ item.roomName }}</span>
            <div class="status-actions">
              <span class="action-btn" :style="{ backgroundColor: item.leaseStatus == 2 ? '#FFB822' : item.leaseStatus == 1 ? '#66BAF8' : '#FF6B6B' }">{{ ['待出租', '已出租', '维修中'][item.leaseStatus - 1] }}</span>
            </div>
          </div>
        </div>
@@ -60,21 +67,18 @@
<script>
export default {
  name: 'RightPanel',
  data() {
    return {
      newUsers: '877',
      quarterlyRate: '4302',
      rentList: [
        { area: '新城大道', status: 'online' },
        { area: '拉萨路', status: 'offline' },
        { area: '新城大道', status: 'online' },
        { area: '拉萨路', status: 'online' },
        { area: '新城大道', status: 'offline' },
        { area: '拉萨路', status: 'online' },
        { area: '新城大道', status: 'offline' },
        { area: '新城大道', status: 'online' }
      ]
  props: {
    staticsData: {
      type: Object,
      default: () => { }
    },
    realTimeRentData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {}
  }
}
</script>
@@ -86,7 +90,7 @@
}
.right-panel {
  width: 300px;
  width: 400px;
  display: flex;
  position: fixed;
  top: 0;
@@ -96,6 +100,7 @@
  z-index: 1000;
  flex-direction: column;
  gap: 10px;
  padding-right: 20px;
}
.data-card {
@@ -103,22 +108,34 @@
  border-radius: 8px;
  padding: 15px;
  height: 80px;
  min-width: 0;
  flex: 1;
  display: flex;
  align-items: center;
}
.data-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  height: 100%;
}
.data-title {
  font-size: 16px;
  color: #fff;
  white-space: nowrap;
  margin-right: 10px;
}
.data-tabs {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  justify-content: flex-end;
  min-width: 0;
}
.tab-value {
@@ -126,49 +143,52 @@
  font-family: 'DIN ', 'DIN', sans-serif;
  font-weight: bold;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: right;
}
.right-panel-top,
.right-panel-top1 {
  display: flex;
  background: rgba(146, 146, 146, 0.5);
  padding: 15px;
  border-radius: 8px;
  gap: 10px;
  height: 110px;
}
.right-panel-top {
  display: flex;
  background: rgba(146, 146, 146, 0.5);
  margin-top: 50px;
  padding-top: 10px;
  padding-bottom: 30px;
  border-radius: 2px;
  margin-right: 20px;
  border-left: 4px solid #87F7C7;
}
.right-panel-top1 {
  display: flex;
  background: rgba(146, 146, 146, 0.5);
  margin-top: 10px;
  padding-top: 10px;
  padding-bottom: 30px;
  border-radius: 2px;
  margin-right: 20px;
  border-left: 4px solid #FFB822;
}
.status-list {
  background: rgba(146, 146, 146, 0.5);
  border-radius: 2px;
  margin-right: 20px;
  border-radius: 8px;
  margin-top: 10px;
  padding: 15px;
  height: 500px;
  max-height: 500px;
  overflow: hidden;
  flex: 1;
}
.status-title {
  font-size: 16px;
  color: #fff;
  margin-bottom: 15px;
}
.status-items {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: calc(100% - 45px);
  animation: scrollUp 20s linear infinite;
}
@@ -179,49 +199,66 @@
.status-items-container {
  height: 100%;
  overflow: hidden;
  position: relative;
}
.status-items-container::before,
.status-items-container::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 10px;
  z-index: 1;
}
.status-items-container::before {
  top: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, transparent 100%);
}
.status-items-container::after {
  bottom: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.1) 0%, transparent 100%);
}
@keyframes scrollUp {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-50%);
  }
}
.status-items::-webkit-scrollbar {
  display: none;
}
.status-items {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.status-item {
  display: flex;
  justify-content: space-between;
  display: grid;
  grid-template-columns: minmax(80px, 1fr) minmax(80px, 1fr) 80px;  /* 设置最小宽度确保文字显示 */
  gap: 10px;
  align-items: center;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 4px;
  padding: 10px;
  margin-bottom: 8px;
}
.area-name {
  font-size: 12px;
  font-size: 14px;
  color: #fff;
  white-space: normal;  /* 允许文字换行 */
  line-height: 1.2;  /* 添加适当的行高 */
}
.status-actions {
  display: flex;
  gap: 10px;
  justify-self: end;
}
.action-btn {
  padding: 2px 8px;
  padding: 4px 12px;
  border-radius: 4px;
  font-size: 12px;
  background: rgba(0, 255, 255, 0.2);
  color: #fff;
  white-space: nowrap;
}
.unit {
src/components/service.js
New file
@@ -0,0 +1,46 @@
import request from '@/utils/request'
// 获取房屋地图分布
export function getStaticsData(data) {
    return request({
        url: '/screen/statics-data',
        method: 'get',
        data
    })
}
// 区域租金排名
export function getRentRank(data) {
    return request({
        url: '/screen/rent-rank',
        method: 'get',
        data
    })
}
// 租金收入趋势
export function getRentIncomeTrend(data) {
    return request({
        url: '/screen/rent-income-trend',
        method: 'get',
        data
    })
}
// 租户数量趋势
export function getTenantCountTrend(data) {
    return request({
        url: '/screen/getTenantCountTrend',
        method: 'get',
        data
    })
}
// 获取实时租赁数据
export function getRealTimeRentData(data) {
    return request({
        url: '/screen/getRealTimeRentData',
        method: 'get',
        data
    })
}
src/main.js
@@ -2,12 +2,15 @@
import App from './App.vue'
import router from './router'
import store from './store'
import './assets/main.css'
Vue.config.productionTip = false
Vue.prototype.$store = store
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
src/store/index.js
New file
@@ -0,0 +1,27 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    mapMarkerStatus: 'all',
  },
  getters: {
    getMapMarkerStatus(state) {
      return state.mapMarkerStatus
    }
  },
  mutations: {
    SET_MAP_MARKER_STATUS(state, status) {
      state.mapMarkerStatus = status
    }
  },
  actions: {
    setMapMarkerStatus({ commit }, status) {
      commit('SET_MAP_MARKER_STATUS', status)
    }
  },
  modules: {
  }
})
src/views/DataScreen.vue
@@ -1,12 +1,12 @@
<template>
  <div >
  <div class="data-screen">
    <Header />
    <div class="main-content">
      <LeftPanel />
    <div v-if="loading" class="main-content">
      <LeftPanel :staticsData="staticsData" :rentRank="rentRank" />
      <MapPanel />
      <RightPanel />
      <RightPanel :staticsData="staticsData" :realTimeRentData="realTimeRentData" />
    </div>
    <BottomCharts />
    <BottomCharts v-if="loading" :rentIncomeTrend="rentIncomeTrend" :tenantCountTrend="tenantCountTrend" />
    <div class="footer"></div>
  </div>
</template>
@@ -17,7 +17,7 @@
import MapPanel from '@/components/MapPanel.vue'
import RightPanel from '@/components/RightPanel.vue'
import BottomCharts from '@/components/BottomCharts.vue'
import { getData } from './service'
import { getStaticsData, getRentRank, getRentIncomeTrend, getTenantCountTrend, getRealTimeRentData } from '@/components/service'
export default {
  name: 'DataScreen',
@@ -28,8 +28,17 @@
    RightPanel,
    BottomCharts
  },
  data() {
    return {
      staticsData: {},
      rentRank: [],
      rentIncomeTrend: [],
      tenantCountTrend: [],
      realTimeRentData: [],
      loading: false
    }
  },
  created() {
    console.log(this.$route);
    if (this.$route.query.token || localStorage.getItem('token')) {
      localStorage.setItem('token', 'Bearer ' + this.$route.query.token)
      this.fetchData()
@@ -37,8 +46,13 @@
  },
  methods: {
    fetchData() {
      getData().then(res => {
        console.log(res)
      Promise.all([getStaticsData(), getRentRank(), getRentIncomeTrend(), getTenantCountTrend(), getRealTimeRentData()]).then(res => {
        this.staticsData = res[0].data
        this.rentRank = res[1].data
        this.rentIncomeTrend = res[2].data
        this.tenantCountTrend = res[3].data
        this.realTimeRentData = res[4].data
        this.loading = true
      })
    }
  }
@@ -46,18 +60,14 @@
</script>
<style>
#app {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  padding: 0 !important;
  overflow: hidden;
  background: linear-gradient(to bottom, #1a2b3c 0%, rgba(26, 43, 60, 0.8) 20%, rgba(26, 43, 60, 0) 100%);
  color: #fff;
  font-family: 'DIN ', 'DIN', sans-serif;
.data-screen {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  position: relative;
  color: #fff;
  font-family: 'DIN', sans-serif;
}
/* 添加全局字体声明 */
@@ -73,15 +83,20 @@
  flex: 1;
  display: flex;
  gap: 10px;
  height: calc(100% - 300px);
  position: relative;
  margin-top: 100px;
  min-height: 0;
}
.footer {
  height: 200px;
  position: fixed;
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100vw;
  width: 100%;
  z-index: 1000;
  background: linear-gradient(to top, rgba(1, 85, 121, 0.8) 0%, rgba(1, 85, 121, 0.4) 20%, rgba(151, 190, 192, 0) 100%);
  pointer-events: none;
}
</style>
src/views/service.js
File was deleted
vite.config.js
@@ -9,10 +9,10 @@
  publicPath: './',
  server: {
    proxy: {
      "/api": {
      "/screen": {
        target: "http://192.168.110.111:8081",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
        rewrite: (path) => path.replace(/^\/screen/, '/screen')
      }
    },
  },