瀏覽代碼

feat 设备拓扑

lvkun996 2 年之前
父節點
當前提交
30d7659676

+ 2 - 0
package.json

@@ -14,6 +14,7 @@
     "@codemirror/lang-vue": "^0.1.1",
     "@codemirror/theme-one-dark": "^6.1.2",
     "@codemirror/view": "^6.10.1",
+    "@ssthouse/vue3-tree-chart": "^0.2.6",
     "@vueuse/core": "^9.13.0",
     "ant-design-vue": "^3.3.0-beta.4",
     "axios": "^1.3.5",
@@ -21,6 +22,7 @@
     "codemirror": "^6.0.1",
     "core-js": "^3.8.3",
     "dayjs": "^1.11.7",
+    "html2canvas": "^1.4.1",
     "mitt": "^3.0.0",
     "path-browserify": "^1.0.1",
     "pinia": "^2.0.33",

+ 2 - 2
src/api/iot/device.ts

@@ -253,8 +253,8 @@ export const getDevicShadow = (deviceId: string) => {
 }
 
 /** 设备拓扑 */
-export const getDevicTopology = (modelId: string) => {
-  return request<IOT.API.DEVICE.Topology[]>({
+export const getDeviceTopology = (modelId: string) => {
+  return request<IOT.API.DEVICE.Topology>({
     url: `/device/topology?modelId=${modelId}`,
     method: 'GET'
   })

+ 14 - 0
src/api/iot/rule.ts

@@ -83,6 +83,20 @@ export const getForwardCount = (status?: boolean) => {
   })
 }
 
+export const forwardDebug = (data: {
+  'ruleId':'xx' // 规则id 必须
+  'modelId':'模型id' // 必须
+  'deviceId':'设备id' // 必须
+  'sessionEvent':''// 设备连接状态 CONNECT 连接。DISCONNECT 断开连接
+  'payload':[] // 数据
+  }) => {
+  return request<number>({
+    url: '/forwardRule/debug',
+    method: 'POST',
+    data
+  })
+}
+
 /**
  * 此函数使用 PUT 请求更新转发规则的状态。
  * @param params - `params` 对象包含两个属性:

+ 15 - 0
src/api/iot/task.ts

@@ -51,3 +51,18 @@ export const delTask = (id: string) => {
     method: 'DELETE'
   })
 }
+
+export const trackTaskPage = (params: COMMON.API.QueryParams | { taskId: string }) => {
+  return request<IOT.API.TASK.TrackTask>({
+    url: '/taskTrace/page',
+    method: 'GET',
+    params
+  })
+}
+
+export const trackTasById = (id: string) => {
+  return request<IOT.API.TASK.TrackTask>({
+    url: `/taskTrace/${id}`,
+    method: 'GET'
+  })
+}

+ 17 - 1
src/controller/iot/device.ts

@@ -2,7 +2,7 @@ import {
   addDevice, addSubDevice, delDevice, delDeviceMul, delDeviceTag,
   getDeviceById, getDeviceCount, getDeviceList, getDeviceMsgList, addDeviceMsg, getDeviceTag,
   getSubDeviceList, updateDeviceLabel, addDeviceCmd, getDeviceCmdList, addDeviceTag, addDeviceGroup,
-  listDeviceGroup, postGroupBindDevice, delGroupBindDevice, getDeviceByGroup, getDevicePage, delSubDevice, getDeviceAttribute, getDevicShadow
+  listDeviceGroup, postGroupBindDevice, delGroupBindDevice, getDeviceByGroup, getDevicePage, delSubDevice, getDeviceAttribute, getDevicShadow, getDeviceTopology
 } from '@/api/iot/device'
 import { DeviceMsgEnum } from '@/enum/common'
 import { message } from 'ant-design-vue'
@@ -207,4 +207,20 @@ export class DeviceContriller {
   static async getDevicShadow (deviceId: string) {
     return await getDevicShadow(deviceId)
   }
+
+  static async getDeviceTopology (modelId: string) {
+    // const { data } = await getDeviceTopology(modelId)
+    // const { gatewayDevice, model, subDevice } = data
+
+    // return {
+    //   data: {
+    //     gatewayDevice: gatewayDevice.map(item => {
+    //       return {
+    //         name: item.
+    //       }
+    //     })
+    //   }
+    // }
+    return await getDeviceTopology(modelId)
+  }
 }

+ 9 - 1
src/controller/iot/task.ts

@@ -1,4 +1,4 @@
-import { delTask, getTaskById, getTaskCount, getTaskPage, postTask, updateTask, updateTaskStatus } from '@/api/iot/task'
+import { delTask, getTaskById, getTaskCount, getTaskPage, postTask, trackTasById, trackTaskPage, updateTask, updateTaskStatus } from '@/api/iot/task'
 import { message } from 'ant-design-vue'
 
 export class TaskController {
@@ -41,4 +41,12 @@ export class TaskController {
     await updateTaskStatus(id, status)
     message.success('修改成功')
   }
+
+  static async trackTaskPage (params: COMMON.API.QueryParams | { taskId: string }) {
+    return await trackTaskPage(params)
+  }
+
+  static async trackTasById (id: string) {
+    return await trackTasById(id)
+  }
 }

+ 197 - 0
src/pages/Iot/device/topology.vue

@@ -0,0 +1,197 @@
+<template>
+  <a-card>
+    <a-row>
+      <a-col>
+        <a-select
+          placeholder="请选择产品"
+          v-model:value="state.modelId"
+        >
+          <a-select-option
+            v-for="model in state.modelList"
+            :key="model.id"
+            :value="model.id"
+          >
+            {{model.modelLabel}}
+          </a-select-option>
+        </a-select>
+        <a-button @click="exportDomToImage" >导出</a-button>
+      </a-col>
+    </a-row>
+
+    <div class="topology-preview" >
+      <a-row class="zoom-navbar" >
+        <a-col>
+          <a-tooltip>
+            <template #title>放大</template>
+            <zoom-in-outlined  :style="{fontSize: '34px'}" />
+          </a-tooltip>
+          <a-tooltip>
+            <template #title>缩小</template>
+            <zoom-out-outlined :style="{fontSize: '34px'}" />
+          </a-tooltip>
+        </a-col>
+      </a-row>
+      <vue-tree
+        style="width: 1000px; height: 600px; border: 1px solid gray;"
+        :dataset="treeData.richMediaData"
+        :config="treeData.treeConfig"
+        id="vueTreeRef"
+        ref="vueTreeRef"
+      >
+        <template v-slot:node="{ node, collapsed }">
+          <a-row
+            class="rich-media-node"
+          >
+            <a-col :span="24"  >
+              <a-space>
+                📦
+                  <div>
+                  {{node.value}}
+                  {{node.modelLabel}}
+                  {{node.deviceLabel}}
+                </div>
+              </a-space>
+            </a-col>
+            <a-col>
+              <div>
+                {{node.transportType}}
+              {{node.payloadType}}
+              </div>
+
+            </a-col>
+            <!-- {{node.deviceLabel}} -->
+          </a-row>
+        </template>
+      </vue-tree>
+    </div>
+
+  </a-card>
+</template>
+
+<script lang="ts" setup >
+import { DeviceContriller, ModelController } from '@/controller'
+import { reactive, onMounted, watch, ref } from 'vue'
+import VueTree from '@ssthouse/vue3-tree-chart'
+import '@ssthouse/vue3-tree-chart/dist/vue3-tree-chart.css'
+import { AppstoreOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons-vue'
+import html2canvas from 'html2canvas'
+
+const state = reactive<{
+  modelId: string,
+  modelList: IOT.API.MODEL.Model[],
+  dataSource: {
+    gatewayDevice: any[]
+    model: IOT.API.MODEL.Model[]
+    subDevice: IOT.API.DEVICE.Device[]
+  }
+}>({
+  modelId: '',
+  modelList: [],
+  dataSource: {
+    gatewayDevice: [],
+    model: [],
+    subDevice: []
+  }
+})
+
+const vueTreeRef = ref('')
+
+const treeData = reactive({
+  richMediaData: {},
+  treeConfig: { nodeWidth: 120, nodeHeight: 80, levelHeight: 200 }
+}
+)
+
+window.addEventListener('wheel', function (event) {
+  console.log(event.deltaY)
+  if (event.deltaY > 0) {
+    vueTreeRef.value.zoomOut()
+  } else {
+    vueTreeRef.value.zoomIn()
+  }
+})
+
+watch(
+  () => state.modelId,
+  () => getDeviceTopology()
+)
+
+function exportDomToImage () {
+  html2canvas(document.getElementById('vueTreeRef')).then(canvas => {
+    const imgData = canvas.toDataURL('image/png')
+    const link = document.createElement('a')
+    link.href = imgData
+    link.download = 'exported-image.png'
+    link.click()
+  })
+}
+
+const getDeviceTopology = async () => {
+  const { data } = await DeviceContriller.getDeviceTopology(state.modelId)
+  const { gatewayDevice, model, subDevice } = data
+
+  const obj = {
+    name: '产品模型',
+    value: '产品模型',
+    children: model
+  }
+
+  obj.children.forEach(model => {
+    model.children = []
+    gatewayDevice.forEach(item => {
+      if (item.modelId === model.id) {
+        model.children.push(item)
+      }
+      item.children = []
+      subDevice.forEach(sd => {
+        if (sd.gatewayId === item.id) {
+          item.children.push(sd)
+        }
+      })
+    })
+  })
+
+  treeData.richMediaData = obj
+  console.log('obj:', obj)
+}
+
+const getModelList = async () => {
+  const { data } = await ModelController.list()
+  state.modelList = data
+}
+
+onMounted(() => {
+  getModelList()
+})
+
+</script>
+
+<style lang="less" scoped >
+.container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.rich-media-node {
+  padding: 8px;
+  color: white;
+  background-color: #1890ff;
+  border-radius: 4px;
+  a-col {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+.topology-preview {
+  position: relative;
+  .zoom-navbar {
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+}
+
+</style>

+ 11 - 6
src/pages/Iot/model/models.vue

@@ -1,5 +1,10 @@
 <template>
 <a-card>
+  <a-row justify="end" >
+    <a-col>
+      <a-button @click="openModal" >加入</a-button>
+    </a-col>
+  </a-row>
   <a-table
     style="margin-top: 20px;"
     :columns="columns"
@@ -11,9 +16,7 @@
   <template #bodyCell="{column, record}">
         <template v-if="column.key === 'action'" >
           <a-space>
-            <a @click="openModal(record)" >加入</a>
             <a >详情</a>
-            <a >删除</a>
           </a-space>
         </template>
       </template>
@@ -32,7 +35,7 @@
       <a-input v-model:value="modelRef.templateLabel"  />
     </a-form-item>
 
-    <a-form-item label="产品" v-bind="validateInfos.templateLabel">
+    <a-form-item label="产品" v-bind="validateInfos.modelId">
       <a-select v-model:value="modelRef.modelId" >
         <a-select-option
           v-for="item in modelState.dataSource"
@@ -45,7 +48,6 @@
     </a-form-item>
 
   </a-form>
-
 </modal-pro>
 </template>
 <script lang='ts' setup >
@@ -116,7 +118,8 @@ const { resetFields, validate, validateInfos } = useForm(modelRef, reactive({
   modelId: [{ required: true, message: '请选择模型' }]
 }))
 
-const openModal = (record) => {
+const openModal = () => {
+  resetFields()
   state.visible = true
   getModel()
 }
@@ -128,7 +131,9 @@ const changePage = ({ current }) => {
 
 const ok = () => {
   validate().then(async () => {
-
+    await ModelController.postModelTemplate(modelRef)
+    state.visible = false
+    getModelsPage()
   })
 }
 

+ 1 - 0
src/pages/Iot/rule/forwardRule.vue

@@ -90,6 +90,7 @@
       </template>
       <template v-if="column.key === 'action'" >
         <a-space>
+              <a @click="openDetailModal(record.id)">调试</a>
               <a @click="openDetailModal(record.id)">详情</a>
               <a @click="openModal('update', record)">编辑</a>
               <a-popconfirm

+ 0 - 1
src/pages/Iot/sys/notice.vue

@@ -167,7 +167,6 @@
 import { reactive, onMounted, computed } from 'vue'
 import { SysController } from '@/controller'
 import { Form } from 'ant-design-vue'
-import { Item } from 'ant-design-vue/lib/menu'
 
 const useForm = Form.useForm
 

+ 119 - 1
src/pages/Iot/task/track.vue

@@ -1,9 +1,127 @@
 <template>
 <a-card>
-  任务追踪
+  <a-row>
+    <a-col>
+      <a-range-picker v-model:value="time" :format="['DD/MM/YYYY', 'DD/MM/YYYY']"  />
+    </a-col>
+  </a-row>
+
+  <a-table
+    style="margin-top: 20px;"
+    :columns="columns"
+    :data-source="state.dataSource"
+    :loading="state.loading"
+    :pagination="queryParamsState"
+    @change="changePage"
+  >
+    <template #bodyCell="{column, record}">
+          <template v-if="column.key === 'action'" >
+            <a-space>
+              <a @click="openModal('preview', record.id)" >详情</a>
+            </a-space>
+          </template>
+    </template>
+  </a-table>
 </a-card>
+
+<modal-pro
+    style="width: 700px;overflow: hidden;"
+    label="最终详情"
+    :visible="state.visable"
+    @cancel="state.visable = false"
+    @ok="state.visable = false"
+  >
+
+</modal-pro>
+
 </template>
+
 <script lang='ts' setup >
+import { TaskController } from '@/controller/iot/task'
+import { onMounted, reactive, ref, watch } from 'vue'
+import dayjs from 'dayjs'
+
+const columns = [
+  {
+    title: '任务id',
+    dataIndex: 'taskId'
+  },
+  {
+    title: '执行时间',
+    dataIndex: 'ts',
+    key: 'ts'
+  },
+  {
+    title: '任务参数',
+    dataIndex: 'params',
+    key: 'params'
+  },
+  {
+    title: '任务结果',
+    dataIndex: 'ret',
+    key: 'ret'
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    key: 'action'
+  }
+]
+
+const time = ref([])
+
+const queryParamsState = reactive({
+  page: 1,
+  pageSize: 10,
+  start: 0,
+  end: 0,
+  taskId: ''
+})
+
+const state = reactive({
+  taskList: [],
+  dataSource: [],
+  loading: false,
+  visable: false,
+  opraState: '',
+  id: ''
+})
+
+watch(
+  () => time.value,
+  () => {
+    if (dayjs(time.value).format('YYYY/MM/DD') === 'Invalid Date') {
+      queryParamsState.start = ''
+      queryParamsState.end = ''
+    } else {
+      queryParamsState.start = new Date(dayjs(time.value[0])).getTime()
+      queryParamsState.end = new Date(dayjs(time.value[1])).getTime()
+    }
+  }
+)
+
+const openModal = (opraState: 'preview', id: string) => {
+  state.opraState = opraState
+  state.id = id
+}
+
+const changePage = ({ current }) => {
+  queryParamsState.page = current
+  getTrackTaskList()
+}
+
+const getTrackTaskList = async () => {
+  state.loading = true
+  const { data } = await TaskController.trackTaskPage(queryParamsState)
+  state.loading = false
+  state.dataSource = data
+}
+
+onMounted(() => {
+  getTrackTaskList()
+})
+
 </script>
+
 <style lang='less' scoped >
 </style>

+ 6 - 1
src/router/index.ts

@@ -59,8 +59,13 @@ export const routes: Array<ROUTER.RoutesProps> = [
           },
           {
             path: '/device/group',
-            name: '群组',
+            name: '设备群组',
             component: () => import('@/pages/iot/device/group.vue')
+          },
+          {
+            path: '/device/topology',
+            name: '设备拓扑 ',
+            component: () => import('@/pages/iot/device/topology.vue')
           }
         ]
       },

+ 3 - 0
src/type/common.d.ts

@@ -5,6 +5,9 @@ declare namespace COMMON {
       interface QueryParams {
         page: number,
         pageSize: number
+        start?: number
+        end?: number,
+        status?: boolean
       }
 
       interface Transport {

+ 45 - 12
src/type/iot.d.ts

@@ -182,17 +182,41 @@ declare namespace IOT {
       }
 
       interface Topology {
-        'id': string,
-        'createAt': number,
-        'updateAt': string,
-        'deleted': string,
-        'modelLabel': string,
-        'transportType': string,
-        'payloadType': string,
-        'deviceType': string,
-        'modelDescription': string,
-        'tenantId': string
-      }
+        gatewayDevice: {
+          'id': string,
+          'modelLabel': string,
+          'deviceCode': string,
+          'deviceLabel': string,
+          'deviceDescription': string,
+          'modelId': string,
+          'deviceStatus': string,
+          'deviceNodeType': string,
+          'lastConnectTs': string,
+          'lastActivityTs': string,
+          'gatewayId': string
+        }[],
+        model: {
+          'id': string,
+          'modelLabel': string,
+          'transportType': string,
+          'payloadType': string,
+          'deviceType': string,
+          'modelDescription': string,
+        }[],
+        subDevice: {
+          'id': string,
+          'modelLabel': string,
+          'deviceCode': string,
+          'deviceLabel': string,
+          'deviceDescription': string,
+          'modelId': string,
+          'deviceStatus': string,
+          'deviceNodeType': string,
+          'lastConnectTs': string,
+          'lastActivityTs': string,
+          'gatewayId': string
+        }[]
+    }
     }
 
     namespace EVENT {
@@ -338,6 +362,14 @@ declare namespace IOT {
         'DISABLE': number,
         'ENABLE': number
       }
+
+      interface TrackTask {
+        'id': string,
+        'taskId': string, // 任务id
+        'ts': bigint, // 执行时间
+        'params': null, // 任务参数
+        'ret': null, // 任务结果
+      }
     }
 
     namespace SYS {
@@ -371,6 +403,7 @@ declare namespace IOT {
         }
       }
     }
-}
+
+  }
 
 }

+ 320 - 6
yarn.lock

@@ -1361,6 +1361,20 @@
   resolved "https://registry.npmmirror.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz"
   integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
 
+"@ssthouse/tree-chart-core@^1.1.0":
+  version "1.1.2"
+  resolved "https://registry.npmjs.org/@ssthouse/tree-chart-core/-/tree-chart-core-1.1.2.tgz#2857292c2fe809305abe265c3458512a814cdb9f"
+  integrity sha512-UiPOu+K8XoS1dPPerhlpf2WZMsREBYnuTWKYcWps/oQPyLdLKbS6ZFBZXaFqm4Q91GEvf+AfD8MK8Cllw2V4kg==
+  dependencies:
+    d3 "^7.2.0"
+
+"@ssthouse/vue3-tree-chart@^0.2.6":
+  version "0.2.6"
+  resolved "https://registry.npmjs.org/@ssthouse/vue3-tree-chart/-/vue3-tree-chart-0.2.6.tgz#5b99a259fad1fe7bde6be93f6bef72233dc76162"
+  integrity sha512-ihSTPYXTri834ViIKlp4DGfZ5Kt+xwy8H5Bd9KBD2QO/S7Dath0UreF+5Dk5fd8gMDqvvI/wFg6OZxwiNf3iHA==
+  dependencies:
+    "@ssthouse/tree-chart-core" "^1.1.0"
+
 "@trysound/sax@0.2.0":
   version "0.2.0"
   resolved "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz"
@@ -2541,6 +2555,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
+  integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
 base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz"
@@ -2899,16 +2918,16 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@7, commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@^7.2.0:
-  version "7.2.0"
-  resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz"
-  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-
 commander@^8.2.0, commander@^8.3.0:
   version "8.3.0"
   resolved "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz"
@@ -3085,6 +3104,13 @@ css-declaration-sorter@^6.3.1:
   resolved "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz"
   integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==
 
+css-line-break@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
+  integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+  dependencies:
+    utrie "^1.0.2"
+
 css-loader@^6.5.0:
   version "6.7.3"
   resolved "https://registry.npmmirror.com/css-loader/-/css-loader-6.7.3.tgz"
@@ -3201,6 +3227,250 @@ csstype@^2.6.8:
   resolved "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz"
   integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
 
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
+  version "3.2.4"
+  resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+  integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+  dependencies:
+    internmap "1 - 2"
+
+d3-axis@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+  integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+  integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "3"
+    d3-transition "3"
+
+d3-chord@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+  integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+  dependencies:
+    d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+  version "3.1.0"
+  resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+  version "4.0.2"
+  resolved "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+  integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+  dependencies:
+    d3-array "^3.2.0"
+
+d3-delaunay@6:
+  version "6.0.4"
+  resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
+  integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+  dependencies:
+    delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+  integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+  dependencies:
+    commander "7"
+    iconv-lite "0.6"
+    rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+  integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+  integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+  dependencies:
+    d3-dsv "1 - 3"
+
+d3-force@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+  version "3.1.0"
+  resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+  version "3.1.0"
+  resolved "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
+  integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
+  dependencies:
+    d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+  version "3.1.2"
+  resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+  integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+  integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+  integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-scale-chromatic@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
+  integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
+  dependencies:
+    d3-color "1 - 3"
+    d3-interpolate "1 - 3"
+
+d3-scale@4:
+  version "4.0.2"
+  resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+  dependencies:
+    d3-array "2.10.0 - 3"
+    d3-format "1 - 3"
+    d3-interpolate "1.2.0 - 3"
+    d3-time "2.1.1 - 3"
+    d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+  integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3:
+  version "3.2.0"
+  resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+  integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+  dependencies:
+    d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+  version "4.1.0"
+  resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+  version "3.1.0"
+  resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+  integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+  dependencies:
+    d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+  integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+  dependencies:
+    d3-color "1 - 3"
+    d3-dispatch "1 - 3"
+    d3-ease "1 - 3"
+    d3-interpolate "1 - 3"
+    d3-timer "1 - 3"
+
+d3-zoom@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+  integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "2 - 3"
+    d3-transition "2 - 3"
+
+d3@^7.2.0:
+  version "7.8.5"
+  resolved "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c"
+  integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==
+  dependencies:
+    d3-array "3"
+    d3-axis "3"
+    d3-brush "3"
+    d3-chord "3"
+    d3-color "3"
+    d3-contour "4"
+    d3-delaunay "6"
+    d3-dispatch "3"
+    d3-drag "3"
+    d3-dsv "3"
+    d3-ease "3"
+    d3-fetch "3"
+    d3-force "3"
+    d3-format "3"
+    d3-geo "3"
+    d3-hierarchy "3"
+    d3-interpolate "3"
+    d3-path "3"
+    d3-polygon "3"
+    d3-quadtree "3"
+    d3-random "3"
+    d3-scale "4"
+    d3-scale-chromatic "3"
+    d3-selection "3"
+    d3-shape "3"
+    d3-time "3"
+    d3-time-format "4"
+    d3-timer "3"
+    d3-transition "3"
+    d3-zoom "3"
+
 dayjs@^1.10.5, dayjs@^1.11.7:
   version "1.11.7"
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz"
@@ -3274,6 +3544,13 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
     has-property-descriptors "^1.0.0"
     object-keys "^1.1.1"
 
+delaunator@5:
+  version "5.0.0"
+  resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
+  integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
+  dependencies:
+    robust-predicates "^3.0.0"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
@@ -4411,6 +4688,14 @@ html-webpack-plugin@^5.1.0:
     pretty-error "^4.0.0"
     tapable "^2.0.0"
 
+html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 htmlparser2@^6.1.0:
   version "6.1.0"
   resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz"
@@ -4484,7 +4769,7 @@ iconv-lite@0.4.24:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-iconv-lite@^0.6.3:
+iconv-lite@0.6, iconv-lite@^0.6.3:
   version "0.6.3"
   resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz"
   integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -4561,6 +4846,11 @@ internal-slot@^1.0.5:
     has "^1.0.3"
     side-channel "^1.0.4"
 
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
 interpret@^1.4.0:
   version "1.4.0"
   resolved "https://registry.npmmirror.com/interpret/-/interpret-1.4.0.tgz"
@@ -6365,6 +6655,11 @@ rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
+robust-predicates@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
+  integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
 run-parallel@^1.1.9:
   version "1.2.0"
   resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz"
@@ -6372,6 +6667,11 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
 rxjs@^7.5.1:
   version "7.8.1"
   resolved "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.1.tgz"
@@ -6985,6 +7285,13 @@ terser@^5.10.0, terser@^5.16.5:
     commander "^2.20.0"
     source-map-support "~0.5.20"
 
+text-segmentation@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
+  integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+  dependencies:
+    utrie "^1.0.2"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz"
@@ -7211,6 +7518,13 @@ utils-merge@1.0.1:
   resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz"
   integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
 
+utrie@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
+  integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+  dependencies:
+    base64-arraybuffer "^1.0.2"
+
 uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz"