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