Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
fb93301585 | 4 days ago |
|
|
bf8d9a5a49 | 5 days ago |
|
|
86b72ae916 | 6 days ago |
|
|
14c198b985 | 7 days ago |
|
|
7ceb1c731c | 7 days ago |
|
|
8d5f906b8a | 1 week ago |
|
|
88fa1ce11a | 1 week ago |
|
|
4ca9409216 | 2 weeks ago |
|
|
387e7afc33 | 2 weeks ago |
|
|
c91b673487 | 2 weeks ago |
|
|
e996449376 | 3 weeks ago |
|
|
369a6dd341 | 3 weeks ago |
|
|
970cadcca8 | 3 weeks ago |
|
|
9e7040ec83 | 3 weeks ago |
|
|
75c2bd64ad | 3 weeks ago |
|
|
7e8c0a1607 | 3 weeks ago |
|
|
f8f85d653f | 4 weeks ago |
18 changed files with 5642 additions and 1598 deletions
@ -0,0 +1,468 @@ |
|||
<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" chartType="percentage" /> |
|||
</div> |
|||
<div v-if="timeRange === 'month'" class="card-content"> |
|||
<ComparisonBarChart :chartData="getMonthlyComparisonData" |
|||
:title="comparisonType === 'yearOnYear' ? '月度用电量同比对比' : '月度用电量环比对比'" yAxisName="用电量(kWh)" |
|||
currentColor="#36A2EB" previousColor="#FF6384" chartType="percentage" /> |
|||
</div> |
|||
<div class="card-content" v-if="timeRange === 'day'"> |
|||
<ComparisonBarChart :chartData="getDailyComparisonData" |
|||
:title="comparisonType === 'yearOnYear' ? '日用电量同比对比' : '日用电量环比对比'" yAxisName="用电量(kWh)" currentColor="#36A2EB" |
|||
previousColor="#FF6384" chartType="percentage" /> |
|||
</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, MetricData, RealtimeDeviceData } from '@/types/shorepower'; |
|||
|
|||
interface Props { |
|||
// realtimeDeviceData: RealtimeDeviceData[]; |
|||
// activeHeadGroup?: number; |
|||
handleClose: () => void; |
|||
realtimeDeviceDataTime: string; |
|||
comparativeData: MetricData; |
|||
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: props.comparativeData.year.growthRate, |
|||
currentValue: convertPowerUsage(props.comparativeData.year.current.value, selectedCard.value), |
|||
previousValue: convertPowerUsage(props.comparativeData.year.previous.value, selectedCard.value), |
|||
currentPeriod: props.comparativeData.year.current.period, |
|||
previousPeriod: props.comparativeData.year.previous.period, |
|||
}] |
|||
}) |
|||
|
|||
// 日对比数据(根据对比类型选择环比或同比) |
|||
const getDailyComparisonData = computed(() => { |
|||
const comparisonData = comparisonType.value === 'yearOnYear' |
|||
? props.comparativeData.dayYearOnYear |
|||
: props.comparativeData.day; |
|||
|
|||
return [{ |
|||
name: comparisonType.value === 'yearOnYear' ? '本期去年同期对比' : '本期上期对比', |
|||
growthRate: comparisonData.growthRate, |
|||
currentValue: convertPowerUsage(comparisonData.current.value, selectedCard.value), |
|||
previousValue: convertPowerUsage(comparisonData.previous.value, selectedCard.value), |
|||
currentPeriod: comparisonData.current.period, |
|||
previousPeriod: comparisonData.previous.period, |
|||
}] |
|||
}) |
|||
|
|||
// 月对比数据(根据对比类型选择环比或同比) |
|||
const getMonthlyComparisonData = computed(() => { |
|||
const comparisonData = comparisonType.value === 'yearOnYear' |
|||
? props.comparativeData.monthYearOnYear |
|||
: props.comparativeData.month; |
|||
|
|||
return [{ |
|||
name: comparisonType.value === 'yearOnYear' ? '本期去年同期对比' : '本期上期对比', |
|||
growthRate: comparisonData.growthRate, |
|||
currentValue: convertPowerUsage(comparisonData.current.value, selectedCard.value), |
|||
previousValue: convertPowerUsage(comparisonData.previous.value, selectedCard.value), |
|||
currentPeriod: comparisonData.current.period, |
|||
previousPeriod: comparisonData.previous.period, |
|||
}] |
|||
}) |
|||
|
|||
// 根据时间范围计算起始时间 |
|||
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; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
.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> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,196 @@ |
|||
<template> |
|||
<div ref="chartContainer" class="comparison-bar-chart-container"></div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue' |
|||
import * as echarts from 'echarts' |
|||
|
|||
// 定义组件属性 |
|||
interface Props { |
|||
chartData?: Array<{ |
|||
name: string |
|||
currentValue: number |
|||
previousValue: number |
|||
growthRate?: number |
|||
currentPeriod?: string |
|||
previousPeriod?: string |
|||
}> |
|||
title?: string |
|||
currentColor?: string |
|||
previousColor?: string |
|||
showYAxis?: boolean |
|||
yAxisName?: string |
|||
currentLabel?: string |
|||
previousLabel?: string |
|||
chartType?: 'default' | 'percentage' |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
chartData: () => [], |
|||
title: '', |
|||
currentColor: '#1890FF', |
|||
previousColor: '#A9A9A9', |
|||
showYAxis: true, |
|||
yAxisName: '数值', |
|||
currentLabel: '本期', |
|||
previousLabel: '上期', |
|||
chartType: 'default' |
|||
}) |
|||
|
|||
// 图表容器引用和实例 |
|||
const chartContainer = ref<HTMLDivElement | null>(null) |
|||
let chartInstance: echarts.ECharts | null = null |
|||
|
|||
// 创建图表配置 |
|||
const createBarOption = (currentLabel: string, previousLabel: string, currentValue: number, previousValue: number) => { |
|||
// 根据图表类型决定格式化方式 |
|||
const isPercentage = props.chartType === 'percentage'; |
|||
|
|||
return { |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
axisPointer: { type: 'shadow' }, |
|||
formatter: (params: any) => { |
|||
const value = params[0].value; |
|||
const formattedValue = isPercentage ? `${(value * 100).toFixed(1)}%` : value.toFixed(2); |
|||
return `${params[0].name}: ${formattedValue}`; |
|||
} |
|||
}, |
|||
grid: { |
|||
left: '15%', |
|||
right: '10%', |
|||
bottom: '20%', |
|||
top: '20%' |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
data: [previousLabel, currentLabel], |
|||
axisTick: { show: false }, |
|||
axisLine: { show: false }, |
|||
axisLabel: { |
|||
color: 'rgba(255, 255, 255, 0.7)', |
|||
fontSize: 22 |
|||
} |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { |
|||
formatter: isPercentage ? (value: number) => `${(value * 100).toFixed(0)}%` : '{value}', |
|||
color: 'rgba(255, 255, 255, 0.7)' |
|||
}, |
|||
splitLine: { |
|||
lineStyle: { |
|||
color: 'rgba(255, 255, 255, 0.1)' |
|||
} |
|||
}, |
|||
axisLine: { |
|||
lineStyle: { |
|||
color: 'rgba(255, 255, 255, 0.3)' |
|||
} |
|||
}, |
|||
max: isPercentage ? 1 : undefined |
|||
}, |
|||
series: [{ |
|||
name: isPercentage ? '百分比' : '用量', |
|||
type: 'bar', |
|||
barWidth: '60%', |
|||
data: [ |
|||
{ value: previousValue, itemStyle: { color: props.previousColor } }, |
|||
{ value: currentValue, itemStyle: { color: props.currentColor } } |
|||
], |
|||
label: { |
|||
show: true, |
|||
position: 'top', |
|||
fontWeight: 'bold', |
|||
color: '#fff', |
|||
formatter: (params: any) => { |
|||
if (params.value <= 0) return ''; |
|||
return isPercentage ? `${(params.value * 100).toFixed(1)}%` : params.value.toFixed(2); |
|||
} |
|||
} |
|||
}], |
|||
// 添加标题显示增长率 |
|||
title: props.chartData[0]?.growthRate !== undefined ? { |
|||
text: ` |
|||
数差: ${(currentValue - previousValue).toFixed(2)}${props.chartType === 'percentage' ? '%' : ''} |
|||
${props.chartData[0].growthRate >= 0 ? '↑' : '↓'} ${(props.chartData[0].growthRate * 100).toFixed(1)}%`, |
|||
left: 'center', |
|||
top: '0%', |
|||
textStyle: { |
|||
fontSize: 36, |
|||
fontWeight: 'bold', |
|||
color: props.chartData[0].growthRate >= 0 ? '#52c41a' : '#f5222d', |
|||
fontFamily: 'Arial, sans-serif' |
|||
} |
|||
} : undefined |
|||
}; |
|||
} |
|||
|
|||
// 初始化图表 |
|||
const initChart = () => { |
|||
if (chartContainer.value) { |
|||
chartInstance = echarts.init(chartContainer.value) |
|||
updateChart() |
|||
} |
|||
} |
|||
|
|||
// 更新图表 |
|||
const updateChart = () => { |
|||
if (!chartInstance || !props.chartData.length) return |
|||
|
|||
const dataItem = props.chartData[0] |
|||
|
|||
// 优先使用period字段作为标签,如果没有则使用默认标签 |
|||
const currentLabel = dataItem.currentPeriod || props.currentLabel |
|||
const previousLabel = dataItem.previousPeriod || props.previousLabel |
|||
|
|||
const option = createBarOption( |
|||
currentLabel, |
|||
previousLabel, |
|||
dataItem.currentValue, |
|||
dataItem.previousValue |
|||
) |
|||
|
|||
chartInstance.setOption(option) |
|||
} |
|||
|
|||
// 监听数据变化 |
|||
watch( |
|||
() => props.chartData, |
|||
() => { |
|||
updateChart() |
|||
}, |
|||
{ deep: true } |
|||
) |
|||
|
|||
// 窗口大小改变时重绘图表 |
|||
const handleResize = () => { |
|||
if (chartInstance) { |
|||
chartInstance.resize() |
|||
} |
|||
} |
|||
|
|||
// 组件挂载时初始化图表 |
|||
onMounted(() => { |
|||
initChart() |
|||
|
|||
window.addEventListener('resize', handleResize) |
|||
}) |
|||
|
|||
// 组件销毁前清理资源 |
|||
onBeforeUnmount(() => { |
|||
window.removeEventListener('resize', handleResize) |
|||
if (chartInstance) { |
|||
chartInstance.dispose() |
|||
chartInstance = null |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.comparison-bar-chart-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue