hejianhao
2025-03-26 152c4cbe31e86d5f933d9ba69512c6d4a6f0e174
分组件
2个文件已修改
5个文件已添加
2022 ■■■■ 已修改文件
src/App.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BottomCharts.vue 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Header.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/LeftPanel.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/MapPanel.vue 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RightPanel.vue 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/DataScreen.vue 977 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -4,20 +4,26 @@
  </div>
</template>
<script>
export default {
  name: 'App',
  data() {
    return {
    }
  },
}
</script>
<style>
#app {
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000033;
}
#app {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>
src/components/BottomCharts.vue
New file
@@ -0,0 +1,217 @@
<template>
  <div class="bottom-charts">
    <div class="chart-group">
      <div class="chart-title">收入趋势</div>
      <div class="income-chart" ref="incomeChart"></div>
    </div>
    <div class="chart-group">
      <div class="chart-title">租金收入趋势图</div>
      <div class="trend-chart" ref="rentTrendChart"></div>
    </div>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'BottomCharts',
  data() {
    return {
      charts: {
        income: null,
        rentTrend: null
      }
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initCharts()
    })
  },
  methods: {
    initCharts() {
      // 收入趋势图表
      this.charts.income = echarts.init(this.$refs.incomeChart)
      this.charts.income.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        yAxis: {
          type: 'value',
          splitLine: {
            show: false
          },
          axisLine: {
            show: true,
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        series: [{
          data: [100, 400, 200, 300, 200, 400, 300],
          type: 'line',
          smooth: true,
          symbol: 'circle',
          symbolSize: 8,
          lineStyle: {
            color: '#ffd700',
            width: 2
          },
          itemStyle: {
            color: '#ffd700'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0,
                color: 'rgba(255, 215, 0, 0.3)'
              }, {
                offset: 1,
                color: 'rgba(255, 215, 0, 0.1)'
              }]
            }
          }
        }]
      })
      // 租金趋势图表
      this.charts.rentTrend = echarts.init(this.$refs.rentTrendChart)
      this.charts.rentTrend.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        yAxis: {
          type: 'value',
          splitLine: {
            show: false
          },
          axisLine: {
            show: true,
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        series: [{
          data: [300, 200, 400, 300, 500, 400, 300],
          type: 'line',
          smooth: true,
          symbol: 'circle',
          symbolSize: 8,
          lineStyle: {
            color: '#4facfe',
            width: 2
          },
          itemStyle: {
            color: '#4facfe'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0,
                color: 'rgba(79,172,254,0.3)'
              }, {
                offset: 1,
                color: 'rgba(79,172,254,0.1)'
              }]
            }
          }
        }]
      })
      window.addEventListener('resize', () => {
        Object.values(this.charts).forEach(chart => {
          chart && chart.resize()
        })
      })
    }
  }
}
</script>
<style scoped>
.bottom-charts {
  height: 200px;
  width: 700px;
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 1003;
  display: flex;
  gap: 10px;
}
.chart-group {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 10px;
}
.chart-title {
  font-size: 16px;
  margin-bottom: 15px;
  color: #fff;
}
.income-chart,
.trend-chart {
  height: calc(100% - 30px);
}
</style>
src/components/Header.vue
New file
@@ -0,0 +1,95 @@
<template>
  <div class="header">
    <div class="time-weather">
      <span class="time">{{ currentTime }}</span>
      <span class="date">{{ currentDate }}</span>
      <span class="weather">
        <i class="weather-icon"></i>
        {{ temperature }}°C
      </span>
    </div>
  </div>
