| | |
| | | <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'; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .icon-area, |
| | |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #0ff; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | width: 100%; |
| | | } |
| | | |
| | | .unit { |
| | |
| | | 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> |