Browse Source

feat(views): 设备工况模块支持点击参数查看其历史数据

wangcong 2 months ago
parent
commit
67c080fdde

+ 10 - 0
src/constants/index.ts

@@ -94,3 +94,13 @@ export const enum DeviceRunningStatus {
    */
   Run,
 }
+
+/**
+ * 时间范围预设
+ */
+export const enum TimeRangePreset {
+  custom,
+  recent1Day,
+  recent7Day,
+  recent30Day,
+}

+ 6 - 0
src/i18n/locales/zh.json

@@ -10,6 +10,7 @@
     "cancel": "取消",
     "cannotEmpty": "不能为空",
     "confirm": "确认",
+    "custom": "自定义",
     "delete": "删除",
     "deleteConfirmation": "删除确定",
     "emptyData": "请求数据为空",
@@ -33,12 +34,16 @@
     "plzSelect": "请选择{name}",
     "query": "查询",
     "reLogin": "你已被登出,请重新登录",
+    "recent1Day": "最近 1 天",
+    "recent30Day": "最近 30 天",
+    "recent7Day": "最近 7 天",
     "requestTimedOut": "请求超时",
     "reset": "重置",
     "return": "返回",
     "revise": "修改",
     "save": "保存",
     "search": "搜索",
+    "selectTime": "选择时间",
     "selected": "已选",
     "sequenceNumber": "序列号",
     "serialNumber": "序号",
@@ -48,6 +53,7 @@
     "success": "成功",
     "templateImport": "模板导入",
     "tip": "提示",
+    "to": "至",
     "turnOff": "关闭",
     "unimatIoT": "亿维物联",
     "updateTime": "更新时间",

+ 3 - 0
src/types/index.ts

@@ -5,6 +5,7 @@ import type { Component, ComponentPublicInstance } from 'vue';
 import type { StepProps, UploadProps } from 'ant-design-vue';
 import type { Rule, RuleObject } from 'ant-design-vue/es/form';
 import type { FormLabelAlign } from 'ant-design-vue/es/form/interface';