</template>
<script>
import dayjs from 'dayjs'
export default {
  name: 'Header',
  data() {
    return {
      currentTime: '',
      currentDate: '',
      temperature: '--'
    }
  },
  mounted() {
    this.startTimeUpdate()
    this.startWeatherUpdate()
  },
  methods: {
    startTimeUpdate() {
      const updateTime = () => {
        this.currentTime = dayjs().format('HH:mm:ss')
        this.currentDate = dayjs().format('YYYY/MM/DD')
      }
      updateTime()
      setInterval(updateTime, 1000)
    },
    startWeatherUpdate() {
      const updateWeather = async () => {
        try {
          const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=29.6487&longitude=91.1172&current=temperature_2m&timezone=Asia%2FShanghai')
          const data = await response.json()
          if (data.current) {
            this.temperature = Math.round(data.current.temperature_2m)
          }
        } catch (error) {
          console.error('获取天气数据失败:', error)
        }
      }
      updateWeather()
      setInterval(updateWeather, 30 * 60 * 1000)
    }
  }
}
</script>
<style scoped>
.header {
  height: 120px;
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  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);
}
.time-weather {
  display: flex;
  gap: 20px;
  align-items: center;
}
.time {
  font-size: 24px;
  font-weight: bold;
}
.date {
  color: #fff;
}
.weather {
  display: flex;
  align-items: center;
  gap: 5px;
}
</style>
src/components/LeftPanel.vue
New file
@@ -0,0 +1,233 @@
<template>
  <div class="left-panel">
    <div class="header-center">
      <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="stat-group">
        <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>
        </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>
        </div>
      </div>
      <div class="header-right">
        <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>
          </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>
          </div>
        </div>
      </div>
    </div>
    <div class="chart-card" style="width: 350px;">
      <div class="chart-title">区域租金分析</div>
      <div class="area-chart" ref="areaRentChart"></div>
    </div>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'LeftPanel',
  data() {
    return {
      totalArea: '386.5',
      rentedArea: '316.5',
      todayRent: '316.5',
      todayIncome: '124.5',
      charts: {
        areaRent: null
      }
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initCharts()
    })
  },
  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'
              }]
            }
          }
        }]
      })
      window.addEventListener('resize', () => {
        this.charts.areaRent && this.charts.areaRent.resize()
      })
    }
  }
}
</script>
<style scoped>
.left-panel {
  width: 400px;
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  height: calc(100vh);
  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;
  padding-bottom: 200px;
  gap: 10px;
}
.header-center {
  margin-left: 20px;
  margin-top: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #8CB4C6;
}
.title {
  font-size: 30px;
  font-weight: bold;
  background: linear-gradient(90deg, #fff, #0ff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.sub-title {
  font-size: 12px;
  color: fff;
}
.stat-group {
  display: flex;
  gap: 10px;
}
.stat-item {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.icon-area,
.icon-money {
  width: 40px;
  height: 40px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
}
.stat-info {
  flex: 1;
}
.mt-10 {
  margin-top: 10px;
}
.stat-label {
  font-size: 14px;
  color: #fff;
  margin-bottom: 5px;
}
.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #0ff;
}
.unit {
  font-size: 14px;
  margin-left: 4px;
  color: #fff;
}
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  min-height: 300px;
}
.chart-title {
  font-size: 16px;
  margin-bottom: 15px;
  color: #fff;
}
.area-chart {
  height: calc(100% - 30px);
}
</style>
src/components/MapPanel.vue
New file
@@ -0,0 +1,238 @@
<template>
  <div class="center-panel">
    <div class="map-container" ref="mapContainer"></div>
  </div>
