10 changed files with 891 additions and 86 deletions
@ -0,0 +1,523 @@ |
|||
<template> |
|||
<div class="shore-power-usage"> |
|||
<div class="card digital-twin-card--deep-blue"> |
|||
<div style="display: flex; align-items: center; justify-content: space-between;"> |
|||
<div class="card-title"> |
|||
<div class="vertical-line"></div> |
|||
<img src="@/assets/svgs/data.svg" class="title-icon" /> |
|||
<span class="title-text">岸电使用率</span> |
|||
</div> |
|||
<div class="show-value"> |
|||
|
|||
<button v-for="option in timeRangeOptions" :key="option.value" |
|||
:class="['time-range-btn', { active: timeRange === option.value }]" |
|||
@click.stop="handleTimeRangeChange(option.value)"> |
|||
{{ option.label }} |
|||
</button> |
|||
<!-- <span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label || |
|||
''}}量:</span> |
|||
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div> --> |
|||
<el-button class="close-btn" size="small" type="text" @click.stop="handleClose()">x</el-button> |
|||
|
|||
</div> |
|||
|
|||
</div> |
|||
<div style="display: flex; align-items: center; justify-content: space-between;"> |
|||
<div class="time-range-item">{{ startTimeDisplay }} 至 {{ realtimeDeviceDataTime }}</div> |
|||
|
|||
<div v-show="timeRange !== 'year'" class="comparison-type-selector"> |
|||
<button :class="['comparison-type-btn', { active: comparisonType === 'chain' }]" |
|||
@click="comparisonType = 'chain'"> |
|||
环比 |
|||
</button> |
|||
<button :class="['comparison-type-btn', { active: comparisonType === 'yearOnYear' }]" |
|||
@click="comparisonType = 'yearOnYear'"> |
|||
同比 |
|||
</button> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<div v-if="timeRange === 'year'" class="card-content"> |
|||
<ComparisonBarChart :chartData="getYearlyData" title="年度用电量对比" yAxisName="用电量(kWh)" currentColor="#36A2EB" |
|||
previousColor="#FF6384" /> |
|||
</div> |
|||
<div v-if="timeRange === 'month'" class="card-content"> |
|||
<ComparisonBarChart :chartData="getMonthlyComparisonData" |
|||
:title="comparisonType === 'yearOnYear' ? '月度用电量同比对比' : '月度用电量环比对比'" yAxisName="用电量(kWh)" |
|||
currentColor="#36A2EB" previousColor="#FF6384" /> |
|||
</div> |
|||
<div class="card-content" v-if="timeRange === 'day'"> |
|||
<ComparisonBarChart :chartData="getDailyComparisonData" |
|||
:title="comparisonType === 'yearOnYear' ? '日用电量同比对比' : '日用电量环比对比'" yAxisName="用电量(kWh)" currentColor="#36A2EB" |
|||
previousColor="#FF6384" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue' |
|||
import { MapApi } from "@/api/shorepower/map"; |
|||
import ComparisonBarChart from './charts/ComparisonBarChart.vue' |
|||
import { ComparativeData, RealtimeDeviceData } from '@/types/shorepower'; |
|||
|
|||
interface Props { |
|||
// realtimeDeviceData: RealtimeDeviceData[]; |
|||
// activeHeadGroup?: number; |
|||
handleClose: () => void; |
|||
realtimeDeviceDataTime: string; |
|||
comparativeData: ComparativeData; |
|||
initialTimeRange?: 'realtime' | 'day' | 'month' | 'year'; |
|||
} |
|||
|
|||
|
|||
|
|||
const props = defineProps<Props>() |
|||
|
|||
export interface MeasureDataRealtimeRespVO { |
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
createTime: Date; |
|||
/** |
|||
* 设备编号code |
|||
*/ |
|||
deviceCode: string; |
|||
/** |
|||
* 设备编号 |
|||
*/ |
|||
deviceId: number; |
|||
/** |
|||
* 设备名称 |
|||
*/ |
|||
deviceName: string; |
|||
/** |
|||
* 设备状态 |
|||
*/ |
|||
deviceStatus: number; |
|||
/** |
|||
* 编号 |
|||
*/ |
|||
id: number; |
|||
/** |
|||
* 增量费用 |
|||
*/ |
|||
incrementCost?: number; |
|||
/** |
|||
* 增量值 |
|||
*/ |
|||
incrementValue: number; |
|||
/** |
|||
* 采集时间 |
|||
*/ |
|||
measureTime: Date; |
|||
/** |
|||
* 采集值 |
|||
*/ |
|||
measureValue: number; |
|||
} |
|||
|
|||
export interface deviceData { |
|||
[key: string]: MeasureDataRealtimeRespVO |
|||
} |
|||
|
|||
const getYearlyData = computed(() => { |
|||
return [{ |
|||
name: '本期上期对比', |
|||
growthRate: .11, |
|||
currentValue: 22, |
|||
previousValue: 33, |
|||
currentPeriod: '2024', |
|||
previousPeriod: '2025', |
|||
}] |
|||
}) |
|||
|
|||
// 日对比数据(根据对比类型选择环比或同比)- 本地模拟数据 |
|||
const getDailyComparisonData = computed(() => { |
|||
// 本地模拟岸电使用率数据 |
|||
const mockDailyData = { |
|||
chain: { // 环比数据 |
|||
name: '本期上期对比', |
|||
growthRate: .52, // 增长率5.2% |
|||
currentValue: 85.5, // 当前值85.5% |
|||
previousValue: 81.3, // 上期值81.3% |
|||
currentPeriod: new Date().toISOString().split('T')[0], // 当前期间 |
|||
previousPeriod: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 前一天 |
|||
}, |
|||
yearOnYear: { // 同比数据 |
|||
name: '本期去年同期对比', |
|||
growthRate: .128, // 同比增长率12.8% |
|||
currentValue: 85.5, // 当前值85.5% |
|||
previousValue: 75.8, // 去年同期值75.8% |
|||
currentPeriod: new Date().toISOString().split('T')[0], // 当前期间 |
|||
previousPeriod: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 去年同期 |
|||
} |
|||
}; |
|||
|
|||
return [{ |
|||
name: mockDailyData[comparisonType.value].name, |
|||
growthRate: mockDailyData[comparisonType.value].growthRate, |
|||
currentValue: convertPowerUsage(mockDailyData[comparisonType.value].currentValue, selectedCard.value), |
|||
previousValue: convertPowerUsage(mockDailyData[comparisonType.value].previousValue, selectedCard.value), |
|||
currentPeriod: mockDailyData[comparisonType.value].currentPeriod, |
|||
previousPeriod: mockDailyData[comparisonType.value].previousPeriod, |
|||
}] |
|||
}) |
|||
|
|||
// 月对比数据(根据对比类型选择环比或同比)- 本地模拟数据 |
|||
const getMonthlyComparisonData = computed(() => { |
|||
// 本地模拟岸电使用率数据 |
|||
const mockMonthlyData = { |
|||
chain: { // 环比数据 |
|||
name: '本期上期对比', |
|||
growthRate: .83, // 增长率8.3% |
|||
currentValue: 87.2, // 当前值87.2% |
|||
previousValue: 80.5, // 上期值80.5% |
|||
currentPeriod: new Date().toISOString().slice(0, 7), // 当前月份,格式为YYYY-MM |
|||
previousPeriod: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 7), // 上个月,格式为YYYY-MM |
|||
}, |
|||
yearOnYear: { // 同比数据 |
|||
name: '本期去年同期对比', |
|||
growthRate: .156, // 同比增长率15.6% |
|||
currentValue: 87.2, // 当前值87.2% |
|||
previousValue: 75.4, // 去年同期值75.4% |
|||
currentPeriod: new Date().toISOString().slice(0, 7), // 当前月份,格式为YYYY-MM |
|||
previousPeriod: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 7), // 去年同期月份 |
|||
} |
|||
}; |
|||
|
|||
return [{ |
|||
name: mockMonthlyData[comparisonType.value].name, |
|||
growthRate: mockMonthlyData[comparisonType.value].growthRate, |
|||
currentValue: convertPowerUsage(mockMonthlyData[comparisonType.value].currentValue, selectedCard.value), |
|||
previousValue: convertPowerUsage(mockMonthlyData[comparisonType.value].previousValue, selectedCard.value), |
|||
currentPeriod: mockMonthlyData[comparisonType.value].currentPeriod, |
|||
previousPeriod: mockMonthlyData[comparisonType.value].previousPeriod, |
|||
}] |
|||
}) |
|||
|
|||
// 根据时间范围计算起始时间 |
|||
const startTimeDisplay = computed(() => { |
|||
const now = new Date(); |
|||
let startDate: Date; |
|||
|
|||
switch (timeRange.value) { |
|||
case 'day': |
|||
// 今日的零点 |
|||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
|||
break; |
|||
case 'month': |
|||
// 当月的第一天零点 |
|||
startDate = new Date(now.getFullYear(), now.getMonth(), 1); |
|||
break; |
|||
case 'year': |
|||
// 当年的第一天零点 |
|||
startDate = new Date(now.getFullYear(), 0, 1); |
|||
break; |
|||
/* case 'realtime': |
|||
// 总数据从2020年开始 |
|||
startDate = new Date(2020, 0, 1); |
|||
break; */ |
|||
default: |
|||
// 默认为今日的零点 |
|||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
|||
} |
|||
|
|||
// 格式化日期时间 |
|||
const year = startDate.getFullYear(); |
|||
const month = String(startDate.getMonth() + 1).padStart(2, '0'); |
|||
const day = String(startDate.getDate()).padStart(2, '0'); |
|||
const hours = String(startDate.getHours()).padStart(2, '0'); |
|||
const minutes = String(startDate.getMinutes()).padStart(2, '0'); |
|||
const seconds = String(startDate.getSeconds()).padStart(2, '0'); |
|||
|
|||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
|||
}); |
|||
|
|||
const selectedCard = ref<string>('overview') |
|||
|
|||
// 时间范围选项 |
|||
const timeRange = ref<'day' | 'month' | 'year'>( |
|||
(props.initialTimeRange === 'realtime' ? 'year' : props.initialTimeRange) || 'day' |
|||
) |
|||
|
|||
// 比较类型选项:chain(环比)或 yearOnYear(同比) |
|||
const comparisonType = ref<'chain' | 'yearOnYear'>('chain') |
|||
|
|||
// 时间范围选项定义 |
|||
const timeRangeOptions = [ |
|||
{ value: 'day', label: '当日' }, |
|||
{ value: 'month', label: '当月' }, |
|||
{ value: 'year', label: '当年' }, |
|||
// { value: 'realtime', label: '汇总' }, |
|||
] |
|||
|
|||
// 处理时间范围选择 |
|||
const handleTimeRangeChange = (range: 'day' | 'month' | 'year') => { |
|||
timeRange.value = range |
|||
// 这里可以添加根据时间范围切换数据源的逻辑 |
|||
} |
|||
|
|||
// 通用数据转换函数 |
|||
const convertPowerUsage = (powerUsage: number, type: string): number => { |
|||
const conversionFactors: Record<string, number> = { |
|||
totalPower: 1, // 直接使用原值 |
|||
fuel: 0.22 / 1, // 转化为千克 |
|||
co2: 670 / 1000, // 克转化为千克 |
|||
pm25: 1.46 / 1000, // 克转化为千克 |
|||
nox: 18.1 / 1000, // 克转化为千克 |
|||
so2: 10.5 / 1000 // 克转化为千克 |
|||
}; |
|||
|
|||
const factor = conversionFactors[type] || 1; |
|||
return Number((powerUsage * factor).toFixed(2)); |
|||
}; |
|||
|
|||
|
|||
/* watch(() => props.realtimeDeviceData, (newValue) => { |
|||
handleGetRealTimeAllData(newValue) |
|||
}) */ |
|||
|
|||
// 组件挂载时初始化数据和定时器 |
|||
// 添加一个变量来保存事件处理函数的引用 |
|||
let cardSelectedHandler: any = null |
|||
|
|||
onMounted(() => { |
|||
// handleGetAllDeviceDataByTimeRange() |
|||
// handleGetRealTimeAllData(props.realtimeDeviceData) |
|||
|
|||
// 添加卡片选择事件监听器 |
|||
// cardSelectedHandler = (event: CustomEvent) => { |
|||
// selectedCard.value = event.detail |
|||
// } |
|||
// window.addEventListener('cardSelected', cardSelectedHandler) |
|||
}) |
|||
|
|||
// 监听 activeHeadGroup 变化,当组件变为可见时触发 resize |
|||
watch(() => props.activeHeadGroup, (newVal) => { |
|||
if (newVal === 1) { |
|||
// 延迟执行 resize,确保 DOM 已经更新 |
|||
setTimeout(() => { |
|||
window.dispatchEvent(new Event('resize')) |
|||
}, 100) |
|||
} |
|||
}, { immediate: true }) |
|||
|
|||
// 监听 initialTimeRange 变化,当父组件传递新的时间范围时更新当前时间范围 |
|||
watch(() => props.initialTimeRange, (newVal) => { |
|||
console.log('newVal', newVal) |
|||
if (newVal) { |
|||
if (newVal === 'realtime') { |
|||
timeRange.value = 'year' |
|||
} else { |
|||
timeRange.value = newVal as 'day' | 'month' | 'year' |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// 组件销毁前清除定时器 |
|||
onBeforeUnmount(() => { |
|||
if (cardSelectedHandler) { |
|||
window.removeEventListener('cardSelected', cardSelectedHandler) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.shore-power-usage { |
|||
display: flex; |
|||
height: 100%; |
|||
width: 100%; |
|||
gap: 10px; |
|||
} |
|||
|
|||
.card .card-content { |
|||
overflow: hidden !important; |
|||
} |
|||
|
|||
.average { |
|||
|
|||
.overview-value { |
|||
font-size: 36px; |
|||
} |
|||
|
|||
.overview-label { |
|||
font-size: 18px; |
|||
} |
|||
} |
|||
|
|||
.magnify { |
|||
width: 100%; |
|||
padding: 12px; |
|||
height: calc(100vh - 72px); |
|||
position: absolute; |
|||
top: 72px; |
|||
left: 0px; |
|||
display: flex; |
|||
// flex-direction: column; |
|||
/* background-color: red; */ |
|||
gap: 8px; |
|||
overflow-y: auto; |
|||
|
|||
.big { |
|||
height: 100%; |
|||
width: calc(55%); |
|||
|
|||
.card { |
|||
height: 100%; |
|||
} |
|||
|
|||
|
|||
.card .card-title .title-text { |
|||
width: 120px; |
|||
} |
|||
|
|||
.overview-value { |
|||
font-size: 48px; |
|||
} |
|||
|
|||
.overview-label { |
|||
font-size: 28px; |
|||
} |
|||
} |
|||
|
|||
.right-row { |
|||
width: calc(45% - 8px); |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
flex: 1; |
|||
gap: 8px; |
|||
// background-color: red; |
|||
} |
|||
|
|||
.one-row { |
|||
height: 30%; |
|||
display: flex; |
|||
gap: 8px; |
|||
|
|||
.card .card-title .title-text { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.card { |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.card { |
|||
padding: 6px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.time-range-selector { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.show-value { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
font-weight: bold; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
margin-bottom: 8px; |
|||
color: #FFF; |
|||
} |
|||
|
|||
.show-value-label { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.show-value-value { |
|||
font-size: 20px; |
|||
font-weight: 600; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.time-range-item { |
|||
font-size: 24px; |
|||
font-weight: 600; |
|||
font-weight: bold; |
|||
color: rgba(255, 255, 255, 0.7); |
|||
} |
|||
|
|||
.growth-rate { |
|||
flex: 1; |
|||
text-align: right; |
|||
margin-left: 10px; |
|||
font-weight: bold; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-size: 32px; |
|||
} |
|||
|
|||
.growth-up { |
|||
color: #52C41A; |
|||
} |
|||
|
|||
.growth-down { |
|||
color: #F5222D; |
|||
} |
|||
|
|||
.growth-icon { |
|||
margin-right: 4px; |
|||
} |
|||
|
|||
.comparison-type-selector { |
|||
display: inline-flex; |
|||
margin: 0 10px; |
|||
width: fit-content; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
padding: 2px; |
|||
// margin-bottom: 8px; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.comparison-type-btn { |
|||
border: none; |
|||
background: transparent; |
|||
color: #ccc; |
|||
padding: 4px 8px; |
|||
border-radius: 2px; |
|||
cursor: pointer; |
|||
font-size: 14px; |
|||
transition: all 0.3s; |
|||
|
|||
&.active { |
|||
background: #1890ff; |
|||
color: white; |
|||
} |
|||
} |
|||
|
|||
.right { |
|||
/* gap: 0 */ |
|||
} |
|||
|
|||
.close-btn { |
|||
width: 24px; |
|||
height: 24px; |
|||
// border-radius: 50%; |
|||
// background-color: #ff4d4f; |
|||
color: white; |
|||
border: none; |
|||
font-weight: bold; |
|||
font-size: 16px; |
|||
cursor: pointer; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-left: 10px; |
|||
// margin-bottom: 8px; |
|||
} |
|||
|
|||
.close-btn:hover { |
|||
color: #f5222d; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue