hejianhao
2025-04-21 c08d0ebace5e9f20eb442ad7cb1db05d61ecbd0d
src/components/LeftPanel.vue
@@ -4,126 +4,187 @@
      <div class="title">西藏国投租金系统数据看板</div>
      <div class="sub-title">Ecological simulation display in business district</div>
    </div>
    <div class="header-left" style="width: 350px;">
    <div class="header-left" style="width: 410px;">
      <div class="stat-group">
        <div class="stat-item">
          <img :src="require('@/assets/area.png')" width="40" height="40" />
          <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">{{ Number(staticsData.houseTotalArea / 10000).toFixed(2) }}<span class="unit">万m²</span>
            </div>
          </div>
        </div>
        <div class="stat-item">
          <img :src="require('@/assets/rent.png')" width="40" height="40" />
          <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">{{ Number(staticsData.houseRentedArea / 10000).toFixed(2) }}<span class="unit">万m²</span></div>
          </div>
        </div>
      </div>
      <div class="header-right">
        <div class="stat-group">
          <div class="stat-item mt-10">
            <img :src="require('@/assets/money.png')" width="40" height="40" />
            <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">
            <img :src="require('@/assets/successPay.png')" width="40" height="40" />
            <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 class="stat-group mt-10">
        <div class="stat-item">
          <img :src="require('@/assets/arrears.png')" width="40" height="40" />
          <div class="stat-info">
            <div class="stat-label"> <i class="icon-area"></i> 本季度欠费</div>
            <div class="stat-value">{{ staticsData.totalRentOwe }}<span class="unit">万元</span></div>
          </div>
        </div>
        <div class="stat-item">
          <img :src="require('@/assets/totalArrears.png')" width="40" height="40" />
          <div class="stat-info">
            <div class="stat-label"> <i class="icon-area"></i>总计欠费</div>
            <div class="stat-value">{{ staticsData.totalRentOweAll }}<span class="unit">万元</span></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'
          }
        },
        yAxis: {
          type: 'category',
          data: ['新城大道', '拉萨路', '新城大道', '拉萨路', '新城大道', '新城大道', '拉萨路', '新城大道', '拉萨路', '新城大道'],
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        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'
              }]
            }
          }
        }]
      })
    startScroll() {
      this.isScrolling = true;
      if (!this.animationFrameId) {
        this.lastTimestamp = performance.now();
        this.startAutoScroll();
      }
    },
    stopScroll() {
      this.isScrolling = false;
    },
    clearScrollTimer() {
      if (this.animationFrameId) {
        cancelAnimationFrame(this.animationFrameId);
        this.animationFrameId = null;
      }
    },
    startAutoScroll() {
      const animate = (timestamp) => {
        if (!this.isScrolling) {
          this.lastTimestamp = timestamp;
          this.animationFrameId = requestAnimationFrame(animate);
          return;
        }
      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 +197,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 +238,7 @@
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.icon-area,
@@ -205,6 +267,10 @@
  font-size: 24px;
  font-weight: bold;
  color: #0ff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
.unit {
@@ -219,15 +285,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;
}
</style>
.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>