</template>
<script>
import AMapLoader from '@amap/amap-jsapi-loader'
export default {
  name: 'MapPanel',
  data() {
    return {
      map: null,
      markers: [],
      infoWindow: null
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initMap()
    })
  },
  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)
      }
    },
    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
            })
          })
          requestAnimationFrame(animate)
        }
        animate()
      })
    }
  }
}
</script>
<style scoped>
.center-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.map-container {
  border-radius: 8px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
  width: 100%;
  height: 100vh;
}
.info-window {
  padding: 10px;
  background: rgba(146, 146, 146, 0.5);
  border-radius: 8px;
  z-index: 10003;
  min-width: 200px;
}
.info-title {
  font-size: 16px;
  font-weight: bold;
  color: #fff;
  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: #fff;
}
.info-window[data-status="rented"] .info-title {
  color: #fff;
}
.amap-info {
  z-index: 10003 !important;
  display: block !important;
}
.amap-info-content {
  background: rgba(146, 146, 146, 0.5);
  padding: 0 !important;
}
.amap-info-sharp {
  display: none !important;
}
</style>
src/components/RightPanel.vue
New file
@@ -0,0 +1,232 @@
<template>
  <div class="right-panel">
    <div class="right-panel-top">
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本月新增用户</div>
          <div class="data-tabs">
            <div class="tab-value">{{ newUsers }}<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-tabs">
            <div class="tab-value">{{ quarterlyRate }}<span class="unit">元</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="right-panel-top1">
      <div class="data-card">
        <div class="data-header flex">
          <div class="data-title">本月新增用户</div>
          <div class="data-tabs">
            <div class="tab-value">{{ newUsers }}<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-tabs">
            <div class="tab-value">{{ quarterlyRate }}<span class="unit">万元</span></div>
            <div class="tab-chart"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="status-list">
      <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 class="status-actions">
              <span class="action-btn">启用中</span>
              <span class="action-btn">合同签署</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<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' }
      ]
    }
  }
}
</script>
<style scoped>
.flex {
  display: flex;
  flex-direction: column;
}
.right-panel {
  width: 300px;
  display: flex;
  position: fixed;
  top: 0;
  right: 0;
  height: calc(100vh - 100px);
  margin-top: 100px;
  z-index: 1000;
  flex-direction: column;
  gap: 10px;
}
.data-card {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  height: 80px;
}
.data-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.data-title {
  font-size: 16px;
}
.data-tabs {
  display: flex;
  align-items: center;
  gap: 10px;
}
.tab-value {
  font-size: 30px;
  font-family: 'DIN ', 'DIN', sans-serif;
  font-weight: bold;
  color: #fff;
}
.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;
  margin-top: 10px;
  padding: 15px;
  height: 500px;
  overflow: hidden;
}
.status-title {
  font-size: 16px;
}
.status-items {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: calc(100% - 45px);
  animation: scrollUp 20s linear infinite;
}
.status-items:hover {
  animation-play-state: paused;
}
.status-items-container {
  height: 100%;
  overflow: hidden;
}
@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;
  align-items: center;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 4px;
}
.area-name {
  font-size: 12px;
}
.status-actions {
  display: flex;
  gap: 10px;
}
.action-btn {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
  background: rgba(0, 255, 255, 0.2);
}
.unit {
  font-size: 14px;
  margin-left: 4px;
  color: #fff;
}
</style>
src/views/DataScreen.vue
@@ -1,177 +1,32 @@
<template>
  <div id="app">
    <div class="header">
      <div class="time-weather">
        <span class="time">{{ currentTime }}</span>
        <span class="date">{{ currentDate }}</span>
        <span class="weather">
          <i class="weather-icon"></i>
          {{ temperature }}°C
        </span>
      </div>
    </div>
  <div >
    <Header />
    <div class="main-content">
      <div class="left-panel">
        <div class="header-center">
          <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="stat-group">
            <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>
            </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>
            </div>
          </div>
          <div class="header-right">
            <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>
              </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>
              </div>
            </div>
          </div>
        </div>
        <div class="chart-card" style="width: 350px;">
          <div class="chart-title">区域租金分析</div>
          <div class="area-chart" ref="areaRentChart"></div>
        </div>
        <div class="bottom-charts">
          <div class="bottom-charts">
            <div class="chart-group">
              <div class="chart-title">收入趋势</div>
              <div class="income-chart" ref="incomeChart"></div>
            </div>
            <div class="chart-group">
              <div class="chart-title">租金收入趋势图</div>
              <div class="trend-chart" ref="rentTrendChart"></div>
            </div>
          </div>
        </div>
      </div>
      <div class="center-panel">
        <div class="map-container" ref="mapContainer"></div>
      </div>
      <div class="right-panel">
        <div class="right-panel-top">
          <div class="data-card">
            <div class="data-header flex">
              <div class="data-title">本月新增用户</div>
              <div class="data-tabs">
                <div class="tab-value">{{ newUsers }}<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-tabs">
                <div class="tab-value">{{ quarterlyRate }}<span class="unit">元</span></div>
                <div class="tab-chart"></div>
              </div>
            </div>
          </div>
        </div>
        <div class="right-panel-top1">
          <div class="data-card">
            <div class="data-header flex">
              <div class="data-title">本月新增用户</div>
              <div class="data-tabs">
                <div class="tab-value">{{ newUsers }}<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-tabs">
                <div class="tab-value">{{ quarterlyRate }}<span class="unit">万元</span></div>
                <div class="tab-chart"></div>
              </div>
            </div>
          </div>
        </div>
        <div class="status-list">
          <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 class="status-actions">
                  <span class="action-btn">启用中</span>
                  <span class="action-btn">合同签署</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <LeftPanel />
      <MapPanel />
      <RightPanel />
    </div>
    <BottomCharts />
    <div class="footer"></div>
  </div>
