ModelOfMatter.vue 8.72 KB
<script lang="ts" setup>
  import { nextTick, onUnmounted, reactive, ref, unref } from 'vue';
  import { List, Button, Tooltip, Card } from 'ant-design-vue';
  import { PageWrapper } from '/@/components/Page';
  import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons-vue';
  import { BasicTable, useTable } from '/@/components/Table';
  import { realTimeDataColumns } from '../../config/detail.config';
  import { useWebSocket } from '@vueuse/core';
  import { getAuthCache } from '/@/utils/auth';
  import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { formatToDateTime } from '/@/utils/dateUtil';
  import { BasicForm, useForm } from '/@/components/Form';
  import HistoryData from './HistoryData.vue';
  import { BasicModal, useModal } from '/@/components/Modal';
  import { getDeviceAttrs } from '/@/api/device/deviceManager';
  import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  import { computed } from '@vue/reactivity';
  import { Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel';
  import { isArray, isObject } from '/@/utils/is';

  interface ReceiveMessage {
    data: {
      [key: string]: [number, string][];
    };
  }

  interface DataSource {
    key?: string;
    value?: string;
    time?: number;
  }

  const props = defineProps<{
    deviceDetail: Record<'tbDeviceId' | 'profileId' | 'id' | 'deviceProfileId', string>;
  }>();

  const grid = {
    gutter: 8,
    column: 3,
  };

  const token = getAuthCache(JWT_TOKEN_KEY);

  const socketInfo = reactive({
    origin: `${import.meta.env.VITE_WEB_SOCKET}${token}`,
    attr: undefined as string | undefined,
    originData: [] as DataSource[],
    dataSource: [] as DataSource[],
    message: {} as ReceiveMessage['data'],
    attrKeys: [] as DeviceModelOfMatterAttrs[],
  });

  const getSendValue = computed(() => {
    return {
      tsSubCmds: [
        {
          entityType: 'DEVICE',
          entityId: props.deviceDetail!.tbDeviceId,
          scope: 'LATEST_TELEMETRY',
          cmdId: 1,
          keys: socketInfo.attrKeys.map((item) => item.identifier).join(','),
        },
      ],
    };
  });

  const [registerForm, { getFieldsValue }] = useForm({
    schemas: [
      {
        field: 'value',
        label: '键/值',
        component: 'Input',
        colProps: { span: 8 },
        componentProps: {
          placeholder: '请输入键/值',
        },
      },
    ],
    labelWidth: 100,
    compact: true,
    showAdvancedButton: true,
    submitFunc: async () => {
      try {
        const { value } = getFieldsValue() || {};
        if (!value) setTableData(socketInfo.originData);
        const data = unref(socketInfo.originData).filter(
          (item) => item.key?.includes(value) || item.value?.includes(value)
        );
        await nextTick();
        socketInfo.dataSource = data;
        setTableData(data);
      } catch (error) {}
    },
    resetFunc: async () => {
      try {
        socketInfo.dataSource = socketInfo.originData;
        setTableData(socketInfo.originData);
      } catch (error) {}
    },
  });

  const [registerTable, { setTableData }] = useTable({
    columns: realTimeDataColumns,
    showTableSetting: true,
    bordered: true,
    showIndexColumn: false,
  });

  const [registerModal, { openModal }] = useModal();

  enum Mode {
    TABLE = 'table',
    CARD = 'card',
  }

  const mode = ref<Mode>(Mode.CARD);

  const switchMode = async (value: Mode) => {
    mode.value = value;
    await nextTick();
    setTableData(socketInfo.dataSource);
  };

  const { createMessage } = useMessage();

  const getUnit = (record: StructJSON) => {
    const { dataType } = record;
    const { specs } = dataType! || {};

    return isObject(specs)
      ? (specs as Specs).unitName && (specs as Specs).unit
        ? `${(specs as Specs).unitName}/${(specs as Specs).unit}`
        : ''
      : '';
  };

  const { send, close, data } = useWebSocket(socketInfo.origin, {
    async onConnected() {
      const { deviceProfileId } = props.deviceDetail;
      const value = await getDeviceAttrs({ deviceProfileId });
      socketInfo.attrKeys = isArray(value) ? value : [];
      send(JSON.stringify(unref(getSendValue)));
    },
    async onMessage() {
      try {
        const value = JSON.parse(unref(data)) as ReceiveMessage;
        if (value) {
          const { data } = value;
          const keys = Object.keys(data);
          keys.forEach((key) => {
            socketInfo.message[key] = value.data[key];
          });

          socketInfo.originData = socketInfo.dataSource = socketInfo.attrKeys.map((item) => {
            const { identifier: key, name, detail } = item;
            const unit = getUnit(detail);
            const [time, value] = socketInfo.message[key].at(0) || [];
            return { key, value, time, name, unit };
          });

          await nextTick();
          setTableData(socketInfo.dataSource);
        }
      } catch (error) {}
    },
    onDisconnected() {
      close();
    },
    onError() {
      createMessage.error('webSocket连接超时,请联系管理员');
    },
  });

  const handleShowDetail = (record: DataSource) => {
    const { key } = record;
    socketInfo.attr = key;
    openModal(true);
  };

  onUnmounted(() => close());
</script>

<template>
  <PageWrapper
    dense
    content-class="flex flex-col bg-transparent p-4"
    :content-style="{ backgroundColor: '#F0F2F5' }"
  >
    <section class="flex flex-col justify-between w-full bg-light-50 pt-3 mb-4">
      <div class="flex-auto">
        <BasicForm @register="registerForm" />
      </div>
    </section>
    <section class="bg-light-50">
      <div v-show="mode === Mode.CARD" class="flex h-70px items-center justify-end p-2">
        <Tooltip title="卡片模式">
          <Button
            :class="[mode === Mode.CARD && '!bg-blue-500 svg:text-light-50']"
            class="!p-2 !children:flex flex justify-center items-center border-r-0"
            @click="switchMode(Mode.CARD)"
          >
            <AppstoreOutlined />
          </Button>
        </Tooltip>

        <Tooltip title="列表模式">
          <Button
            class="!p-2 !children:flex flex justify-center items-center"
            @click="switchMode(Mode.TABLE)"
          >
            <BarsOutlined />
          </Button>
        </Tooltip>
      </div>
      <List
        v-if="mode === Mode.CARD"
        class="list-mode !px-2"
        :data-source="socketInfo.dataSource"
        :grid="grid"
      >
        <template #renderItem="{ item }">
          <List.Item>
            <Card class="shadow-md">
              <template #title>
                <span class="text-base font-normal">{{ item.name }}</span>
              </template>
              <template #extra>
                <Button type="link" class="!p-0" @click="handleShowDetail(item)">历史数据</Button>
              </template>
              <section class="min-h-16 flex flex-col justify-between">
                <div class="flex font-bold text-lg mb-4 gap-2">
                  <div>{{ item.value || '--' }}</div>
                  <div>{{ item.unit }}</div>
                </div>
                <div class="text-dark-800 text-xs">
                  {{ item.value ? formatToDateTime(item.time) : '--' }}
                </div>
              </section>
            </Card>
          </List.Item>
        </template>
      </List>
    </section>

    <BasicTable v-if="mode === Mode.TABLE" @register="registerTable">
      <template #toolbar>
        <div v-show="mode === Mode.TABLE" class="flex h-70px items-center justify-end p-2">
          <Tooltip title="卡片模式">
            <Button
              class="!p-2 !children:flex flex justify-center items-center border-r-0"
              @click="switchMode(Mode.CARD)"
            >
              <AppstoreOutlined />
            </Button>
          </Tooltip>

          <Tooltip title="列表模式">
            <Button
              :class="[mode === Mode.TABLE && '!bg-blue-500 svg:text-light-50']"
              class="!p-2 !children:flex flex justify-center items-center"
              @click="switchMode(Mode.TABLE)"
            >
              <BarsOutlined />
            </Button>
          </Tooltip>
        </div>
      </template>
    </BasicTable>
    <BasicModal
      @register="registerModal"
      title="历史数据"
      width="50%"
      destroy-on-close
      dialogClass="history-modal"
    >
      <HistoryData :deviceDetail="props.deviceDetail" :attr="socketInfo.attr" />
    </BasicModal>
  </PageWrapper>
</template>

<style scoped lang="less">
  .list-mode:deep(.ant-card-head) {
    border-bottom: 0;
  }

  .list-mode:deep(.ant-card-body) {
    padding-top: 0;
  }
</style>

<style>
  .history-modal .ant-input-number {
    min-width: 0 !important;
    width: 100% !important;
  }
</style>