|
@@ -0,0 +1,1075 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
+import VChart from 'vue-echarts';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import { BarChart, GaugeChart, LineChart, PieChart } from 'echarts/charts';
|
|
|
+import { GridComponent, LegendComponent, TitleComponent, TooltipComponent } from 'echarts/components';
|
|
|
+import { use } from 'echarts/core';
|
|
|
+import { CanvasRenderer } from 'echarts/renderers';
|
|
|
+
|
|
|
+import TimeRangeSelect from '@/components/TimeRangeSelect.vue';
|
|
|
+import { useRequest } from '@/hooks/request';
|
|
|
+import { useViewVisible } from '@/hooks/view-visible';
|
|
|
+import { t } from '@/i18n';
|
|
|
+import { downloadCoolingHisData, getCoolingDataStatistics, getCoolingHistoryData, getCoolingRealTimeData } from '@/api';
|
|
|
+import { downloadBlob, formatTimeByScale, timeSorter } from '@/utils';
|
|
|
+import { CoolingDataType, TimeScaleType } from '@/constants';
|
|
|
+
|
|
|
+import chilledWaterFlowRate from '@/assets/img/chilled-water-flow-rate.png';
|
|
|
+import coolingCapacity from '@/assets/img/cooling-capacity.png';
|
|
|
+import coolingPower from '@/assets/img/cooling-power.png';
|
|
|
+import coolingStationEfficiency from '@/assets/img/cooling-station-efficiency.png';
|
|
|
+
|
|
|
+import { DeviceType } from '../device-work-status/device-card';
|
|
|
+
|
|
|
+import type { Dayjs } from 'dayjs';
|
|
|
+import type { BarSeriesOption, GaugeSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts';
|
|
|
+import type {
|
|
|
+ DatasetComponentOption,
|
|
|
+ GridComponentOption,
|
|
|
+ LegendComponentOption,
|
|
|
+ TitleComponentOption,
|
|
|
+ TooltipComponentOption,
|
|
|
+} from 'echarts/components';
|
|
|
+import type { ComposeOption } from 'echarts/core';
|
|
|
+import type {
|
|
|
+ CoolingEnergyCardItem,
|
|
|
+ CoolingHistoryDataResult,
|
|
|
+ CoolingRealTimeDataResult,
|
|
|
+ CoolingStatisticsResult,
|
|
|
+} from '@/types';
|
|
|
+
|
|
|
+use([
|
|
|
+ CanvasRenderer,
|
|
|
+ BarChart,
|
|
|
+ GaugeChart,
|
|
|
+ LineChart,
|
|
|
+ PieChart,
|
|
|
+ GridComponent,
|
|
|
+ TooltipComponent,
|
|
|
+ LegendComponent,
|
|
|
+ TitleComponent,
|
|
|
+]);
|
|
|
+
|
|
|
+type EChartsOption = ComposeOption<
|
|
|
+ | TitleComponentOption
|
|
|
+ | LegendComponentOption
|
|
|
+ | TooltipComponentOption
|
|
|
+ | DatasetComponentOption
|
|
|
+ | GridComponentOption
|
|
|
+ | BarSeriesOption
|
|
|
+ | GaugeSeriesOption
|
|
|
+ | LineSeriesOption
|
|
|
+ | PieSeriesOption
|
|
|
+>;
|
|
|
+
|
|
|
+const deviceTypes = [DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷却泵, DeviceType.冷冻泵];
|
|
|
+
|
|
|
+const { handleRequest } = useRequest();
|
|
|
+const coolingRTDResult = ref<CoolingRealTimeDataResult>();
|
|
|
+let coolingRTDTimer: number | undefined;
|
|
|
+
|
|
|
+const getCoolingRTDResult = () => {
|
|
|
+ clearTimeout(coolingRTDTimer);
|
|
|
+
|
|
|
+ handleRequest(async () => {
|
|
|
+ const now = dayjs();
|
|
|
+
|
|
|
+ coolingRTDResult.value = await getCoolingRealTimeData({
|
|
|
+ deviceGroupId: 7,
|
|
|
+ startTime: now.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ endTime: now.endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ coolingRTDTimer = window.setTimeout(getCoolingRTDResult, 3600 * 1000);
|
|
|
+};
|
|
|
+
|
|
|
+const { handleRequest: handleCoolingHisDataRequest } = useRequest();
|
|
|
+const { visible, showView } = useViewVisible();
|
|
|
+const coolingHisResult = ref<CoolingHistoryDataResult[]>([]);
|
|
|
+
|
|
|
+watch(visible, () => {
|
|
|
+ if (visible.value) {
|
|
|
+ getCoolingHisData();
|
|
|
+ } else {
|
|
|
+ coolingHisResult.value = [];
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const viewCoolingHisData = (energyCard: CoolingEnergyCardItem) => {
|
|
|
+ selectedEnergyCard.value = energyCard;
|
|
|
+ showView();
|
|
|
+};
|
|
|
+
|
|
|
+const getCoolingHisData = () => {
|
|
|
+ handleCoolingHisDataRequest(async () => {
|
|
|
+ coolingHisResult.value = await getCoolingHistoryData({
|
|
|
+ deviceGroupId: 7,
|
|
|
+ deviceTypes,
|
|
|
+ startTime: '2025-04-01 00:00:00',
|
|
|
+ endTime: '2025-05-01 23:00:00',
|
|
|
+ type: selectedEnergyCard.value?.type as CoolingDataType,
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const coolingHisDataOption = computed<EChartsOption>(() => {
|
|
|
+ const unitText = selectedEnergyCard.value?.unit ?? '-';
|
|
|
+ const legendData = coolingHisResult.value.map((device) => device.deviceTypeName);
|
|
|
+
|
|
|
+ const allTimesSet = new Set<string>();
|
|
|
+
|
|
|
+ coolingHisResult.value.forEach((device) => {
|
|
|
+ device.data.forEach((point) => allTimesSet.add(point.time));
|
|
|
+ });
|
|
|
+
|
|
|
+ const times: string[] = Array.from(allTimesSet).sort(timeSorter);
|
|
|
+
|
|
|
+ const seriesData = coolingHisResult.value.map<LineSeriesOption>((device) => {
|
|
|
+ const valueKey = Object.keys(device.data[0] ?? {}).find((key) => key !== 'time');
|
|
|
+ const timeMap: Record<string, string | number> = {};
|
|
|
+
|
|
|
+ if (valueKey) {
|
|
|
+ device.data.reduce((acc, point) => {
|
|
|
+ acc[point.time] = point[valueKey];
|
|
|
+ return acc;
|
|
|
+ }, timeMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: device.deviceTypeName,
|
|
|
+ type: 'line',
|
|
|
+ data: times.map((time) => timeMap[time] ?? '-'),
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ title: [
|
|
|
+ {
|
|
|
+ text: `${t('common.unit')}: ${unitText}`,
|
|
|
+ textStyle: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 12,
|
|
|
+ fontWeight: 400,
|
|
|
+ lineHeight: 17,
|
|
|
+ },
|
|
|
+ left: 20,
|
|
|
+ top: 0,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ formatter(params) {
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ const tempParms = params as any[];
|
|
|
+ const time = dayjs(tempParms[0].axisValue).format('MM-DD HH:mm');
|
|
|
+
|
|
|
+ let tableHtml = '<table class="echarts-tooltip-electricity">';
|
|
|
+ tableHtml += `<tr><th>${time}</th></tr>`;
|
|
|
+
|
|
|
+ tempParms.forEach((param) => {
|
|
|
+ tableHtml += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}${unitText}</td></tr>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</table>';
|
|
|
+ return tableHtml;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'horizontal',
|
|
|
+ bottom: 0,
|
|
|
+ textStyle: {
|
|
|
+ color: '#000',
|
|
|
+ fontSize: 12,
|
|
|
+ },
|
|
|
+ itemWidth: 22,
|
|
|
+ itemHeight: 8,
|
|
|
+ itemGap: 24,
|
|
|
+ data: legendData,
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: true,
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ formatter(value) {
|
|
|
+ return dayjs(value).format('MM-DD HH:mm');
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ data: times,
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ series: seriesData,
|
|
|
+ grid: {
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 38,
|
|
|
+ top: 25,
|
|
|
+ containLabel: true,
|
|
|
+ },
|
|
|
+ color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getCoolingRTDResult();
|
|
|
+ getEnergyConsumeResult();
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ clearTimeout(coolingRTDTimer);
|
|
|
+});
|
|
|
+
|
|
|
+const energyEfficiencyResult = ref<CoolingStatisticsResult>();
|
|
|
+
|
|
|
+const getEnergyConsumeResult = () => {
|
|
|
+ handleRequest(async () => {
|
|
|
+ energyEfficiencyResult.value = await getCoolingDataStatistics({
|
|
|
+ deviceGroupId: 7,
|
|
|
+ deviceTypes,
|
|
|
+ startTime: '2025-04-16 00:00:00',
|
|
|
+ endTime: '2025-04-16 23:00:00',
|
|
|
+ });
|
|
|
+
|
|
|
+ energyEfficiencyResult.value.hisQueryVos.forEach((item) => {
|
|
|
+ item.groupName = item.deviceTypeName + t('common.group');
|
|
|
+ });
|
|
|
+
|
|
|
+ if (energyEfficiencyResult.value.timeScaleType !== TimeScaleType.Hour) {
|
|
|
+ energyEfficiencyResult.value.hisCoolingDataVos.forEach((item) => {
|
|
|
+ item.time = formatTimeByScale(item.time);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const selectedEnergyCard = ref<CoolingEnergyCardItem>();
|
|
|
+
|
|
|
+const energyCardList = computed<CoolingEnergyCardItem[]>(() => {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ type: CoolingDataType.ColdStationEfficiency,
|
|
|
+ value: coolingRTDResult.value?.coolingStationEnergyEfficiency ?? '-',
|
|
|
+ unit: 'kw/kw',
|
|
|
+ description: t('energyAnalysis.coolingStationEfficiency'),
|
|
|
+ icon: coolingStationEfficiency,
|
|
|
+ bgColor: '#FFF6E6',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: CoolingDataType.CoolingCapacity,
|
|
|
+ value: coolingRTDResult.value?.coolingStationCoolingData ?? '-',
|
|
|
+ unit: 'kw',
|
|
|
+ description: t('energyAnalysis.coolingStationCoolingCapacity'),
|
|
|
+ icon: coolingCapacity,
|
|
|
+ bgColor: '#EFF1FE',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: CoolingDataType.Power,
|
|
|
+ value: coolingRTDResult.value?.coolingStationOutputActivePower ?? '-',
|
|
|
+ unit: 'kw',
|
|
|
+ description: t('energyAnalysis.coolingStationCoolingPower'),
|
|
|
+ icon: coolingPower,
|
|
|
+ bgColor: 'var(--antd-color-primary-opacity-15)',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: CoolingDataType.ChilledWaterFlow,
|
|
|
+ value: coolingRTDResult.value?.coolingStationWaterFlow ?? '-',
|
|
|
+ unit: 'm³/h',
|
|
|
+ description: t('energyAnalysis.coolingStationChilledWaterFlowRate'),
|
|
|
+ icon: chilledWaterFlowRate,
|
|
|
+ bgColor: 'rgba(104, 194, 58, 0.12)',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+});
|
|
|
+
|
|
|
+interface SubEfficiencyItem {
|
|
|
+ label: string;
|
|
|
+ value: string | number;
|
|
|
+ color: string;
|
|
|
+}
|
|
|
+
|
|
|
+const subItemEfficiency = computed<SubEfficiencyItem[]>(() => {
|
|
|
+ const devEfficiencyMap: Partial<Record<DeviceType, string | number>> = {};
|
|
|
+
|
|
|
+ energyEfficiencyResult.value?.deviceTypeCoolingEfficiencyVos.forEach((item) => {
|
|
|
+ devEfficiencyMap[item.deviceType as DeviceType] = item.coolingEfficiencyTotal;
|
|
|
+ });
|
|
|
+
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ label: t('energyAnalysis.chilledWaterChillerGroup'),
|
|
|
+ value: devEfficiencyMap[DeviceType.冷水主机] ?? '-',
|
|
|
+ color: '#32BAC0',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: t('energyAnalysis.refrigerationPumpGroup'),
|
|
|
+ value: devEfficiencyMap[DeviceType.冷冻泵] ?? '-',
|
|
|
+ color: '#5B8FF9',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: t('energyAnalysis.coolingPumpGroup'),
|
|
|
+ value: devEfficiencyMap[DeviceType.冷却泵] ?? '-',
|
|
|
+ color: '#5D7092',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: t('energyAnalysis.coolingTowerGroup'),
|
|
|
+ value: devEfficiencyMap[DeviceType.冷却塔] ?? '-',
|
|
|
+ color: '#F6BD16',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+});
|
|
|
+
|
|
|
+const gaugeOption = computed<EChartsOption>(() => {
|
|
|
+ const value = energyEfficiencyResult.value?.groupCoolingEfficiency ?? 0;
|
|
|
+ const progressColor = '#32BAC0';
|
|
|
+
|
|
|
+ return {
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'gauge',
|
|
|
+ startAngle: 210,
|
|
|
+ endAngle: -30,
|
|
|
+ radius: '80%',
|
|
|
+ min: 0,
|
|
|
+ max: 10,
|
|
|
+ itemStyle: {
|
|
|
+ color: progressColor,
|
|
|
+ },
|
|
|
+ progress: {
|
|
|
+ show: true,
|
|
|
+ width: 15,
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ width: 15,
|
|
|
+ color: [[1, '#E4E7ED']],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ pointer: {
|
|
|
+ icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
|
|
|
+ length: '8%',
|
|
|
+ width: 12,
|
|
|
+ offsetCenter: [0, '-63%'],
|
|
|
+ itemStyle: {
|
|
|
+ color: progressColor,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ length: 8,
|
|
|
+ lineStyle: {
|
|
|
+ color: 'auto',
|
|
|
+ width: 2,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ length: 8,
|
|
|
+ lineStyle: {
|
|
|
+ color: 'auto',
|
|
|
+ width: 2,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ offsetCenter: [0, '5%'],
|
|
|
+ fontSize: 14,
|
|
|
+ lineHeight: 22,
|
|
|
+ },
|
|
|
+ detail: {
|
|
|
+ color: '#000',
|
|
|
+ fontSize: 30,
|
|
|
+ lineHeight: 36,
|
|
|
+ offsetCenter: [0, '-20%'],
|
|
|
+ valueAnimation: true,
|
|
|
+ formatter(value: number) {
|
|
|
+ return value.toFixed(2);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ value,
|
|
|
+ name: 'kw/kw',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'gauge',
|
|
|
+ startAngle: 210,
|
|
|
+ endAngle: -30,
|
|
|
+ radius: '47%',
|
|
|
+ min: 0,
|
|
|
+ max: 10,
|
|
|
+ itemStyle: {
|
|
|
+ color: progressColor,
|
|
|
+ },
|
|
|
+ progress: {
|
|
|
+ show: true,
|
|
|
+ width: 3,
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ width: 3,
|
|
|
+ color: [[1, '#E4E7ED']],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ pointer: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ detail: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ value,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const energyEfficiencyLineOption = computed<EChartsOption>(() => {
|
|
|
+ const timeScaleType = energyEfficiencyResult.value?.timeScaleType;
|
|
|
+ const hisQueryVos = energyEfficiencyResult.value?.hisQueryVos || [];
|
|
|
+ const coolingStationDataVos = energyEfficiencyResult.value?.coolingStationDataVos || [];
|
|
|
+ const legendData = [t('energyAnalysis.coolingStation'), ...hisQueryVos.map((item) => item.groupName as string)];
|
|
|
+
|
|
|
+ const allTimesSet = new Set<string>();
|
|
|
+
|
|
|
+ hisQueryVos.forEach((item) => {
|
|
|
+ item.valueList.forEach((value) => {
|
|
|
+ allTimesSet.add(value.time);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ coolingStationDataVos.forEach((item) => {
|
|
|
+ allTimesSet.add(item.time);
|
|
|
+ });
|
|
|
+
|
|
|
+ const times: string[] = Array.from(allTimesSet).sort(timeSorter);
|
|
|
+
|
|
|
+ return {
|
|
|
+ title: {
|
|
|
+ text: `${t('energyAnalysis.energyEfficiency')}(kw/kw)`,
|
|
|
+ textStyle: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ fontWeight: 400,
|
|
|
+ lineHeight: 14,
|
|
|
+ },
|
|
|
+ left: '4%',
|
|
|
+ top: '9%',
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ formatter(params) {
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ const tempParms = params as any[];
|
|
|
+ const time = formatTimeByScale(tempParms[0].axisValue, timeScaleType);
|
|
|
+
|
|
|
+ let tableHtml = '<table class="echarts-tooltip-electricity">';
|
|
|
+ tableHtml += `<tr><th>${time}</th></tr>`;
|
|
|
+
|
|
|
+ const isEfficientRoomExisted = tempParms.find(
|
|
|
+ (param) => param.seriesName === t('energyAnalysis.efficientRoomEnergyStandard'),
|
|
|
+ );
|
|
|
+
|
|
|
+ if (isEfficientRoomExisted) {
|
|
|
+ tableHtml += `<tr><td>${t('energyAnalysis.efficientRoomEnergyStandard')}</td><td>5.00kw/kw</td></tr>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ tempParms.forEach((param) => {
|
|
|
+ if (param.seriesName === t('energyAnalysis.efficientRoomEnergyStandard')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ tableHtml += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}kw/kw</td></tr>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</table>';
|
|
|
+ return tableHtml;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'horizontal',
|
|
|
+ bottom: 0,
|
|
|
+ textStyle: {
|
|
|
+ color: '#000',
|
|
|
+ fontSize: 12,
|
|
|
+ },
|
|
|
+ itemWidth: 22,
|
|
|
+ itemHeight: 8,
|
|
|
+ itemGap: 24,
|
|
|
+ data: [
|
|
|
+ ...legendData,
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.efficientRoomEnergyStandard'),
|
|
|
+ icon: 'path://M128 501.333v64H0v-64zm213.333 0v64h-128v-64zm213.334 0v64h-128v-64zm234.666 0v64h-128v-64z',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: true,
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ formatter(value) {
|
|
|
+ return formatTimeByScale(value, timeScaleType);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ data: times,
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.coolingStation'),
|
|
|
+ type: 'line',
|
|
|
+ data: times.map((time) => {
|
|
|
+ const value = coolingStationDataVos.find((item) => item.time === time);
|
|
|
+ return value?.coolingEfficiencyTotal ?? '-';
|
|
|
+ }),
|
|
|
+ symbolSize: 6,
|
|
|
+ },
|
|
|
+ ...hisQueryVos.map<LineSeriesOption>((item) => ({
|
|
|
+ name: item.groupName as string,
|
|
|
+ type: 'line',
|
|
|
+ data: times.map((time) => {
|
|
|
+ const value = item.valueList.find((value) => value.time === time);
|
|
|
+ return value?.coolingEfficiency ?? '-';
|
|
|
+ }),
|
|
|
+ symbolSize: 6,
|
|
|
+ })),
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.efficientRoomEnergyStandard'),
|
|
|
+ type: 'line',
|
|
|
+ data: new Array(times.length).fill(5),
|
|
|
+ symbolSize: 6,
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ width: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ grid: {
|
|
|
+ left: '4%',
|
|
|
+ right: 0,
|
|
|
+ bottom: 46,
|
|
|
+ top: '17.5%',
|
|
|
+ containLabel: true,
|
|
|
+ },
|
|
|
+ color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107', '#FF6384'],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const pieOption = computed<EChartsOption>(() => {
|
|
|
+ const legendData = energyEfficiencyResult.value?.hisQueryVos.map((item) => item.groupName as string);
|
|
|
+ const seriesData = energyEfficiencyResult.value?.hisQueryVos.map((item) => ({
|
|
|
+ name: item.groupName as string,
|
|
|
+ value: item.billTotal,
|
|
|
+ }));
|
|
|
+ let centerValue = 0;
|
|
|
+
|
|
|
+ seriesData?.forEach((item) => {
|
|
|
+ centerValue += item.value;
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ formatter(params: any) {
|
|
|
+ return `${params.marker}${params.name}: ${params.value}${t('energyAnalysis.coolingCapacityUnit')}`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'horizontal',
|
|
|
+ bottom: 0,
|
|
|
+ textStyle: {
|
|
|
+ color: '#000',
|
|
|
+ fontSize: 12,
|
|
|
+ },
|
|
|
+ itemWidth: 8,
|
|
|
+ itemHeight: 8,
|
|
|
+ itemGap: 24,
|
|
|
+ data: legendData,
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['40%', '65%'],
|
|
|
+ avoidLabelOverlap: true,
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'outside',
|
|
|
+ formatter: '{d}%',
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: true,
|
|
|
+ },
|
|
|
+ data: seriesData,
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['0', '30%'],
|
|
|
+ itemStyle: {
|
|
|
+ color: 'transparent',
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ position: 'center',
|
|
|
+ formatter: `{value|${centerValue}}\n{title|${t('energyAnalysis.coolingCapacityUnit')}}`,
|
|
|
+ rich: {
|
|
|
+ value: {
|
|
|
+ fontSize: 30,
|
|
|
+ fontWeight: 'bold',
|
|
|
+ color: '#000',
|
|
|
+ lineHeight: 36,
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#666',
|
|
|
+ lineHeight: 22,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: [{ value: 1, name: '' }],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const lineOption = computed<EChartsOption>(() => {
|
|
|
+ const timeScaleType = energyEfficiencyResult.value?.timeScaleType;
|
|
|
+ const hisQueryVos = energyEfficiencyResult.value?.hisCoolingDataVos || [];
|
|
|
+ const tempDataList = energyEfficiencyResult.value?.tempDataList || [];
|
|
|
+ const coolingData: Array<number | string> = [];
|
|
|
+ const coolingBill: Array<number | string> = [];
|
|
|
+ const outdoorTempData: Array<number | string> = [];
|
|
|
+
|
|
|
+ const allTimesSet = new Set<string>();
|
|
|
+ hisQueryVos.forEach((item) => allTimesSet.add(item.time));
|
|
|
+ tempDataList.forEach((item) => allTimesSet.add(item.time));
|
|
|
+
|
|
|
+ const times: string[] = Array.from(allTimesSet).sort(timeSorter);
|
|
|
+
|
|
|
+ times.forEach((time) => {
|
|
|
+ const coolingItem = hisQueryVos.find((item) => item.time === time);
|
|
|
+ const tempItem = tempDataList.find((item) => item.time === time);
|
|
|
+ coolingData.push(coolingItem?.valueList[0]?.coolingData ?? '-');
|
|
|
+ coolingBill.push(coolingItem?.valueList[0]?.bill ?? '-');
|
|
|
+ outdoorTempData.push(tempItem?.temperature ?? '-');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算 Y 轴的分割段数
|
|
|
+ const yAxisSegments = 6;
|
|
|
+ const validCoolingData = coolingData.filter((value) => typeof value === 'number');
|
|
|
+ const validCoolingBill = coolingBill.filter((value) => typeof value === 'number');
|
|
|
+ const coolingDataMax =
|
|
|
+ validCoolingData.length > 0 ? Math.ceil(Math.max(...validCoolingData) / yAxisSegments) * yAxisSegments : 0;
|
|
|
+ const coolingBillMax =
|
|
|
+ validCoolingBill.length > 0 ? Math.ceil(Math.max(...validCoolingBill) / yAxisSegments) * yAxisSegments : 0;
|
|
|
+
|
|
|
+ return {
|
|
|
+ title: [
|
|
|
+ {
|
|
|
+ text: `${t('energyAnalysis.coolingCapacity')}(GJ)`,
|
|
|
+ textStyle: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ fontWeight: 400,
|
|
|
+ lineHeight: 14,
|
|
|
+ },
|
|
|
+ left: '5%',
|
|
|
+ top: '9%',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ text: `${t('energyAnalysis.coolingPricePerUnit')}(${t('energyAnalysis.coolingCapacityUnit')})`,
|
|
|
+ textStyle: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ fontWeight: 400,
|
|
|
+ lineHeight: 14,
|
|
|
+ },
|
|
|
+ right: '1%',
|
|
|
+ top: '9%',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ formatter(params) {
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ const tempParms = params as any[];
|
|
|
+ let tooltipStr = '<table class="echarts-tooltip-electricity">';
|
|
|
+ let currentTime = formatTimeByScale(tempParms[0].name, timeScaleType);
|
|
|
+
|
|
|
+ if (timeScaleType === TimeScaleType.Hour) {
|
|
|
+ currentTime += ` - ${currentTime.split(':')[0]}:59`; // 若时间尺度为小时,则显示为 01:00 - 01:59
|
|
|
+ }
|
|
|
+
|
|
|
+ tooltipStr += `<tr><th>${currentTime}</th></tr>`;
|
|
|
+
|
|
|
+ tempParms.forEach((param) => {
|
|
|
+ const { seriesName } = param;
|
|
|
+ let unit = '';
|
|
|
+
|
|
|
+ if (seriesName === t('energyAnalysis.coolingCapacity')) {
|
|
|
+ unit = 'GJ';
|
|
|
+ } else if (seriesName === t('energyAnalysis.coolingPricePerUnit')) {
|
|
|
+ unit = t('energyAnalysis.coolingCapacityUnit');
|
|
|
+ } else if (seriesName === t('envMonitor.outdoorTemperature')) {
|
|
|
+ unit = '℃';
|
|
|
+ }
|
|
|
+
|
|
|
+ tooltipStr += `<tr><td>${param.marker}${param.seriesName}</td><td>${param.value}${unit}</td></tr>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ tooltipStr += '</table>';
|
|
|
+ return tooltipStr;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'horizontal',
|
|
|
+ bottom: 0,
|
|
|
+ textStyle: {
|
|
|
+ color: '#000',
|
|
|
+ fontSize: 12,
|
|
|
+ },
|
|
|
+ itemWidth: 22,
|
|
|
+ itemHeight: 8,
|
|
|
+ itemGap: 24,
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.coolingCapacity'),
|
|
|
+ icon: 'path://M0 0h8v8h-8z',
|
|
|
+ },
|
|
|
+ t('energyAnalysis.coolingPricePerUnit'),
|
|
|
+ t('envMonitor.outdoorTemperature'),
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: true,
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ formatter(value) {
|
|
|
+ return formatTimeByScale(value, timeScaleType);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ data: times,
|
|
|
+ },
|
|
|
+ yAxis: [
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ position: 'left',
|
|
|
+ min: 0,
|
|
|
+ max: coolingDataMax,
|
|
|
+ interval: coolingDataMax / yAxisSegments,
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ position: 'right',
|
|
|
+ min: 0,
|
|
|
+ max: coolingBillMax,
|
|
|
+ interval: coolingBillMax / yAxisSegments,
|
|
|
+ axisLabel: {
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#EBEBEB',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.coolingCapacity'),
|
|
|
+ type: 'bar',
|
|
|
+ data: coolingData,
|
|
|
+ barWidth: 24,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: t('energyAnalysis.coolingPricePerUnit'),
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 1,
|
|
|
+ data: coolingBill,
|
|
|
+ symbolSize: 6,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: t('envMonitor.outdoorTemperature'),
|
|
|
+ type: 'line',
|
|
|
+ data: outdoorTempData,
|
|
|
+ symbolSize: 6,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ grid: {
|
|
|
+ left: '4%',
|
|
|
+ right: 0,
|
|
|
+ bottom: 46,
|
|
|
+ top: '17.5%',
|
|
|
+ containLabel: true,
|
|
|
+ },
|
|
|
+ color: ['#4ECDC4', '#5470C6', '#36A2EB', '#FFC107'],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const exportData = async () => {
|
|
|
+ const fileName = t('energyAnalysis.energyEfficiencyAnalysis') + ' - ' + dayjs().format('YYYYMMDDHHmmss');
|
|
|
+ const file = await downloadCoolingHisData({
|
|
|
+ deviceGroupId: 7,
|
|
|
+ deviceTypes,
|
|
|
+ startTime: '2025-04-01 00:00:00',
|
|
|
+ endTime: '2025-05-01 23:00:00',
|
|
|
+ });
|
|
|
+
|
|
|
+ downloadBlob(file, fileName);
|
|
|
+};
|
|
|
+
|
|
|
+const handleTimeChange = (range: [Dayjs, Dayjs]) => {
|
|
|
+ console.log(range[0].format('YYYY-MM-DD HH:mm:ss'), range[1].format('YYYY-MM-DD HH:mm:ss'));
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <ACard :title="$t('energyAnalysis.realTimeValue')" :bordered="false">
|
|
|
+ <ARow class="energy-card-list" :gutter="[24, 24]">
|
|
|
+ <ACol v-for="(item, index) in energyCardList" :key="index" :xs="24" :sm="24" :md="12" :lg="12" :xl="6" :xxl="6">
|
|
|
+ <div
|
|
|
+ class="energy-card-container"
|
|
|
+ :style="{
|
|
|
+ backgroundColor: item.bgColor,
|
|
|
+ }"
|
|
|
+ @click="viewCoolingHisData(item)"
|
|
|
+ >
|
|
|
+ <img class="energy-card-icon" :src="item.icon" />
|
|
|
+ <div>
|
|
|
+ <div class="energy-card-title">
|
|
|
+ <span class="energy-card-value">{{ item.value }}</span>
|
|
|
+ <span class="energy-card-unit">{{ item.unit }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="energy-card-description">{{ item.description }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </ACol>
|
|
|
+ </ARow>
|
|
|
+ </ACard>
|
|
|
+ <ACard :title="$t('energyAnalysis.coolingStationEfficiency')" :bordered="false">
|
|
|
+ <template #extra>
|
|
|
+ <TimeRangeSelect @change="handleTimeChange" />
|
|
|
+ <AButton class="export-button" @click="exportData">{{ $t('common.export') }}</AButton>
|
|
|
+ </template>
|
|
|
+ <div class="chart-container">
|
|
|
+ <div class="chart-wrapper chart-pie">
|
|
|
+ <div class="chart-pie-title">{{ $t('energyAnalysis.subItemEnergyEfficiency') }}</div>
|
|
|
+ <VChart class="chart" :option="gaugeOption" autoresize />
|
|
|
+ <!-- <div class="chart-pie-level">
|
|
|
+ <span>中等</span>
|
|
|
+ </div> -->
|
|
|
+ <ARow class="sub-efficiency-container" :gutter="[24, 8]">
|
|
|
+ <ACol v-for="(item, index) in subItemEfficiency" :key="index" :span="12">
|
|
|
+ <div class="sub-efficiency-item">
|
|
|
+ <div :style="{ backgroundColor: item.color }"></div>
|
|
|
+ <span>{{ item.label }}: {{ item.value }}</span>
|
|
|
+ </div>
|
|
|
+ </ACol>
|
|
|
+ </ARow>
|
|
|
+ </div>
|
|
|
+ <div class="chart-wrapper chart-line">
|
|
|
+ <VChart class="chart" :option="energyEfficiencyLineOption" autoresize />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </ACard>
|
|
|
+ <ACard :title="$t('energyAnalysis.coolingPricePerUnit')" :bordered="false">
|
|
|
+ <div class="chart-container">
|
|
|
+ <div class="chart-wrapper chart-pie">
|
|
|
+ <div class="chart-pie-title">
|
|
|
+ {{ $t('energyAnalysis.cumulativeCoolingCapacity') }}(GJ):
|
|
|
+ {{ energyEfficiencyResult?.totalCoolingData ?? '-' }}
|
|
|
+ </div>
|
|
|
+ <VChart class="chart" :option="pieOption" autoresize />
|
|
|
+ </div>
|
|
|
+ <div class="chart-wrapper chart-line">
|
|
|
+ <VChart class="chart" :option="lineOption" autoresize />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </ACard>
|
|
|
+ <AModal v-model:open="visible" :title="$t('deviceWorkStatus.viewData')" :width="920" :footer="null">
|
|
|
+ <TimeRangeSelect class="cooling-history-select" is-last />
|
|
|
+ <VChart class="cooling-history-chart" :option="coolingHisDataOption" autoresize />
|
|
|
+ </AModal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.energy-card-container {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-efficiency-container {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ padding-inline: 59px;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-efficiency-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ div {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 20px;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-wrapper {
|
|
|
+ height: 318px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-pie {
|
|
|
+ position: relative;
|
|
|
+ width: 380px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-pie-title {
|
|
|
+ position: absolute;
|
|
|
+ padding: 4px 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ text-align: center;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-pie-level {
|
|
|
+ position: absolute;
|
|
|
+ top: 197px;
|
|
|
+ right: 0;
|
|
|
+ left: 0;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: inline-block;
|
|
|
+ height: 24px;
|
|
|
+ padding-inline: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 24px;
|
|
|
+ color: #e6a23c;
|
|
|
+ text-align: center;
|
|
|
+ background: rgb(230 162 60 / 12%);
|
|
|
+ border: 1px solid #e6a23c;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chart-line {
|
|
|
+ width: calc(100% - 380px);
|
|
|
+}
|
|
|
+
|
|
|
+.chart {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.cooling-history-select {
|
|
|
+ margin-block: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.cooling-history-chart {
|
|
|
+ height: 401px;
|
|
|
+}
|
|
|
+</style>
|