</template>
<script>
import AMapLoader from '@amap/amap-jsapi-loader'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
import Header from '@/components/Header.vue'
import LeftPanel from '@/components/LeftPanel.vue'
import MapPanel from '@/components/MapPanel.vue'
import RightPanel from '@/components/RightPanel.vue'
import BottomCharts from '@/components/BottomCharts.vue'
import { getData } from './service'
export default {
  name: 'DataScreen',
  data() {
    return {
      map: null,
      markers: [],
      infoWindow: null,
      currentTime: '',
      currentDate: '',
      temperature: '--',
      totalArea: '386.5',
      rentedArea: '316.5',
      todayRent: '316.5',
      todayIncome: '124.5',
      newUsers: '877',
      quarterlyRate: '4302',
      weather: null,
      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' }
      ],
      charts: {
        areaRent: null,
        income: null,
        rentTrend: null
      }
    }
  components: {
    Header,
    LeftPanel,
    MapPanel,
    RightPanel,
    BottomCharts
  },
  created() {
    console.log(this.$route);
@@ -180,408 +35,10 @@
      this.fetchData()
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initMap()
      this.initCharts()
    })
    this.startTimeUpdate()
    this.startWeatherUpdate()
  },
  methods: {
    fetchData() {
      getData().then(res => {
        console.log(res)
      })
    },
    startTimeUpdate() {
      const updateTime = () => {
        this.currentTime = dayjs().format('HH:mm:ss')
        this.currentDate = dayjs().format('YYYY/MM/DD')
      }
      updateTime()
      setInterval(updateTime, 1000)
    },
    startWeatherUpdate() {
      const updateWeather = async () => {
        try {
          // 使用 OpenMeteo API
          const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=29.6487&longitude=91.1172&current=temperature_2m&timezone=Asia%2FShanghai')
          const data = await response.json()
          if (data.current) {
            this.temperature = Math.round(data.current.temperature_2m)
          }
        } catch (error) {
          console.error('获取天气数据失败:', error)
        }
      }
      // 立即更新一次
      updateWeather()
      // 每30分钟更新一次
      setInterval(updateWeather, 30 * 60 * 1000)
    },
    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)
      }
    },
    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
            })
          })
          requestAnimationFrame(animate)
        }
        animate()
      })
    },
    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, 1100, 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 0],
          barWidth: '30%',
          itemStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 1,
              y2: 0,
              colorStops: [{
                offset: 0,
                color: '#0ff'
              }, {
                offset: 1,
                color: '#0ff'
              }]
            }
          }
        }]
      })
      // 收入趋势图表
      this.charts.income = echarts.init(this.$refs.incomeChart)
      this.charts.income.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        yAxis: {
          type: 'value',
          splitLine: {
            show: false
          },
          axisLine: {
            show: true,
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        series: [{
          data: [100, 400, 200, 300, 200, 400, 300],
          type: 'line',
          smooth: true,
          symbol: 'circle',
          symbolSize: 8,
          lineStyle: {
            color: '#ffd700',
            width: 2
          },
          itemStyle: {
            color: '#ffd700'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0,
                color: 'rgba(255, 215, 0, 0.3)'
              }, {
                offset: 1,
                color: 'rgba(255, 215, 0, 0.1)'
              }]
            }
          }
        }]
      })
      // 租金趋势图表
      this.charts.rentTrend = echarts.init(this.$refs.rentTrendChart)
      this.charts.rentTrend.setOption({
        grid: {
          top: '15%',
          left: '3%',
          right: '3%',
          bottom: '3%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis'
        },
        xAxis: {
          type: 'category',
          data: ['23-4月', '23-12月', '24-3月', '24-4月', '24-5月', '24-6月', '25-3月'],
          axisLine: {
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        yAxis: {
          type: 'value',
          splitLine: {
            show: false
          },
          axisLine: {
            show: true,
            lineStyle: {
              color: '#fff'
            }
          },
          axisLabel: {
            color: '#fff'
          }
        },
        series: [{
          data: [300, 200, 400, 300, 500, 400, 300],
          type: 'line',
          smooth: true,
          symbol: 'circle',
          symbolSize: 8,
          lineStyle: {
            color: '#4facfe',
            width: 2
          },
          itemStyle: {
            color: '#4facfe'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0,
                color: 'rgba(79,172,254,0.3)'
              }, {
                offset: 1,
                color: 'rgba(79,172,254,0.1)'
              }]
            }
          }
        }]
      })
      // 监听窗口大小变化
      window.addEventListener('resize', () => {
        Object.values(this.charts).forEach(chart => {
          chart && chart.resize()
        })
      })
    }
  }
@@ -589,11 +46,6 @@
</script>
<style>
.flex {
  display: flex;
  flex-direction: column;
}
#app {
  width: 100vw;
  height: 100vh;