+import type { Dayjs } from 'dayjs';
 import type { DeviceRunningStatus, ProtocolConfigMethod } from '@/constants';
 
 export interface ApiResponse<T> {
@@ -51,6 +52,8 @@ export type FormRules<T> = {
 
 export type RuleValidator<T> = (rule: RuleObject, value: T, callback: (error?: string) => void) => Promise<void>;
 
+export type RangeValue = [Dayjs, Dayjs];
+
 export interface DictTypeDataParams {
   id?: number;
   dictCode?: DictCode;

+ 287 - 0
src/views/device-work-status/DevWorkParamData.vue

@@ -0,0 +1,287 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import VChart from 'vue-echarts';
+import dayjs from 'dayjs';
+import { LineChart } from 'echarts/charts';
+import {
+  AxisPointerComponent,
+  DatasetComponent,
+  GridComponent,
+  LegendComponent,
+  MarkLineComponent,
+  TooltipComponent,
+} from 'echarts/components';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+
+import SvgIcon from '@/components/SvgIcon.vue';
+import { useRequest } from '@/hooks/request';
+import { useViewVisible } from '@/hooks/view-visible';
+import { t } from '@/i18n';
+import { getDevWorkHistoryData } from '@/api';
+import { TimeRangePreset } from '@/constants';
+
+import type { Dayjs } from 'dayjs';
+import type { LineSeriesOption } from 'echarts/charts';
+import type {
+  DatasetComponentOption,
+  GridComponentOption,
+  LegendComponentOption,
+  TooltipComponentOption,
+} from 'echarts/components';
+import type { ComposeOption } from 'echarts/core';
+import type { OptionItem, RangeValue } from '@/types';
+
+use([
+  CanvasRenderer,
+  AxisPointerComponent,
+  MarkLineComponent,
+  LegendComponent,
+  TooltipComponent,
+  DatasetComponent,
+  GridComponent,
+  LineChart,
+]);
+
+type EChartsOption = ComposeOption<
+  LegendComponentOption | TooltipComponentOption | DatasetComponentOption | GridComponentOption | LineSeriesOption
+>;
+
+interface Props {
+  devId: number;
+  paramCodes: string[];
+}
+
+const props = defineProps<Props>();
+
+const { visible, showView, hideView } = useViewVisible();
+const { isLoading, handleRequest } = useRequest();
+const timeRange = ref<RangeValue>([dayjs().add(-1, 'd'), dayjs()]);
+const currTimeRangePreset = ref<TimeRangePreset>(TimeRangePreset.recent1Day);
+
+const timeRangePresets = computed<OptionItem<TimeRangePreset>[]>(() => {
+  return [
+    { value: TimeRangePreset.recent1Day, label: t('common.recent1Day') },
+    { value: TimeRangePreset.recent7Day, label: t('common.recent7Day') },
+    { value: TimeRangePreset.recent30Day, label: t('common.recent30Day') },
+    { value: TimeRangePreset.custom, label: t('common.custom') },
+  ];
+});
+
+const disabledDate = (current: Dayjs) => {
+  return current && current > dayjs().endOf('day');
+};
+
+const setTimeRange = () => {
+  switch (currTimeRangePreset.value) {
+    case TimeRangePreset.recent1Day:
+      timeRange.value = [dayjs().add(-1, 'd'), dayjs()];
+      break;
+    case TimeRangePreset.recent7Day:
+      timeRange.value = [dayjs().add(-7, 'd'), dayjs()];
+      break;
+    case TimeRangePreset.recent30Day:
+      timeRange.value = [dayjs().add(-30, 'd'), dayjs()];
+      break;
+  }
+};
+
+const handleRangePresetChange = () => {
+  setTimeRange();
+
+  if (currTimeRangePreset.value !== TimeRangePreset.custom) {
+    getParamHistoryData();
+  }
+};
+
+const handleTimeRangeChange = () => {
+  getParamHistoryData();
+};
+
+watch(visible, () => {
+  if (visible.value) {
+    getParamHistoryData();
+  }
+});
+
+const historyData = ref<Record<string, string | number>[]>([]);
+
+const option = computed<EChartsOption>(() => {
+  return {
+    color: ['#32BAC0'],
+    legend: {
+      show: false,
+    },
+    tooltip: {
+      show: true,
+      trigger: 'axis',
+    },
+    dataset: {
+      dimensions: ['time', ...props.paramCodes],
+      source: historyData.value,
+    },
+    xAxis: {
+      type: 'category',
+      axisLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+      axisLabel: {
+        color: '#999',
+        formatter(value) {
+          return dayjs(value).format('MM-DD HH:mm');
+        },
+      },
+      axisTick: {
+        show: false,
+      },
+    },
+    yAxis: {
+      axisLabel: {
+        color: '#999',
+      },
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: '#EBEBEB',
+        },
+      },
+    },
+    series: props.paramCodes.map((item) => ({
+      type: 'line',
+      name: item,
+    })),
+    grid: {
+      top: 10,
+      right: 0,
+      bottom: 20,
+      left: 30,
+    },
+  };
+});
+
+const getParamHistoryData = () => {
+  handleRequest(async () => {
+    const [startTime, endTime] = timeRange.value;
+    const data = await getDevWorkHistoryData({
+      deviceIds: [props.devId],
+      deviceParamCode: props.paramCodes,
+      startTime: startTime.format('YYYY-MM-DD HH:mm:ss'),
+      endTime: endTime.format('YYYY-MM-DD HH:mm:ss'),
+    });
+
+    if (data.length) {
+      historyData.value = data[0].deviceParamMapList;
+    } else {
+      historyData.value = [];
+    }
+  });
+};
+
+const handleClose = () => {
+  historyData.value = [];
+  currTimeRangePreset.value = TimeRangePreset.recent1Day;
+  setTimeRange();
+};
+
+defineExpose({
+  showView,
+  hideView,
+});
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    wrap-class-name="dev-work-param-data-modal"
+    :title="$t('deviceWorkStatus.viewData')"
+    :width="920"
+    centered
+    :footer="null"
+    :after-close="handleClose"
+  >
+    <ASpin class="center-loading" :spinning="isLoading" />
+    <div class="param-data-select">
+      <span class="param-data-label">{{ $t('common.selectTime') }}</span>
+      <ASelect v-model:value="currTimeRangePreset" class="param-preset-picker" @change="handleRangePresetChange">
+        <ASelectOption v-for="item in timeRangePresets" :key="item.value" :value="item.value">
+          {{ item.label }}
+        </ASelectOption>
+      </ASelect>
+      <ARangePicker
+        class="param-date-picker"
+        v-model:value="timeRange"
+        :allow-clear="false"
+        :separator="$t('common.to')"
+        :disabled-date="disabledDate"
+        @change="handleTimeRangeChange"
+      >
+        <template #suffixIcon>
+          <SvgIcon name="calendar" color="#333" />
+        </template>
+      </ARangePicker>
+    </div>
+    <span class="param-name-unit">{{ '制冷量(kw)' }}</span>
+    <VChart class="param-data-chart" :option="option" />
+  </AModal>
+</template>
+
+<style lang="scss">
+.dev-work-param-data-modal {
+  .ant-modal,
+  .ant-modal > div,
+  .ant-modal-content {
+    height: 498px;
+  }
+
+  .ant-modal-content {
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .ant-modal-body {
+    display: flex;
+    flex-direction: column;
+    height: calc(100% - 32px);
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+.param-data-select {
+  margin-bottom: 16px;
+}
+
+.param-data-label {
+  font-size: 14px;
+  line-height: 24px;
+  color: #333;
+}
+
+.param-preset-picker {
+  width: 110px;
+  margin-inline: 12px;
+}
+
+.param-date-picker {
+  width: 256px;
+
+  :deep(.ant-picker-range-separator) {
+    padding-right: 16px;
+  }
+}
+
+.param-name-unit {
+  margin-left: 24px;
+  font-size: 12px;
+  line-height: 17px;
+  color: #999;
+}
+
+.param-data-chart {
+  height: 100%;
+}
+</style>

+ 28 - 2
src/views/device-work-status/DeviceWorkStatus.vue

@@ -9,6 +9,7 @@ import { DevParamCtrlCabinet } from '@/constants/device-params';
 
 import { deviceCardData, DeviceType } from './device-card';
 import DeviceWorkParams from './DeviceWorkParams.vue';
+import DevWorkParamData from './DevWorkParamData.vue';
 
 import type { DevicesListItem, DeviceTypeCount, PageParams } from '@/types';
 
@@ -116,6 +117,28 @@ const viewDevParam = (id: number) => {
   currentDevId.value = id;
   deviceWorkParamsRef.value?.showView();
 };
+
+const devWorkParamDataRef = useTemplateRef('devWorkParamData');
+
+const currentDevParamCodes = ref<string[]>([]);
+
+const viewHistoryData = (paramCode: string) => {
+  currentDevParamCodes.value = [paramCode];
+  devWorkParamDataRef.value?.showView();
+};
+
+/**
+ * 使用事件委托处理设置了参数编码属性 data-param-code 的元素的点击事件,用来查看该参数的历史数据
+ */
+const handleDevCardClick = (devId: number, e: Event) => {
+  const target = e.target as HTMLElement;
+  const { paramCode } = target.dataset;
+
+  if (paramCode) {
+    currentDevId.value = devId;
+    viewHistoryData(paramCode);
+  }
+};
 </script>
 
 <template>
@@ -137,7 +160,7 @@ const viewDevParam = (id: number) => {
     </div>
     <ARow :gutter="[22, 24]">
       <ACol v-for="item in deviceList" :key="item.id" :xs="24" :sm="24" :md="24" :lg="24" :xl="12" :xxl="8">
-        <div class="device-card-container">
+        <div class="device-card-container" @click="handleDevCardClick(item.id, $event)">
           <div class="device-card-header">
             <span
               :class="[
@@ -168,7 +191,8 @@ const viewDevParam = (id: number) => {
         </div>
       </ACol>
     </ARow>
-    <DeviceWorkParams ref="deviceWorkParams" :dev-id="currentDevId" />
+    <DeviceWorkParams ref="deviceWorkParams" :dev-id="currentDevId" @view-history-data="viewHistoryData" />
+    <DevWorkParamData ref="devWorkParamData" :dev-id="currentDevId" :param-codes="currentDevParamCodes" />
   </div>
 </template>
 
@@ -292,6 +316,7 @@ const viewDevParam = (id: number) => {
 
   + .progress-text-bar {
     margin-top: 4px;
+    cursor: pointer;
   }
 }
 
@@ -302,5 +327,6 @@ const viewDevParam = (id: number) => {
   font-weight: bold;
   line-height: 24px;
   color: #333;
+  cursor: pointer;
 }
 </style>