ReportPreviewModal.vue 11.2 KB
<template>
  <div>
    <BasicModal
      destroyOnClose
      v-bind="$attrs"
      width="60rem"
      :height="heightNum"
      @register="register"
      title="报表趋势图"
      :minHeight="500"
      :showOkBtn="false"
    >
      <div :class="[chartInstance.length % 2 !== 0 ? '' : 'wrapper']">
        <Spin :spinning="loading">
          <div
            v-if="chartInstance.length > 0"
            :class="[chartInstance.length % 2 !== 0 ? '' : 'chart-style']"
          >
            <div
              :class="[
                chartInstance.length % 2 !== 0 ? '' : 'inner',
                chartInstance.length % 2 !== 0 ? '' : 'item',
              ]"
              class="mb-20"
              v-for="item in chartInstance"
              :key="item.device"
            >
              <p class="text-black text-lg">{{ item.name }}</p>
              <div class="flex text-black items-center">
                <div class="mr-4 text-sm">属性:</div>
                <Select
                  class="min-w-25"
                  v-model:value="item.active"
                  @change="(value) => handleChangeChars(value, item.device, item)"
                  v-bind="createPickerSearch()"
                  placeholder="请选择设备属性"
                  :options="
                    item?.attributes?.map((attrItem: any) => ({
                      label: attrItem.label,
                      value: attrItem.value,
                    }))
                  "
                />
              </div>
              <div class="w-full h-full flex justify-center items-center">
                <Empty v-show="item.notFoundData" description="暂无数据" class="text-dark-50" />
                <div
                  v-show="!item.notFoundData"
                  :id="`chart-${item.device}`"
                  class="w-full h-96"
                ></div>
              </div>
            </div>
          </div>
          <div v-else style="display: flex; justify-content: center; align-items: center">
            <div style="position: relative; left: 0; top: 3rem">暂无数据</div>
          </div>
        </Spin>
      </div>
    </BasicModal>
  </div>
</template>
<script setup lang="ts">
  import { ref, PropType, nextTick, shallowReactive, onMounted, onUnmounted, unref } from 'vue';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import * as echarts from 'echarts';
  import { exportViewChartApi } from '/@/api/export/exportManager';
  import moment from 'moment';
  import { ExecuteReportRecord } from '/@/api/export/model/exportModel';
  import { Select, Spin, Empty } from 'ant-design-vue';
  import { createPickerSearch } from '/@/utils/pickerSearch';
  import { getDeviceDetail } from '/@/api/device/deviceManager';
  import { getAttribute } from '/@/api/ruleengine/ruleengineApi';

  interface ResponsData {
    attr: string;
    val: { ts: number; value: string }[];
  }

  interface ChartInstance {
    device: string;
    name: string;
    attributes: string[];
    active?: string;
    notFoundData?: boolean;
    loading?: boolean;
  }

  defineProps({
    width: {
      type: String as PropType<string>,
      default: '100%',
    },
    height: {
      type: String as PropType<string>,
      default: '400px',
    },
  });
  defineEmits(['register']);
  const heightNum = ref(550);

  let currentRecord: ExecuteReportRecord = {} as unknown as ExecuteReportRecord;

  const chartInstance = ref<ChartInstance[]>([]);

  const chartsInstance = shallowReactive<{ [key: string]: echarts.ECharts }>({});
  //生成随机颜色
  let getRandomColor = function () {
    return (
      'rgb(' +
      Math.round(Math.random() * 255) +
      ',' +
      Math.round(Math.random() * 255) +
      ',' +
      Math.round(Math.random() * 10) +
      ')'
    );
  };

  const getChartsOption = (result: ResponsData) => {
    const generateInfo = Object.entries(result).map((item) => {
      const [attr, val] = item;
      return { attr, val } as { attr: string; val: { ts: number; value: string }[] };
    });
    const chartDataConfig =
      generateInfo.map((item) => {
        const seriesData = item.val.map((m1) => Number(m1.value));
        return {
          xAxis: item.val.map((m) => moment(m.ts).format('YYYY-MM-DD HH:mm:ss')),
          series: {
            name: item.attr,
            type: 'line',
            data: seriesData,
            lineStyle: {
              color: getRandomColor(),
            },
          },
        } as echarts.EChartsOption;
      }) || ([] as echarts.EChartsOption[]);

    const chartOption = {
      xAxisData: (chartDataConfig.at(0)?.xAxis as string[])?.map((str) => str.replace(' ', '\n')),
      series: [chartDataConfig.at(0)?.series],
      legend: {
        data: Object.keys(result),
      },
    };
    return chartOption;
  };

  const handleDeviceProfileAttributes = async (entityId: string) => {
    const deviceDetailRes = await getDeviceDetail(entityId);
    const { deviceProfileId } = deviceDetailRes;
    if (!deviceProfileId) return;
    const attributeRes = await getAttribute(deviceProfileId);
    return handleDataFormat(deviceDetailRes, attributeRes);
  };

  const handleDataFormat = (deviceDetail: any, attributes: any) => {
    const { name, tbDeviceId } = deviceDetail;
    const attribute = attributes.map((item: any) => ({
      identifier: item.identifier,
      name: item.name,
      detail: item.detail,
    }));
    return {
      name,
      tbDeviceId,
      attribute,
    };
  };

  const [register, { setModalProps }] = useModalInner(
    async (data: { record: ExecuteReportRecord }) => {
      setModalProps({ loading: true });
      try {
        currentRecord = data.record;
        const deviceInfo = data.record.executeCondition.executeAttributes || [];
        let attributesList: Recordable[] = [];
        // 处理为物模型里的标识符名
        const reflectDeviceList = deviceInfo.map(async (item) => {
          const { device, attributes } = item as Recordable;
          if (!device) return;
          const thingsModel = (await handleDeviceProfileAttributes(item.device)) as Recordable;
          const { tbDeviceId, attribute } = thingsModel as Recordable;
          if (!tbDeviceId) return;
          if (device === tbDeviceId) {
            attributesList = attributes.reduce((acc, curr) => {
              attribute.forEach((item: Recordable) => {
                if (item.identifier === curr) {
                  acc.push({
                    label: item.name,
                    value: item.identifier,
                  });
                }
              });
              return [...acc];
            }, []);
          }
          return {
            ...item,
            active: attributes.at(0),
            attributes: attributesList,
          };
        });
        chartInstance.value = (await Promise.all(reflectDeviceList)) as any as ChartInstance[];
        for (const item of unref(chartInstance)) {
          const { attributes, device } = item;

          const keys = attributes.length ? (attributes as any[]).at(0)?.value : '';

          const sendParams = {
            ...data.record.executeCondition.executeCondition,
            ...{
              keys,
            },
          };

          const result = await exportViewChartApi(device, sendParams);
          item.notFoundData = validateHasData(result);
          const { xAxisData, series } = getChartsOption(result as unknown as ResponsData);

          await nextTick();
          chartsInstance[device] = echarts.init(
            document.getElementById(`chart-${device}`) as HTMLElement
          );

          const chartOption = {
            title: {
              left: 'center',
            },
            tooltip: {
              trigger: 'axis',
              axisPointer: {
                type: 'cross',
                animation: false,
                label: {
                  backgroundColor: '#505765',
                },
              },
            },
            legend: {
              // data: attributes,
              top: '20px',
            },
            toolbox: {},
            grid: {
              left: '14%',
              right: '20%',
              bottom: 80,
              containLabel: true,
            },
            dataZoom: [
              {
                show: true,
                realtime: true,
                start: 0,
                end: 15,
                height: '30',
              },
              {
                type: 'inside',
                realtime: true,
                start: 0,
                end: 15,
                height: '30',
              },
            ],
            xAxis: {
              type: 'category',
              data: xAxisData,
              boundaryGap: false,
              axisPointer: { type: 'shadow' },
              axisLabel: {
                show: false,
                interval: 0,
                rotate: 65,
                textStyle: {
                  color: '#000',
                  fontSize: 10,
                },
              },
              axisLine: {
                show: true,
                interval: 0,
                lineStyle: {
                  color: 'RGB(210,221,217)',
                },
              },
            },
            yAxis: {
              type: 'value',
              boundaryGap: false,
            },
            series,
          };
          chartsInstance[device].setOption(chartOption);
          //自适应
          // window.addEventListener('resize', () => {
          //   chartsInstance[device].resize();
          // });
        }
      } catch (error) {
        throw error;
      } finally {
        setModalProps({ loading: false });
      }
    }
  );
  const loading = ref(false);
  const renderCharts = async (device: string, keys: string, item: ChartInstance) => {
    const sendParams = {
      ...currentRecord.executeCondition.executeCondition,
      ...{
        keys,
      },
    };
    try {
      loading.value = true;
      const result = await exportViewChartApi(device, sendParams);

      item.notFoundData = validateHasData(result);
      const { xAxisData, series } = getChartsOption(result as unknown as ResponsData);

      chartsInstance[device].setOption({
        series,
        xAxis: { data: xAxisData },
      } as echarts.EChartsOption);

      await nextTick();
      resize();
    } catch (error) {
    } finally {
      loading.value = false;
    }
  };

  const validateHasData = (record: Recordable) => {
    // const { val = [], attr } = (record as unknown as ResponsData) || {};
    if (!Object.keys(record).length) {
      return true;
    }
    return false;
  };

  const handleChangeChars = (value: string, device: string, item: ChartInstance) => {
    renderCharts(device, value, item);
  };

  const resize = () => {
    Object.keys(chartsInstance).forEach((key) => {
      chartsInstance[key].resize();
    });
  };

  onMounted(() => {
    window.addEventListener('resize', resize);
  });

  onUnmounted(() => {
    window.removeEventListener('resize', resize);
  });
</script>
<style lang="less" scoped>
  .wrapper {
    margin: 10px 60px 60px 60px;
  }

  .inner {
    width: 400px;
    height: 400px;
  }

  .item {
    text-align: center;
    font-size: 200%;
    color: #fff;
  }

  .chart-style {
    display: flex;
    flex-wrap: wrap;
    width: 825px;
    margin-top: 10px;
    justify-content: space-between;
  }
</style>