@@ -617,20 +69,10 @@
  font-display: swap;
}
.header {
  height: 120px;
.main-content {
  flex: 1;
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  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;
  /* background: rgba(255, 255, 255, 0.05); */
  backdrop-filter: blur(10px);
  gap: 10px;
}
.footer {
@@ -641,384 +83,5 @@
  width: 100vw;
  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%);
}
.header-left,
.header-right {
  /* width: 30%; */
}
.header-center {
  /* text-align: center; */
  margin-left: 20px;
  margin-top: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #8CB4C6;
}
.title {
  font-size: 30px;
  font-weight: bold;
  background: linear-gradient(90deg, #fff, #0ff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /* margin-bottom: 10px; */
}
.sub-title {
  font-size: 12px;
  color: fff;
}
.time-weather {
  display: flex;
  gap: 20px;
  align-items: center;
  /* justify-content: center; */
}
.time {
  font-size: 24px;
  font-weight: bold;
}
.date {
  color: #fff;
}
.weather {
  display: flex;
  align-items: center;
  gap: 5px;
}
.main-content {
  flex: 1;
  display: flex;
  /* padding: 10px; */
  gap: 10px;
}
.left-panel {
  width: 400px;
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  height: calc(100vh);
  background: linear-gradient(to right, rgba(1, 85, 121, 0.8) 0%, rgba(1, 85, 121, 0.8) 20%, rgba(151, 190, 192, 0));
  /* margin-top: 100px; */
  /* padding-top: 10px; */
  z-index: 1002;
  flex-direction: column;
  padding-bottom: 200px;
  gap: 10px;
}
.bottom-charts {
  height: 200px;
  width: 700px;
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 1000;
  /* background: linear-gradient(to right, rgba(1, 85, 121, 0.1) 0%, rgba(1, 85, 121, 0.1) 50%, rgba(151, 190, 192, 0) 100%); */
}
.right-panel {
  width: 300px;
  display: flex;
  position: fixed;
  top: 0;
  right: 0;
  height: calc(100vh - 100px);
  margin-top: 100px;
  z-index: 1000;
  flex-direction: column;
  gap: 10px;
}
.center-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.map-container {
  flex: 1;
  border-radius: 8px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
  width: 100%;
  height: 100%;
  min-height: 500px;
}
.bottom-charts {
  /* height: 200px; */
  display: flex;
  gap: 10px;
  /* margin-bottom: 40px; */
}
.stat-group {
  display: flex;
  gap: 10px;
}
.stat-item {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.icon-area,
.icon-money {
  width: 40px;
  height: 40px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 50%;
}
.stat-info {
  flex: 1;
}
.mt-10 {
  margin-top: 10px;
}
.stat-label {
  font-size: 14px;
  color: #fff;
  margin-bottom: 5px;
}
.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #0ff;
}
.unit {
  font-size: 14px;
  margin-left: 4px;
  color: #fff;
}
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  min-height: 300px;
}
.chart-title {
  font-size: 16px;
  margin-bottom: 15px;
  color: #fff;
}
.area-chart {
  height: calc(100% - 30px);
}
.data-card {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 15px;
  height: 80px;
}
.data-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.data-title {
  font-size: 16px;
}
.data-tabs {
  display: flex;
  align-items: center;
  gap: 10px;
}
.tab-value {
  font-size: 30px;
  font-family: 'DIN ', 'DIN', sans-serif;
  font-weight: bold;
  color: #fff;
  /* font-family: "DINPro-Bold"; */
}
.status-list {
  background: rgba(146, 146, 146, 0.5);
  border-radius: 2px;
  margin-right: 20px;
  margin-top: 10px;
  padding: 15px;
  height: 500px;
  overflow: hidden;
}
.status-title {
  font-size: 16px;
  /* margin-bottom: 15px; */
}
.status-items {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: calc(100% - 45px);
  animation: scrollUp 20s linear infinite;
}
.status-items:hover {
  animation-play-state: paused;
}
.status-items-container {
  height: 100%;
  overflow: hidden;
}
@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;
  align-items: center;
  /* padding: 10px; */
  background: rgba(255, 255, 255, 0.05);
  border-radius: 4px;
}
.area-name {
  font-size: 12px;
}
.status-actions {
  display: flex;
  gap: 10px;
}
.action-btn {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
  background: rgba(0, 255, 255, 0.2);
}
.chart-group {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
  padding: 10px;
}
.income-chart,
.trend-chart {
  height: calc(100% - 30px);
}
.info-window {
  padding: 10px;
  background: rgba(146, 146, 146, 0.5);
  border-radius: 8px;
  z-index: 10003;
  min-width: 200px;
}
.info-title {
  font-size: 16px;
  font-weight: bold;
  color: #fff;
  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: #fff;
}
.info-window[data-status="rented"] .info-title {
  color: #fff;
}
.amap-info {
  z-index: 10003 !important;
  display: block !important;
}
.amap-info-content {
  background: rgba(146, 146, 146, 0.5);
  padding: 0 !important;
}
.amap-info-sharp {
  display: none !important;
}
.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;
  /* height: 100px; */
}
.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;
  /* height: 100px; */
}
</style>