ananzhusen 4 月之前
父節點
當前提交
dfb635a4bb
共有 3 個文件被更改,包括 2878 次插入0 次删除
  1. 79 0
      src/services/iot.ts
  2. 2477 0
      src/views/components/DataSource.vue
  3. 322 0
      src/views/components/Net.vue

+ 79 - 0
src/services/iot.ts

@@ -0,0 +1,79 @@
+import axios from "axios";
+//获取设备列表
+export async function getDevices(){
+  const body: any = {
+    case: '',
+    name: '',
+    productId: '',
+  };
+  const ret: { list: any[]; total: number; type:number } = await axios.post(
+    '/api/iot/devices'
+    // '/api/iot/device/list'
+  );
+  return ret;
+}
+
+export async function getMqttUrl () {
+  const ret:any = await axios.get('/api/iot/mqtt/url');
+  return ret;
+}
+
+//获取设备属性列表
+export async function getDeviceProperties(deviceId:string){
+  // const ret:any = await axios.get(`/api/iot/device/properties?id=${deviceId}`);
+  const ret:any = await axios.post(`/api/iot/device/properties`,{
+    deviceId:deviceId
+  });
+  return ret.list||[];
+}
+
+export async function subscribeProperties(devices:{deviceId:string; token:string;properties:string[]}[]){
+  const ret:any = await axios.post(`/api/iot/subscribe/properties`,{
+    devices
+  });
+  return ret;
+}
+
+export async function unsubscribeProperties(token:string){
+  const ret:any = await axios.post(`/api/iot/unsubscribe/properties`,{
+    token
+  });
+  return ret;
+}
+
+//获取sql数据源列表
+export async function getSqlSourceList(){
+  const ret:any = await axios.post(
+    '/api/iot/data/sql/sources',
+    {
+      params: {
+        current: 1,
+        pageSize: 100,
+      },
+    }
+  );
+  return ret;
+}
+
+//执行查数据库操作 eg:SELECT id, fullpath FROM "directory" ;
+export type SqlType = 'list' | 'get' | 'exec' | 'add' | 'update' | 'delete';
+export async function doSqlCode(sql:{method?:SqlType, dbid?:string,sql?:string,pageSize?:number,current?:number}) {
+  let _sql = sql.sql 
+  if(sql.method==='list'){
+    _sql+= ` LIMIT ${sql.pageSize||20}`+(sql.current>1?(' OFFSET '+(sql.current-1)*sql.pageSize):'');
+  }
+  console.log("doSqlGet",_sql);
+
+  const ret = await axios.post(
+    `/api/iot/data/sql/${sql.method}`,{
+      dbid:sql.dbid,
+      sql:_sql,
+    }
+  );
+  return ret;
+}
+
+//数据库操作
+//增 INSERT INTO tbl_name SET col_name1=value1,col_name2=value2,......;
+//删 DELETE FROM users WHERE name = 'John';
+//改 UPDATE tbl_name SET col_name1=value1,col_name2=value2,...... WHERE some_column=some_value;

+ 2477 - 0
src/views/components/DataSource.vue

@@ -0,0 +1,2477 @@
+<template>
+  <div
+    class="content"
+    style="height: calc(100vh - 82px); overflow-y: auto"
+    v-if="group === '数据'"
+  >
+    <div
+      class="flex mt-16 mb-16"
+      style="justify-content: space-between; padding-right: 8px"
+    >
+      <div style="line-height: 32px">数据列表</div>
+      <t-dropdown :minColumnWidth="168">
+        <div class="icon-box">
+          <AddIcon />
+        </div>
+        <t-dropdown-menu>
+          <t-dropdown-item @click="onShowIot"> 物联网平台 </t-dropdown-item>
+          <t-dropdown-item @click="addSql"> sql数据源 </t-dropdown-item>
+          <t-dropdown-item @click="addNetwork('mqtt')"> MQTT </t-dropdown-item>
+          <t-dropdown-item @click="addNetwork('websocket')"> Websocket </t-dropdown-item>
+          <t-dropdown-item @click="addNetwork('http')"> HTTP </t-dropdown-item>
+        </t-dropdown-menu>
+      </t-dropdown>
+    </div>
+    <div class="flex between mt-8">
+      <t-tooltip content="可批量导入数据图元到画布" placement="top">
+        <t-checkbox v-model="data.checkAll" @change="onCheckAllChange"
+          >批量导入到画布</t-checkbox
+        >
+      </t-tooltip>
+      <!-- <t-tooltip content="开启全局数据模拟" placement="top">
+        <t-checkbox v-model="data.enableMock" @change="onChangeMock"
+          >开启</t-checkbox
+        >
+      </t-tooltip> -->
+    </div>
+
+    <div v-if="data.iotTree?.length">
+      <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
+        <div class="flex">
+          <ApplicationIcon class="tree-icon mt-8" />
+          <div class="ml-8">物联网平台</div>
+        </div>
+        <!-- <div>
+          <Edit2Icon  class="mr-12 hover" style="width: 14px;height: 14px;" @click="onShowIot" />
+        </div> -->
+      </div>
+      <!-- <div> -->
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.iotTree,'iot')"
+        @dragend="onAddShapeEnd"
+      >
+        <t-tree
+          :draggable="false"
+          v-model="allChecked"
+          class="ml-16"
+          style="overflow-y: hidden"
+          activeMultiple
+          :data="data.iotTree"
+          :expand-parent="true"
+          :checkable="data.checkAll"
+          :checkStrictly="false"
+        />
+      </div>
+      <!-- </div> -->
+      <div
+        id="dragElem-iot"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.iotTree.length"
+      >
+        <div v-for="device in data.iotTree">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in device.children"
+            v-show="allChecked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div v-if="data.sqls?.length">
+      <div class="flex mt-16" style="height: 32px">
+        <DataIcon class="tree-icon" />
+        <div class="ml-8">SQL数据源</div>
+      </div>
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.sqls,'sql')"
+        @dragend="onAddShapeEnd"
+      >
+      <t-tree
+        :draggable="false"
+        class="ml-16"
+        :key="sqlTreeKey"
+        hover
+        style="overflow-y: hidden"
+        activeMultiple
+        :data="data.sqls"
+        :expand-parent="true"
+        :checkable="data.checkAll"
+        :checkStrictly="false"
+      >
+        <template #operations="{ node }">
+          <tepmlate v-if="!node.getParent()">
+            <Edit2Icon
+              class="mr-12"
+              @click="editSql(node.data, node.getIndex())"
+            />
+            <DeleteIcon class="mr-8" @click="deleteSql(node.getIndex())" />
+          </tepmlate>
+        </template>
+      </t-tree>
+      </div>
+      <div
+        id="dragElem-sql"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.sqls.length"
+      >
+        <div v-for="item in data.sqls">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in item.children"
+            v-show="sqlsCheked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- <div v-if="data.networks?.length">
+      <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
+        <div class="flex">
+          <ControlPlatformIcon class="tree-icon mt-8" />
+          <div class="ml-8">Mqtt</div>
+        </div>
+        <div>
+        </div>
+      </div>
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.networks,'network')"
+        @dragend="onAddShapeEnd"
+      >
+      <t-tree
+        :draggable="false"
+        class="ml-16"
+        v-model="networksCheked"
+        ref="networkTree"
+        :key="networkTreeKey"
+        style="overflow-y: hidden"
+        activeMultiple
+        :data="data.networks"
+        :expand-parent="true"
+        :checkable="data.checkAll"
+        :checkStrictly="false"
+      >
+        <template #operations="{ node }">
+          <template v-if="node.getParent()">
+            <Edit2Icon
+              class="mr-12"
+              @click="showAddData(node.getParent().data, node)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteData(node.getParent().data, node.getIndex());
+                node.remove();
+              "
+            />
+          </template>
+          <template v-else>
+            <t-dropdown :minColumnWidth="168" :hide-after-item-click="false">
+              <AddIcon class="mr-12" />
+              <t-dropdown-menu>
+                <t-dropdown-item
+                  :value="2"
+                  :divider="true"
+                  @click="showAddData(node.data)"
+                >
+                  新建属性
+                </t-dropdown-item>
+                <t-dropdown-item :value="2" @click="importDataset(node.data)">
+                  从Excel导入
+                </t-dropdown-item>
+                <t-dropdown-item :value="4">
+                  <a
+                    :href="
+                      isDownload
+                        ? '/v/data.xlsx'
+                        : cdn
+                        ? cdn + '/v/data.xlsx?r=' + Math.random()
+                        : '/data.xlsx'
+                    "
+                    style="color: var(--td-text-color-primary)"
+                    @click.stop
+                  >
+                    下载Excel示例
+                  </a>
+                </t-dropdown-item>
+              </t-dropdown-menu>
+            </t-dropdown>
+            <Edit2Icon
+              class="mr-12"
+              @click="editNetwork(node.data, node.getIndex())"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteNetwork(node.getIndex());
+                node.remove();
+              "
+            />
+          </template>
+        </template>
+      </t-tree>
+      </div>
+      <div
+        id="dragElem-network"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.networks.length"
+      >
+        <div v-for="item in data.networks">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in item.children"
+            v-show="networksCheked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div> -->
+    <div v-if="data.mqtt_networks?.length">
+      <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
+        <div class="flex">
+          <ControlPlatformIcon class="tree-icon mt-8" />
+          <div class="ml-8">MQTT</div>
+        </div>
+        <div>
+        </div>
+      </div>
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.networks,'network')"
+        @dragend="onAddShapeEnd"
+      >
+      <t-tree
+        :draggable="false"
+        class="ml-16"
+        v-model="networksCheked"
+        ref="mqttTree"
+        :key="mqttTreeKey"
+        style="overflow-y: hidden"
+        activeMultiple
+        :data="data.mqtt_networks"
+        :expand-parent="true"
+        :checkable="data.checkAll"
+        :checkStrictly="false"
+      >
+        <template #operations="{ node }">
+          <template v-if="node.getParent()">
+            <Edit2Icon
+              class="mr-12"
+              @click="showAddData(node.getParent().data, node)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteData(node.getParent().data, node.getIndex());
+                node.remove();
+              "
+            />
+          </template>
+          <template v-else>
+            <t-dropdown :minColumnWidth="168" :hide-after-item-click="false">
+              <AddIcon class="mr-12" />
+              <t-dropdown-menu>
+                <t-dropdown-item
+                  :value="2"
+                  :divider="true"
+                  @click="showAddData(node.data)"
+                >
+                  新建属性
+                </t-dropdown-item>
+                <t-dropdown-item :value="2" @click="importDataset(node.data)">
+                  从Excel导入
+                </t-dropdown-item>
+                <t-dropdown-item :value="4">
+                  <a
+                    :href="
+                      isDownload
+                        ? '/v/data.xlsx'
+                        : cdn
+                        ? cdn + '/v/data.xlsx?r=' + Math.random()
+                        : '/data.xlsx'
+                    "
+                    style="color: var(--td-text-color-primary)"
+                    @click.stop
+                  >
+                    下载Excel示例
+                  </a>
+                </t-dropdown-item>
+              </t-dropdown-menu>
+            </t-dropdown>
+            <Edit2Icon
+              class="mr-12"
+              @click="editNetwork(node.data, node.data.tem_index)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteNetwork(node.data.tem_index);
+                node.remove();
+              "
+            />
+          </template>
+        </template>
+      </t-tree>
+      </div>
+      <div
+        id="dragElem-network"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.networks.length"
+      >
+        <div v-for="item in data.networks">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in item.children"
+            v-show="networksCheked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div v-if="data.ws_networks?.length">
+      <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
+        <div class="flex">
+          <ControlPlatformIcon class="tree-icon mt-8" />
+          <div class="ml-8">Websocket</div>
+        </div>
+        <div>
+        </div>
+      </div>
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.networks,'network')"
+        @dragend="onAddShapeEnd"
+      >
+      <t-tree
+        :draggable="false"
+        class="ml-16"
+        v-model="networksCheked"
+        ref="wsTree"
+        :key="wsTreeKey"
+        style="overflow-y: hidden"
+        activeMultiple
+        :data="data.ws_networks"
+        :expand-parent="true"
+        :checkable="data.checkAll"
+        :checkStrictly="false"
+      >
+        <template #operations="{ node }">
+          <template v-if="node.getParent()">
+            <Edit2Icon
+              class="mr-12"
+              @click="showAddData(node.getParent().data, node)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteData(node.getParent().data, node.getIndex());
+                node.remove();
+              "
+            />
+          </template>
+          <template v-else>
+            <t-dropdown :minColumnWidth="168" :hide-after-item-click="false">
+              <AddIcon class="mr-12" />
+              <t-dropdown-menu>
+                <t-dropdown-item
+                  :value="2"
+                  :divider="true"
+                  @click="showAddData(node.data)"
+                >
+                  新建属性
+                </t-dropdown-item>
+                <t-dropdown-item :value="2" @click="importDataset(node.data)">
+                  从Excel导入
+                </t-dropdown-item>
+                <t-dropdown-item :value="4">
+                  <a
+                    :href="
+                      isDownload
+                        ? '/v/data.xlsx'
+                        : cdn
+                        ? cdn + '/v/data.xlsx?r=' + Math.random()
+                        : '/data.xlsx'
+                    "
+                    style="color: var(--td-text-color-primary)"
+                    @click.stop
+                  >
+                    下载Excel示例
+                  </a>
+                </t-dropdown-item>
+              </t-dropdown-menu>
+            </t-dropdown>
+            <Edit2Icon
+              class="mr-12"
+              @click="editNetwork(node.data, node.data.tem_index)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteNetwork(node.data.tem_index);
+                node.remove();
+              "
+            />
+          </template>
+        </template>
+      </t-tree>
+      </div>
+      <div
+        id="dragElem-network"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.networks.length"
+      >
+        <div v-for="item in data.networks">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in item.children"
+            v-show="networksCheked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div v-if="data.http_networks?.length">
+      <div class="flex mt-16 between" style="height: 32px; line-height: 32px">
+        <div class="flex">
+          <ControlPlatformIcon class="tree-icon mt-8" />
+          <div class="ml-8">HTTP</div>
+        </div>
+        <div>
+        </div>
+      </div>
+      <div
+        :draggable="data.checkAll ? true : false"
+        @dragstart="onAddShape($event, data.networks,'network')"
+        @dragend="onAddShapeEnd"
+      >
+      <t-tree
+        :draggable="false"
+        class="ml-16"
+        v-model="networksCheked"
+        ref="httpTree"
+        :key="httpTreeKey"
+        style="overflow-y: hidden"
+        activeMultiple
+        :data="data.http_networks"
+        :expand-parent="true"
+        :checkable="data.checkAll"
+        :checkStrictly="false"
+      >
+        <template #operations="{ node }">
+          <template v-if="node.getParent()">
+            <Edit2Icon
+              class="mr-12"
+              @click="showAddData(node.getParent().data, node)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteData(node.getParent().data, node.getIndex());
+                node.remove();
+              "
+            />
+          </template>
+          <template v-else>
+            <t-dropdown :minColumnWidth="168" :hide-after-item-click="false">
+              <AddIcon class="mr-12" />
+              <t-dropdown-menu>
+                <t-dropdown-item
+                  :value="2"
+                  :divider="true"
+                  @click="showAddData(node.data)"
+                >
+                  新建属性
+                </t-dropdown-item>
+                <t-dropdown-item :value="2" @click="importDataset(node.data)">
+                  从Excel导入
+                </t-dropdown-item>
+                <t-dropdown-item :value="4">
+                  <a
+                    :href="
+                      isDownload
+                        ? '/v/data.xlsx'
+                        : cdn
+                        ? cdn + '/v/data.xlsx?r=' + Math.random()
+                        : '/data.xlsx'
+                    "
+                    style="color: var(--td-text-color-primary)"
+                    @click.stop
+                  >
+                    下载Excel示例
+                  </a>
+                </t-dropdown-item>
+              </t-dropdown-menu>
+            </t-dropdown>
+            <Edit2Icon
+              class="mr-12"
+              @click="editNetwork(node.data, node.data.tem_index)"
+            />
+            <DeleteIcon
+              class="mr-8"
+              @click="
+                deleteNetwork(node.data.tem_index);
+                node.remove();
+              "
+            />
+          </template>
+        </template>
+      </t-tree>
+      </div>
+      <div
+        id="dragElem-network"
+        style="position: absolute; left: 0; z-index: -999"
+        v-if="data.networks.length"
+      >
+        <div v-for="item in data.networks">
+          <div
+            class="flex mt-4"
+            v-for="(prop, i) in item.children"
+            v-show="networksCheked.includes(prop.value)"
+          >
+            <div style="width: 100px; text-align: end">{{ prop.label }}:</div>
+            <div class="ml-4">{{ prop.mock }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      class="flex column middle nodata"
+      v-if="!data.iotTree.length && !data.sqls.length && !data.networks.length"
+    >
+      <img src="/img/no-data.png" />
+      <div class="gray center">暂无数据</div>
+      <!-- <div class="mt-20">
+        <t-button theme="primary" @click="addNetwork()">
+          添加数据源
+        </t-button>
+      </div> -->
+    </div>
+  </div>
+  <div class="content" v-if="group === '解析'">
+    <div class="flex between">
+      <div class="title">数据{{ group }}</div>
+    </div>
+    <div class="mt-8">
+      <CodeEditor
+        :key="data.randomkey"
+        v-model="data.socketCbJs"
+        style="min-height: 300px"
+        @change="onSocketCbJsChange"
+      />
+      <div class="data-full-icon hover">
+        <Fullscreen2Icon @click="showDataTransformation" />
+      </div>
+    </div>
+    <div class="mt-16">
+      参考文档:
+      <a
+        target="_blank"
+        href="https://doc.le5le.com/document/136233394#%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F"
+      >
+        解析自定义格式数据</a
+      >
+    </div>
+  </div>
+
+  <t-dialog
+    v-if="addDataDialog.show"
+    :visible="true"
+    class="data-dialog"
+    :header="addDataDialog.header"
+    @close="addDataDialog.show = false"
+    @confirm="onOkAddData"
+  >
+    <!-- <div class="form-item mt-16">
+        <label>设备</label>
+        <t-input v-model="addDataDialog.data.device" placeholder="设备名称" />
+      </div> -->
+    <div class="form-item mt-16">
+      <label>显示名称</label>
+      <t-input
+        @change="changeDataLabel($event)"
+        :value="addDataDialog.data.label"
+        placeholder="属性简短描述"
+      />
+    </div>
+    <div class="form-item mt-16">
+      <label>属性名</label>
+      <t-input
+        @change="changeDataID($event)"
+        :value="addDataDialog.data.id"
+        placeholder="属性名"
+      />
+    </div>
+    <!-- <div class="form-item mt-16">
+      <label>类型</label>
+      <t-select
+        class="w-full"
+        :options="typeOptions"
+        v-model="addDataDialog.data.type"
+        placeholder="字符串"
+        @change="addDataDialog.data.value = null"
+      />
+    </div>
+    <div class="form-item mt-16">
+      <label
+        >值范围
+        <t-tooltip content="可用做数据模拟" placement="top">
+          <HelpCircleIcon style="font-size: 12px" />
+        </t-tooltip>
+      </label>
+      <div class="w-full">
+        <t-input v-model="addDataDialog.data.mock" placeholder="值范围" />
+        <h6 class="desc mt-8" style="font-size: 12px">值范围说明</h6>
+        <ul class="desc ml-16" style="font-size: 12px">
+          <li>
+            <label class="inline" style="width: 80px">固定值:</label>
+            直接填写,例如:10
+          </li>
+          <li>
+            <label class="inline" style="width: 80px">随机值:</label>
+            值1,值2,...。例如:1,2,3,4,5
+          </li>
+          <li>
+            <label class="inline" style="width: 80px">范围数字:</label>
+            最小值-最大值。例如:0-1.0 或 0-100
+          </li>
+          <li>
+            <label class="inline" style="width: 80px">随机字符串:</label>
+            [长度]。例如:[8]
+          </li>
+        </ul>
+      </div>
+    </div> -->
+  </t-dialog>
+  <t-dialog
+    v-if="networkDialog.show"
+    :visible="true"
+    width="800px"
+    class="data-dialog"
+    :header="networkDialog.header"
+    @close="networkDialog.show = false"
+    @confirm="onOkNetwork"
+  >
+    <template #footer>
+      <div class="flex mr-8" style="justify-content: end">
+        <!-- <t-checkbox v-model="networkDialog.save" class="mr-12">
+          同时保存到我的数据源
+        </t-checkbox> -->
+        <t-button @click="onOkNetwork">确定</t-button>
+      </div>
+    </template>
+    <div style="max-height: 450px; padding: 8px; overflow-y: auto">
+      <Net v-model="networkDialog.network" />
+    </div>
+  </t-dialog>
+  <t-dialog
+    v-if="dataTransformationDialog.show"
+    :visible="true"
+    header="数据监听"
+    @confirm="onOkDataTransformation"
+    @close="dataTransformationDialog.show = false"
+    :width="800"
+  >
+    <CodeEditor v-model="dataTransformationDialog.data" style="height: 300px" />
+    <div class="mt-8">
+      参考文档:
+      <a
+        target="_blank"
+        href="https://doc.le5le.com/document/136233394#%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F"
+      >
+        解析自定义格式数据</a
+      >
+    </div>
+  </t-dialog>
+  <t-dialog
+    v-if="sqlDialog.show"
+    :visible="true"
+    width="800px"
+    class="data-dialog"
+    :header="sqlDialog.header"
+    @close="sqlDialog.show = false"
+    @confirm="onOkSql"
+  >
+    <div style="max-height: 450px; padding: 8px">
+      <div class="form-item mt-8">
+        <label>sql数据源</label>
+        <t-select v-model="sqlDialog.sql.dbid" placeholder="请选择数据源">
+          <t-option
+            v-for="sql in sqlList"
+            @click="sqlChange(sql)"
+            :key="sql.id"
+            :value="sql.id"
+            :label="sql.name + '(' + sql.dbType + ')'"
+          />
+        </t-select>
+      </div>
+      <div class="form-item mt-8">
+        <label>sql轮询间隔</label>
+        <t-input-number
+          theme="column"
+          v-model="sqlDialog.sql.interval"
+          placeholder="不填,仅初始执行一次"
+        />
+      </div>
+      <div class="form-item mt-8">
+        <label>查询方式</label>
+        <t-select v-model="sqlDialog.sql.method">
+          <t-option key="get" value="get" label="单条" />
+          <t-option key="list" value="list" label="列表" />
+        </t-select>
+      </div>
+      <div class="form-item mt-8">
+        <label>sql语句</label>
+        <CodeEditor
+          :json="false"
+          :language="'sql'"
+          v-model="sqlDialog.sql.sql"
+          class="mt-4"
+          style="height: 100px"
+        />
+      </div>
+      <div v-if="sqlDialog.sql.method === 'list'" class="form-item mt-8">
+        <label>第几页</label>
+        <t-input-number
+          v-model="sqlDialog.sql.current"
+          theme="normal"
+          :min="1"
+          placeholder="默认1"
+        />
+      </div>
+      <div v-if="sqlDialog.sql.method === 'list'" class="form-item mt-8">
+        <label>每页数量</label>
+        <t-input-number
+          v-model="sqlDialog.sql.pageSize"
+          theme="normal"
+          placeholder="默认20"
+          :min="1"
+        />
+      </div>
+      <div class="form-item mt-8">
+        <label>关联属性名</label>
+        <t-input v-model="sqlDialog.sql.bindId" placeholder="关联属性名" />
+      </div>
+      <div class="flex mt-8">
+        <!-- <label> -->
+        <t-button style="width: 75px" @click="sqlTest">连接测试</t-button>
+        <!-- </label> -->
+        <p class="ml-8" style="width: 700px">{{ sqlDialog.result }}</p>
+      </div>
+    </div>
+  </t-dialog>
+  <t-dialog
+    v-if="iotDialog.show"
+    :visible="true"
+    width="472px"
+    class="data-dialog"
+    :header="iotDialog.header"
+    @close="iotDialog.show = false"
+    @confirm="onOkIot"
+  >
+    <!-- <t-input
+      v-model="iotSearch"
+      @change="onSearchIot"
+      @enter="onSearchIot"
+      placeholder="设备属性搜索"
+    /> -->
+    <div class="input-search" style="padding: 0px 4px">
+      <div class="btn" style="left: 14px">
+        <img src="/img/icon_search_gray.svg" />
+      </div>
+      <t-input
+        style="height: 32px"
+        v-model="iotSearch"
+        @change="onSearchIot"
+        @enter="onSearchIot"
+        placeholder="搜索设备属性"
+      />
+    </div>
+    <div style="height: 320px; margin-top: 8px; overflow-y: scroll">
+      <t-tree
+        style="overflow-y: hidden"
+        activeMultiple
+        v-model="checkedIots"
+        :data="iots"
+        :expand-parent="true"
+        :checkable="true"
+        :checkStrictly="false"
+        allow-fold-node-on-filter
+        :filter="iotFilter"
+        :scroll="{
+          // rowHeight: 34,
+          bufferSize: 10,
+          threshold: 10,
+          type: 'virtual',
+        }"
+      />
+    </div>
+  </t-dialog>
+</template>
+
+<script lang="ts" setup>
+import { reactive, defineComponent, ref, onMounted, toRaw, watch } from 'vue';
+import {
+  FileImportIcon,
+  FileExportIcon,
+  DeleteIcon,
+  AddIcon,
+  AddCircleIcon,
+  MinusCircleIcon,
+  Edit2Icon,
+  FileExcelIcon,
+  CloudDownloadIcon,
+  SearchIcon,
+  Fullscreen2Icon,
+  FullscreenExit1Icon,
+  HelpCircleIcon,
+  CaretDownSmallIcon,
+  CaretRightSmallIcon,
+  RouterWaveIcon,
+  ArrowUpDown3Icon,
+  Download1Icon,
+  ApplicationIcon,
+  DataIcon,
+  ControlPlatformIcon,
+} from 'tdesign-icons-vue-next';
+import { typeOptions } from '@/services/common';
+import { MessagePlugin } from 'tdesign-vue-next';
+import { Pen, deepClone } from '@meta2d/core';
+import { useDot } from '@/services/common';
+import { isDownload } from '@/services/defaults';
+import { cdn } from '@/services/api';
+import { importExcel, saveAsExcel } from '@/services/excel';
+import axios from 'axios';
+import { transformData } from '@/services/utils';
+import { debounce } from '@/services/debouce';
+import Net from '@/views/components/Net.vue';
+import CodeEditor from '@/views/components/common/CodeEditor.vue';
+import { s8 } from '@/services/random';
+import { useUser } from '@/services/user';
+import {
+  getSqlSourceList,
+  getDevices,
+  getDeviceProperties,
+  getMqttUrl,
+  doSqlCode,
+} from '@/services/iot';
+import { tree } from '@2d-components/domForm';
+
+const props = defineProps<{
+  group: string;
+}>();
+
+const { user } = useUser();
+
+const { dot, setDot } = useDot();
+
+const data = reactive({
+  //列表
+  datesetBak: {},
+  datasetId: '',
+  networks: [],
+  http_networks:[],
+  ws_networks:[],
+  mqtt_networks:[],
+  datasetList: [],
+  dataset: {
+    name: '',
+    id: '',
+    url: '',
+    devices: [],
+  } as any,
+  checkAll: false,
+  //获取
+  networkList: [],
+  input: '',
+  popupVisible: false,
+  networkId: '',
+
+  //监听
+  socketCbJs: '',
+  full: false,
+
+  dataMocks: [
+    // {
+    //   name: '',
+    //   id: '',
+    //   enableMock: true,
+    //   mock: 'aaa',
+    //   type: 'string',
+    //   expend: true,
+    //   show: false,
+    // },
+  ],
+  enableMock: false,
+  randomkey: s8(),
+  sqls: [],
+  iotList: [],
+  iotTree: [],
+});
+
+onMounted(() => {
+  meta2d.store.data.networks?.forEach((network:any)=>
+  { 
+    if(!network.label){
+      network.label = network.name;
+    }
+    if(!network.value){
+      network.value = s8();
+    }
+    network.checkable = false
+  });
+  data.networks = meta2d.store.data.networks || [];
+  data.dataset = (meta2d.store.data as any).dataset || {};
+  if( data.networks?.length&&data.dataset?.devices){
+    data.networks[0].children = deepClone(data.dataset.devices);
+  }
+  data.socketCbJs = meta2d.store.data.socketCbJs || '';
+  // data.dataMocks = meta2d.store.data.dataMocks || [];
+  data.enableMock = meta2d.store.data.enableMock || false;
+  // getNetworks();
+  // getDatasets();
+  // getIot();
+  data.sqls = meta2d.store.data.sqls || [];
+  data.iotList = meta2d.store.data.iot?.list || [];
+
+  data.iotTree = meta2d.store.data.iot?.tree || [];
+  getThirdNetwork();
+});
+
+const getThirdNetwork = (key?:string)=>{
+  if(!meta2d.store.data.networks?.length){
+    return;
+  }
+  meta2d.store.data.networks.forEach((item:any,index)=>{
+    item.tem_index = index;
+  });
+  if(!key||key==='http'){
+    data.http_networks = meta2d.store.data.networks.filter((item)=>item.protocol==='http') || [];  
+  }
+  if(!key||key==='websocket'){
+    data.ws_networks = meta2d.store.data.networks.filter((item)=>item.protocol==='websocket') || [];
+  }
+  if(!key||key==='mqtt'){
+    data.mqtt_networks = meta2d.store.data.networks.filter((item)=>item.protocol==='mqtt') || [];
+  }
+}
+
+const iotDialog = ref({
+  show: false,
+  header: '添加物联网平台',
+});
+
+const onOkIot = () => {
+  let _iots = [];
+  iots.value.forEach((item) => {
+    if (checkedIots.value.includes(item.value)) {
+      _iots.push(deepClone(item));
+    } else {
+      if (item.children?.length) {
+        const child = item.children.filter((child) =>
+          checkedIots.value.includes(child.value)
+        );
+        if (child.length) {
+          _iots.push({
+            label: item.label,
+            value: item.value,
+            deviceId: item.deviceId, //item.id
+            token: item.token,
+            children: deepClone(child),
+          });
+        }
+      }
+    }
+  });
+  data.iotTree = _iots;
+  if (!meta2d.store.data.iot) {
+    meta2d.store.data.iot = {};
+  }
+  meta2d.store.data.iot.tree = _iots;
+  iotDialog.value.show = false;
+};
+
+const onShowIot = async () => {
+  await getIotTree();
+  iotDialog.value.show = true;
+};
+
+const iots = ref([]);
+
+const getIotTree = async () => {
+  if (iots.value.length) {
+    return;
+  }
+  let ret = await getDevices();
+  const type = ret.type;
+  const list = ret.list;
+  for (let i = 0; i < list.length; i++) {
+    const item = list[i];
+    item.label = item.name;
+    item.value = item.id;
+
+    item.deviceId = item.id; //item.id
+    item.token = item.token;
+    // item.children = true;
+    // item.checkable = false;
+    let properties = await getDeviceProperties(item.id);
+    item.children = properties.map((prop: any) => {
+      return {
+        label: prop.name,
+        value: type?prop.key:item.deviceId + '#' + prop.key,
+        _label: item.name + '#' + prop.name,
+        token: item.token,
+        class: 'iot',
+      };
+    });
+    if (!item.children.length) {
+      item.checkable = false;
+    }
+  }
+  // setTimeout(()=>{
+  iots.value = deepClone(list);
+  // },3000);
+};
+
+const iotSearch = ref('');
+const iotFilter = ref(null);
+const onSearchIot = () => {
+  iotFilter.value = iotSearch.value
+    ? (node) =>
+        node.data.label.indexOf(iotSearch.value) >= 0 ||
+        node.data.value.indexOf(iotSearch.value) >= 0
+    : null;
+};
+
+const checkedIots = ref([]);
+
+const onShowSql = async () => {
+  addSql();
+};
+const iotLoad = async (node) => {
+  let properties = await getDeviceProperties(node.value);
+  return properties.map((item: any) => {
+    item.label = item.name;
+    item.value = node.data.deviceId + '#' + item.key;
+    item._label = node.data.name + '#' + item.name;
+    // item.sn = item.sn;
+    item.token = node.data.token;
+    return item;
+  });
+};
+
+const doBind = (node) => {
+};
+
+const iotMqtt = ref({});
+const iotProtocol = ref('');
+const getIot = async () => {
+  let canmqtt: any = await getMqttUrl();
+  if (canmqtt && canmqtt.host) {
+    iotMqtt.value = canmqtt;
+  }
+
+  if (meta2d.store.data.iot) {
+    iotProtocol.value = meta2d.store.data.iot.protocol;
+  }
+};
+
+const selectIot = (protocol: any) => {
+  if (iotProtocol.value === protocol) {
+    meta2d.store.data.iot = {};
+    iotProtocol.value = '';
+  } else {
+    if (protocol === 'mqtt') {
+      meta2d.store.data.iot = {
+        protocol: protocol,
+        ...iotMqtt.value,
+      };
+    } else {
+      meta2d.store.data.iot = {
+        protocol: protocol,
+        // host: iotMqtt.value.host
+      };
+    }
+    iotProtocol.value = protocol;
+  }
+  meta2d.connectNetwork();
+};
+
+const addSql = async () => {
+  sqlList.value = await getSqlSourceList();
+  sqlDialog.header = '添加sql数据源';
+  sqlDialog.show = true;
+  sqlDialog.edit = false;
+  sqlDialog.sql = {
+    interval: undefined,
+    sql: '-- eg:SELECT * FROM "directory"',
+  };
+  sqlDialog.result = '';
+};
+
+const editSql = (sql: any, index: number) => {
+  sqlDialog.header = '编辑sql数据源';
+  sqlDialog.show = true;
+  sqlDialog.edit = true;
+  sqlDialog.index = index;
+  sqlDialog.sql = deepClone(sql);
+};
+
+const deleteSql = (index: number) => {
+  data.sqls.splice(index, 1);
+  (meta2d.store.data as any).sqls = data.sqls;
+  sqlTreeKey.value = s8();
+  setDot(true);
+};
+const sqlTreeKey = ref(s8());
+const onOkSql = async () => {
+  if (!sqlDialog.sql.dbid) {
+    MessagePlugin.error('请选择数据源');
+    return;
+  }
+  if (!sqlDialog.sql.sql) {
+    MessagePlugin.error('请填写sql语句');
+    return;
+  }
+  if (!sqlDialog.sql.interval) {
+    sqlDialog.sql.interval = undefined;
+  }
+  if (!sqlDialog.sql.bindId) {
+    MessagePlugin.error('关联属性名必填');
+    return;
+  }
+  sqlDialog.sql.label = sqlDialog.sql.bindId;
+  sqlDialog.sql.value = sqlDialog.sql.bindId;
+  // let sql = sqlDialog.value.sql+' LIMIT '+(sqlDialog.value.pageSize||20)+(sqlDialog.value.current>1?(' OFFSET '+(sqlDialog.value.current-1)*sqlDialog.value.pageSize):'');
+
+  if (!sqlDialog.sql.children) {
+    await sqlTest();
+    sqlTreeKey.value = s8();
+  }
+  if (!sqlDialog.edit) {
+    data.sqls.push(sqlDialog.sql);
+    // meta2d.store.data.sqls = data.sqls;
+  } else {
+    data.sqls[sqlDialog.index] = sqlDialog.sql;
+  }
+  meta2d.store.data.sqls = toRaw(data.sqls);
+  sqlTreeKey.value = s8();
+  // data.sqls = deepClone(data.sqls);
+  meta2d.connectNetwork();
+  sqlDialog.show = false;
+};
+
+const sqlDialog = reactive<any>({
+  show: false,
+  edit: false,
+  index: -1,
+  header: '添加sql数据源',
+  sql: {
+    interval: undefined,
+    sql: '',
+  },
+});
+const sqlList = ref([]);
+
+const sqlChange = (sql: any) => {
+  sqlDialog.sql.dbid = sql.id;
+  sqlDialog.sql.dbType = sql.dbType;
+  sqlDialog.sql.name = sql.name;
+};
+
+const sqlTest = async () => {
+  let ret: any = await doSqlCode(sqlDialog.sql);
+  if (ret.error) {
+    MessagePlugin.error('连接错误:' + ret.error);
+    sqlDialog.result = '连接错误:' + ret.error;
+  } else {
+    if (sqlDialog.sql.method === 'list') {
+      sqlDialog.result = '连接成功:[' + JSON.stringify(ret[0]) + ',...]';
+      // sqlDialog.sql.columns = ret[0];
+      const columnsKeys = Object.keys(ret[0]);
+      const children = new Array(ret.length).fill(0).map((item, index) => {
+        return {
+          label: index + 1 + '',
+          value: sqlDialog.sql.bindId + '#' + index,
+          _label: sqlDialog.sql.bindId + '#' + index + 1,
+          class: 'sql',
+          children: columnsKeys.map((key) => {
+            return {
+              label: key,
+              value: sqlDialog.sql.bindId + '#' + index + '#' + key,
+              _label: sqlDialog.sql.bindId + '#' + (index + 1) + '#' + key,
+              class: 'sql',
+            };
+          }),
+        };
+      });
+      sqlDialog.sql.class = 'sql';
+      sqlDialog.sql.children = children;
+    } else {
+      sqlDialog.result = '连接成功:' + JSON.stringify(ret);
+      // sqlDialog.sql.columns = ret;
+      const children = [];
+      for (let key in ret) {
+        children.push({
+          label: key,
+          value: sqlDialog.sql.bindId + '#' + key,
+          _label: sqlDialog.sql.bindId + '#' + key,
+          class: 'sql',
+        });
+      }
+      sqlDialog.sql.class = 'sql';
+      sqlDialog.sql.children = children;
+    }
+  }
+};
+
+const addDataDialog = reactive<any>({});
+
+const showAddData = (network: any, node?: any) => {
+  addDataDialog.network = network;
+  const row = node?.data;
+  const index = node?.getIndex();
+  addDataDialog.node = node;
+  if (row) {
+    addDataDialog.header = '编辑数据';
+    addDataDialog.data = JSON.parse(JSON.stringify(row));
+    addDataDialog.index = index;
+  } else {
+    addDataDialog.header = '添加数据';
+    addDataDialog.data = { type: 'string', expend: true };
+  }
+
+  addDataDialog.show = true;
+};
+
+const deleteData = (network: any, index: number) => {
+  network.children.splice(index, 1);
+  meta2d.store.data.networks = toRaw(data.networks);
+  // data.dataset.devices.splice(row, 1);
+  // (meta2d.store.data as any).dataset = data.dataset;
+  setDot(true);
+};
+
+const clearData = () => {
+  data.datasetId = undefined;
+  data.dataset.id = undefined;
+  data.dataset.name = undefined;
+  data.dataset.url = undefined;
+  data.dataset.devices = [];
+  setDot(true);
+};
+
+const changeDataLabel = (value) => {
+  if (!value) {
+    MessagePlugin.error('显示名称不能为空!');
+    return;
+  }
+  let item = data.dataset.devices?.filter((item) => item.label === value);
+  if (item && item.length) {
+    MessagePlugin.error('显示名称重复!');
+    return;
+  }
+  addDataDialog.data.label = value;
+};
+
+const changeDataID = (value) => {
+  if (!value) {
+    MessagePlugin.error('属性名不能为空!');
+    return;
+  }
+  let item = data.dataset.devices?.filter((item) => item.id === value);
+  if (item && item.length) {
+    MessagePlugin.error('属性名重复!');
+    return;
+  }
+  addDataDialog.data.id = value;
+};
+
+const onOkAddData = () => {
+  if (!addDataDialog.data.label) {
+    MessagePlugin.error('请填写名称');
+    return;
+  }
+  if (!addDataDialog.data.id) {
+    MessagePlugin.error('请填写数据ID');
+    return;
+  }
+  if (!addDataDialog.network.children) {
+    addDataDialog.network.children = [];
+  }
+  addDataDialog.data.value = addDataDialog.data.id;
+  if (addDataDialog.header === '添加数据') {
+    addDataDialog.network.children.push(addDataDialog.data);
+    if(addDataDialog.network.protocol === 'http'){
+      httpTree.value.appendTo(addDataDialog.network.value, addDataDialog.data);
+    }else if(addDataDialog.network.protocol === 'websocket'){
+      wsTree.value.appendTo(addDataDialog.network.value, addDataDialog.data);
+    }else if(addDataDialog.network.protocol === 'mqtt'){
+      mqttTree.value.appendTo(addDataDialog.network.value, addDataDialog.data);
+    }
+  } else {
+    // addDataDialog.network.children[addDataDialog.index] = addDataDialog.data;
+    addDataDialog.network.children.splice(
+      addDataDialog.index,
+      1,
+      addDataDialog.data
+    );
+    addDataDialog.node?.insertAfter(deepClone(addDataDialog.data));
+    addDataDialog.node?.remove();
+    // mqttTreeKey.value = s8();
+    // wsTreeKey.value = s8();
+    // httpTreeKey.value = s8();
+    // networkTree.value.setItem(addDataDialog.network.value, {children:deepClone(addDataDialog.network.children)});
+    // addDataDialog.node?.setData(deepClone(addDataDialog.data));
+    //更新所有绑定该id的pen label
+    // let binds = meta2d.store.bind[addDataDialog.data.id];
+    // if (binds) {
+    //   binds.forEach((item) => {
+    //     const pen: Pen = meta2d.findOne(item.id);
+    //     pen.realTimes &&
+    //       pen.realTimes.forEach((_realTime) => {
+    //         if (_realTime.key === item.key) {
+    //           _realTime.bind.label = addDataDialog.data.label;
+    //         }
+    //       });
+    //   });
+    // }
+  }
+  // (meta2d.store.data as any).dataset = data.dataset;
+  meta2d.store.data.networks = toRaw(data.networks);
+  addDataDialog.show = false;
+};
+
+const importDataset = async (network) => {
+  let columns: any = [
+    {
+      header: '设备',
+      key: 'device',
+    },
+    {
+      header: '显示名称',
+      key: 'label',
+    },
+    {
+      header: '属性名',
+      key: 'id',
+    },
+    {
+      header: '类型',
+      key: 'type',
+    },
+    {
+      header: '值范围',
+      key: 'mock',
+    },
+  ];
+  const _data: any = await importExcel(columns);
+  _data.forEach((item) => {
+    if (item.device) {
+      item.label = item.device + '-' + item.label;
+      delete item.device;
+    }
+    item.value = item.id;
+  });
+  if (!network.children) {
+    network.children = [];
+  }
+  mergeDataset(network.children, _data);
+  if(network.protocol === 'http'){
+    httpTree.value.appendTo(network.value, network.children);
+  }else if(network.protocol === 'websocket'){
+    wsTree.value.appendTo(network.value, network.children);
+  }else if(network.protocol === 'mqtt'){
+    mqttTree.value.appendTo(network.value, network.children);
+  }
+  // networkTree.value.appendTo(network.value, network.children);
+  // (meta2d.store.data as any).dataset = data.dataset;
+};
+
+const downloadAsExcel = () => {
+  if (!(data.dataset.devices && data.dataset.devices.length)) {
+    MessagePlugin.error('属性列表不能为空!');
+    return;
+  }
+  const name = meta2d.store.data.name;
+  const columns: any[] = [
+    {
+      key: 'label',
+      header: '显示名称',
+    },
+    {
+      key: 'id',
+      header: '属性名',
+    },
+    {
+      key: 'type',
+      header: '类型',
+    },
+    {
+      key: 'mock',
+      header: '值范围',
+    },
+  ];
+  saveAsExcel(name, columns, data.dataset.devices);
+};
+
+const downloadAsJson = () => {
+  if (!(data.dataset.devices && data.dataset.devices.length)) {
+    MessagePlugin.error('属性列表不能为空!');
+    return;
+  }
+  import('file-saver').then(({ saveAs }) => {
+    saveAs(
+      new Blob([JSON.stringify(data.dataset)], {
+        type: 'text/plain;charset=utf-8',
+      }),
+      `${data.dataset.name || '未命名'}.json`
+    );
+  });
+};
+
+const onOkDataset = async (saveas = false) => {
+  // if (!dataDialog.dataset.name) {
+  //   MessagePlugin.error('名称不能为空');
+  //   return;
+  // }
+  if (!(data.dataset.devices && data.dataset.devices.length)) {
+    MessagePlugin.error('属性列表不能为空');
+    return;
+  }
+  const dataset = JSON.parse(JSON.stringify(data.dataset));
+  let _data = dataset;
+  _data.type = 'dataset';
+  if (!_data.name) {
+    _data.name = meta2d.store.data.name;
+  }
+  _data = transformData(dataset, 'toNetwork');
+  // 保存到我的数据源
+  if (saveas || !dataset.id) {
+    delete dataset.id;
+    delete dataset._id;
+    dataset.type = 'dataset';
+    const ret: any = await axios.post(`/api/data/datasource/add`, _data);
+    if (!ret) {
+      return;
+    }
+    ret.id = ret.id || ret._id;
+    data.datasetId = ret.id;
+    dataset.id = ret.id;
+    data.dataset.id = ret.id;
+    data.datasetList.push(dataset);
+  } else {
+    const ret: any = await axios.post(`/api/data/datasource/update`, _data);
+    if (!ret) {
+      return;
+    }
+    data.datasetList.forEach((item: any, index: number) => {
+      if (
+        item.id === dataset.id ||
+        (item._id === dataset._id && dataset._id != undefined)
+      ) {
+        data.datasetList.splice(index, 1, dataset);
+      }
+    });
+  }
+  delete dataset.devices;
+  // @ts-ignore
+  // meta2d.store.data.dataset = dataset;
+
+  // setDot(true);
+
+  data.editDataset = false;
+  delete data.datesetBak;
+};
+
+const mergeDataset = (arr1: any, arr2: any[]) => {
+  if (!(arr2 && arr2.length)) {
+    return;
+  }
+  if (!arr1) {
+    arr1 = [];
+  }
+  arr2.forEach((item) => {
+    let index = arr1.findIndex((elem) => elem.id === item.id);
+    if (index >= 0) {
+      Object.assign(arr1[index], item);
+    } else {
+      arr1.push(item);
+    }
+  });
+};
+
+const getDatas = async () => {
+  if (!data.dataset.url) {
+    return;
+  }
+  const ret = await axios.get(data.dataset.url);
+  let flattenRet = flattenTree(ret);
+  if (flattenRet) {
+    mergeDataset(data.dataset, flattenRet);
+    (meta2d.store.data as any).dataset = data.dataset;
+  }
+};
+
+//展开树
+const flattenTree = (root) => {
+  if (!root) return []; // 空树返回空数组
+
+  const result = []; // 存储展开后的数组
+
+  // 递归遍历树
+  function dfs(node) {
+    result.push({
+      id: node.id,
+      label: node.label || node.name,
+      device: node.device || node.id,
+      type: node.type,
+      mock: node.mock,
+      // children: node.children,
+    }); // 将当前节点值添加到结果数组
+    // 遍历当前节点的子节点
+    if (node.children) {
+      for (let child of node.children) {
+        dfs(child); // 递归调用DFS
+      }
+    }
+  }
+  root.forEach((item) => {
+    dfs(item);
+  });
+  // dfs(root); // 从根节点开始遍历
+
+  return result;
+};
+
+const onSelDataset = async (datasetId = false) => {
+  if (datasetId) {
+    const dataset = data.datasetList.find((item: any) => {
+      return item.id === datasetId;
+    });
+
+    if (!dataset) {
+      return;
+    }
+
+    if (dataset.url) {
+      const ret = await axios.get(dataset.url);
+      if (ret) {
+        dataset.devices = ret;
+      }
+    } else {
+      const ret = await axios.post(`/api/data/datasource/get`, {
+        id: dataset.id,
+      });
+      if (ret?.data) {
+        Object.assign(dataset, { ...ret.data });
+      }
+    }
+    mergeDataset(data.dataset, dataset.devices);
+
+    (meta2d.store.data as any).dataset = data.dataset;
+    // dataDialog.dataset = dataset;
+
+    // if (!init) {
+    //   const d = JSON.parse(JSON.stringify(dataset));
+    //   delete d.devices;
+    //   // @ts-ignore
+    //   meta2d.store.data.dataset = d;
+
+    //   setDot(true);
+    // }
+  }
+};
+
+// 请求我的数据模型
+const getDatasets = async (name?: string) => {
+  if (!user.id) {
+    MessagePlugin.error('请先登录');
+    return;
+  }
+  const body: any = {
+    type: 'dataset',
+    projection: 'id,data,name,type',
+  };
+  if (name) {
+    body.name = name;
+  }
+  const ret: any = await axios.post(`/api/data/datasource/list`, body, {
+    params: {
+      current: 1,
+      pageSize: 10,
+    },
+  });
+  if (ret?.list) {
+    const list = [];
+    let found = false;
+    for (const item of ret.list) {
+      item.id = item.id || item._id;
+      list.push(transformData(item, 'toMetaNetwork'));
+      if (data.dataset?.id === item.id) {
+        found = true;
+      }
+    }
+    if (data.dataset?.id && !found) {
+      list.push(data.dataset);
+    }
+
+    data.datasetList = list;
+  }
+};
+
+const onDelDataset = async (item: any, i: number) => {
+  const ret: any = await axios.post(`/api/data/datasource/delete`, {
+    id: item.id,
+  });
+  if (
+    (meta2d.store.data as any).dataset &&
+    (meta2d.store.data as any).dataset.id === item.id
+  ) {
+    //@ts-ignore
+    meta2d.store.data.dataset = {};
+    data.dataset = {};
+    data.datasetId = undefined;
+  }
+  if (ret) {
+    data.datasetList.splice(i, 1);
+  }
+};
+
+const search = ref('');
+const onSearch = () => {
+  // if (!search.value) {
+  //   return;
+  // }
+  data.dataset.devices.forEach((item) => {
+    if (
+      item.label.indexOf(search.value) !== -1 ||
+      item.id.indexOf(search.value) !== -1
+    ) {
+      item.show = true;
+    } else {
+      item.show = false;
+    }
+  });
+};
+
+const onInputNetwork = (e) => {
+  debounce(getNetworks, 300, e);
+};
+
+const onSelectNetWork = (value) => {
+  if (!value) {
+    return;
+  }
+  const network: any = data.networks.find((elem: any) => value === elem.id);
+  if (!network) {
+    const item = data.networkList.find((elem: any) => value === elem.id);
+    data.networks.push(item);
+    meta2d.store.data.networks = toRaw(data.networks);
+    meta2d.connectNetwork();
+    setDot(true);
+  }
+  // data.input = null;
+  data.popupVisible = false;
+};
+
+// 请求我的实时数据
+const getNetworks = async (e?) => {
+  const ret: any = await axios.post(
+    `/api/data/datasource/list`,
+    {
+      // q: {
+      name: e,
+      // },
+      type: 'subscribe',
+      projection: 'id,data,name,type',
+    },
+    {
+      params: {
+        current: 1,
+        pageSize: 10,
+      },
+    }
+  );
+  if (ret?.list) {
+    const list = [];
+    for (const item of ret.list) {
+      item.id = item.id || item._id;
+      list.push(transformData(item, 'toMetaNetwork'));
+    }
+    data.networkList = list;
+  }
+};
+
+const onDelNetWork = async (item: any, i: number) => {
+  const ret: any = await axios.post(`/api/data/datasource/delete`, {
+    id: item.id || item._id,
+  });
+  if (ret) {
+    data.networkList.splice(i, 1);
+  }
+};
+
+const networkTree = ref();
+const networkTreeKey = ref(s8());
+const mqttTree = ref();
+const mqttTreeKey = ref(s8());
+const wsTree = ref();
+const wsTreeKey = ref(s8());
+const httpTree = ref();
+const httpTreeKey = ref(s8());
+
+const deleteNetwork = (index: number) => {
+  data.networks.splice(index, 1);
+  meta2d.store.data.networks = toRaw(data.networks);
+  getThirdNetwork('xxx');
+  meta2d.connectNetwork();
+  setDot(true);
+};
+
+const networkDialog = reactive<any>({
+  save: true,
+});
+const editNetwork = (network: any, index: number) => {
+  networkDialog.network = JSON.parse(JSON.stringify(data.networks[index]));
+  networkDialog.editNetwork = 2;
+  networkDialog.editNetworkIndex = index;
+  networkDialog.header = `编辑${networkDialog.network.protocol}数据源`;
+  networkDialog.show = true;
+};
+
+const addNetwork = (protocol:string) => {
+  networkDialog.network = {
+    name: '',
+    type: 'subscribe',
+    protocol,
+    url: '',
+    options: {
+      clientId: '',
+      username: '',
+      password: '',
+      customClientId: false,
+    },
+  };
+  networkDialog.editNetwork = 1;
+  networkDialog.header = `添加${protocol}数据源`;
+  networkDialog.show = true;
+};
+
+const onOkNetwork = async () => {
+  const _data = transformData(networkDialog.network, 'toNetwork');
+  networkDialog.network.label = networkDialog.network.name;
+  // networkDialog.network.value = networkDialog.network.url;
+  networkDialog.network.value = s8();
+  networkDialog.network.checkable = false;
+  if (networkDialog.editNetwork === 1) {
+    if (
+      ['mqtt', 'websocket', 'http'].includes(networkDialog.network.protocol) &&
+      !networkDialog.network.url
+    ) {
+      MessagePlugin.error('URL地址不能为空!');
+      return;
+    }
+    if (!networkDialog.network.name) {
+      MessagePlugin.error('名称不能为空!');
+      return;
+    }
+    // if (networkDialog.save) {
+    //   const ret: any = await axios.post(`/api/data/datasource/add`, _data);
+    //   if (!ret) {
+    //     return;
+    //   }
+    //   ret.id = ret.id || ret._id;
+    //   networkDialog.network.id = ret.id;
+    // }
+    data.networks.push(networkDialog.network);
+    // getThirdNetwork(networkDialog.network.protocol);
+    // data.networkList.push(networkDialog.network);
+    // networkTree.value?.appendTo('', networkDialog.network);
+  } else if (networkDialog.editNetwork === 2) {
+    // if (networkDialog.save) {
+    //   const ret: any = await axios.post(`/api/data/datasource/update`, _data);
+    //   if (!ret) {
+    //     return;
+    //   }
+    // }
+    //替换
+    let index = networkDialog.editNetworkIndex;
+    if (index !== undefined) {
+      data.networks.splice(index, 1, networkDialog.network);
+      // getThirdNetwork(networkDialog.network.protocol);
+      // networkTreeKey.value = s8();
+      if(networkDialog.network.protocol==='http'){
+        httpTreeKey.value = s8();
+      }else if(networkDialog.network.protocol==='websocket'){
+        wsTreeKey.value = s8();
+      }else if(networkDialog.network.protocol==='mqtt'){
+        mqttTreeKey.value = s8();
+      }
+      // networkTree.value.setItem(networkDialog.network.value,networkDialog.network);
+    }
+  }
+  networkDialog.show = false;
+  networkDialog.editNetwork = 0;
+  meta2d.store.data.networks = toRaw(data.networks);
+  getThirdNetwork(networkDialog.network.protocol);
+  meta2d.connectNetwork();
+  setDot(true);
+};
+
+const dataTransformationDialog = reactive<any>({
+  show: false,
+  data: '',
+});
+
+const showDataTransformation = () => {
+  dataTransformationDialog.data = meta2d.store.data.socketCbJs;
+  dataTransformationDialog.show = true;
+};
+
+const onOkDataTransformation = () => {
+  meta2d.store.data.socketCbJs = dataTransformationDialog.data;
+  data.randomkey = s8();
+  data.socketCbJs = dataTransformationDialog.data;
+  meta2d.listenSocket();
+  dataTransformationDialog.show = false;
+};
+
+let timer: any = 0;
+
+const onSocketCbJsChange = (e) => {
+  clearTimeout(timer);
+  timer = setTimeout(() => {
+    meta2d.store.data.socketCbJs = data.socketCbJs;
+    meta2d.listenSocket();
+  }, 3000);
+};
+
+const onChangeMock = () => {
+  // @ts-ignore
+  data.enableMock = !data.enableMock;
+  meta2d.store.data.enableMock = data.enableMock;
+  if (data.enableMock) {
+    meta2d.startDataMock();
+  } else {
+    meta2d.stopDataMock();
+  }
+};
+
+const onCheckAllChange = (e) => {
+  if (!e) {
+    data.dataset?.devices?.forEach((item) => {
+      item.checked = false;
+    });
+  }
+};
+
+const allChecked = ref([]);
+const networksCheked = ref([]);
+const sqlsCheked = ref([]);
+const onAddShape = (e, _data,type) => {
+  e.stopPropagation();
+  let data: any;
+  if (Array.isArray(_data)) {
+    const dragElem = document.getElementById(`dragElem-${type}`);
+    let width = dragElem.offsetWidth;
+    e.dataTransfer.setDragImage(dragElem, width / 2, 10);
+    const checked = [];
+    if(type === 'iot'){
+      _data.forEach((item) => {
+        item.children.forEach((_item) => {
+          if (allChecked.value.includes(_item.value)) {
+            checked.push(_item);
+          }
+        });
+      });
+    }else if(type === 'network'){
+      _data.forEach((item) => {
+        item.children.forEach((_item) => {
+          if (networksCheked.value.includes(_item.value)) {
+            checked.push(_item);
+          }
+        });
+      });
+    }else if(type === 'sql'){
+      _data.forEach((item) => {
+        item.children.forEach((_item) => {
+          if (sqlsCheked.value.includes(_item.value)) {
+            checked.push(_item);
+          }
+        });
+      });
+    }
+    // const checked = _data.filter((item) => item.checked);
+    if (!checked.length) {
+      MessagePlugin.error('请先选择数据');
+      return;
+    }
+    data = [];
+    checked.forEach((item) => {
+      let bind:any = {
+        class: 'iot',
+        id: item.value,
+        label: item._label,
+        token: item.token,
+      };
+      if(type === 'network'){
+        bind = {
+          id: item.id||item.value,
+          label: item.label,
+        }
+      }else if(type === 'sql'){
+        //TODO
+        bind = {
+          id: item.id||item.value,
+          label: item.label,
+        }
+      }
+      if (globalThis.style1) {
+        data.push(
+          ...[
+            {
+              text: item.label,
+              width: 150,
+              height: 20,
+              name: 'text',
+              dataset: true,
+              textAlign: 'right',
+            },
+            {
+              text: item.mock || 0,
+              width: 58,
+              height: 28,
+              name: 'rectangle',
+              dataset: true,
+              // textAlign: 'left',
+              color: '#478BFFFF',
+              textColor: '#A9C9FFFF',
+              background: '#478BFF1F',
+              fontWeight: 'bold',
+              borderRadius: 0.1,
+              lineWidth: 1,
+              realTimes: [
+                {
+                  label: '文字',
+                  key: 'text',
+                  type: 'string',
+                  bind: deepClone(item),
+                },
+              ],
+            },
+          ]
+        );
+      } else {
+        data.push(
+          ...[
+            {
+              text: item.label + ':',
+              width: 150,
+              height: 28,
+              name: 'text',
+              dataset: true,
+              textAlign: 'right',
+            },
+            {
+              text: item.mock,
+              width: 58,
+              height: 20,
+              name: 'text',
+              dataset: true,
+              textAlign: 'left',
+              realTimes: [
+                {
+                  label: '文字',
+                  key: 'text',
+                  type: 'string',
+                  bind: bind,
+                },
+              ],
+            },
+          ]
+        );
+      }
+    });
+  } else {
+    if (globalThis.style1) {
+      data = [
+        {
+          text: _data.label,
+          width: 150,
+          height: 28,
+          name: 'text',
+          dataset: true,
+          textAlign: 'right',
+        },
+        {
+          text: _data.mock || 0,
+          width: 58,
+          height: 28,
+          name: 'rectangle',
+          dataset: true,
+          // textAlign: 'left',
+          color: '#478BFFFF',
+          textColor: '#A9C9FFFF',
+          background: '#478BFF1F',
+          fontWeight: 'bold',
+          borderRadius: 0.1,
+          lineWidth: 1,
+          realTimes: [
+            {
+              label: '文字',
+              key: 'text',
+              type: 'string',
+              bind: deepClone(_data),
+            },
+          ],
+        },
+      ];
+    } else {
+      data = [
+        {
+          text: _data.label + ':',
+          width: 150,
+          height: 20,
+          name: 'text',
+          dataset: true,
+          textAlign: 'right',
+        },
+        {
+          text: _data.mock,
+          width: 58,
+          height: 20,
+          name: 'text',
+          dataset: true,
+          textAlign: 'left',
+          realTimes: [
+            {
+              label: '文字',
+              key: 'text',
+              type: 'string',
+              bind: deepClone(_data),
+            },
+          ],
+        },
+      ];
+    }
+  }
+  meta2d.canvas.addCaches = deepClone(data);
+};
+
+const onAddShapeEnd = () => {
+  setTimeout(() => {
+    meta2d.initBinds();
+  }, 1000);
+};
+
+let lastIndex = -1;
+
+const onCheckChange = (e, i) => {
+  if (lastIndex > -1) {
+    if (e.shiftKey) {
+      let start = Math.min(i, lastIndex);
+      let end = Math.max(i, lastIndex);
+      data.dataset?.devices?.forEach((item, index) => {
+        if (index >= start && index <= end) {
+          item.checked = true;
+        }
+      });
+      e.stopPropagation();
+      e.preventDefault();
+    }
+  }
+  lastIndex = i;
+};
+
+const checkChange = (e, device, i) => {
+  data.checkAll && (device.checked = !device.checked);
+  if (lastIndex > -1) {
+    if (e.shiftKey) {
+      let start = Math.min(i, lastIndex);
+      let end = Math.max(i, lastIndex);
+      data.dataset?.devices?.forEach((item, index) => {
+        if (index >= start && index <= end) {
+          item.checked = true;
+        }
+      });
+    }
+  }
+  lastIndex = i;
+};
+
+const onAddmock = (item: any) => {
+  if (data.dataMocks.find((elem) => elem.id === item.id)) {
+    return;
+  }
+  item.name = item.label;
+  data.dataMocks.push(deepClone(item));
+  (meta2d.store.data as any).dataMocks = data.dataMocks;
+  meta2d.startDataMock();
+};
+
+const onAddAllMock = () => {
+  if (!data.dataset.devices) {
+    return;
+  }
+  data.dataset.devices.forEach((item) => {
+    if (data.dataMocks.find((elem) => elem.id === item.id)) {
+      return;
+    }
+    item.name = item.label;
+    data.dataMocks.push(deepClone(item));
+  });
+  (meta2d.store.data as any).dataMocks = data.dataMocks;
+  meta2d.startDataMock();
+};
+
+const deleteMock = (index: number) => {
+  data.dataMocks.splice(index, 1);
+  (meta2d.store.data as any).dataMocks = data.dataMocks;
+  setDot(true);
+};
+</script>
+<style lang="postcss" scoped>
+/* :deep(.data-input-box){
+  position: absolute;
+    left: 193px;
+    width: 501px;
+    top: 80px;
+} */
+.content {
+  background: var(--color-background-active);
+  padding: 16px;
+
+  .tree-icon {
+    width: 14px;
+    height: 14px;
+  }
+
+  .prop-title {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  :deep(.t-collapse) {
+    border: none;
+  }
+
+  :deep(.t-collapse-panel__header) {
+    border: none;
+    font-size: 12px;
+    font-weight: 400;
+    padding: 8px 0px;
+    background: var(--color-background-active);
+  }
+  :deep(.t-collapse-panel__body) {
+    border: none;
+  }
+  :deep(.t-collapse-panel__content) {
+    background-color: var(--color-background-active);
+    color: var(--color);
+    padding: 8px 0px;
+  }
+
+  .btn-select {
+    height: 32px;
+    line-height: 32px;
+    color: #fff;
+    border: 1px solid var(--color-border);
+    border-radius: 4px;
+    padding: 0px 8px;
+    background-color: var(--color-background-input);
+    cursor: pointer;
+  }
+
+  .btn-select-active {
+    background-color: var(--color-primary);
+  }
+
+  .input-search {
+    flex-shrink: 0;
+    color: #878f9c;
+    padding: 0px;
+    margin-top: 10px;
+
+    .btn {
+      background: #fff0;
+      left: 12px;
+
+      img {
+        background-color: #fff0;
+        margin-top: 9px;
+      }
+    }
+
+    :deep(.t-select) {
+      .t-input {
+        padding-left: 8px !important;
+      }
+    }
+  }
+
+  .select-search {
+    .t-input {
+      padding-left: 8px !important;
+
+      .t-icon {
+        font-size: 12px;
+      }
+    }
+  }
+
+  .title {
+    height: 32px;
+    line-height: 32px;
+  }
+
+  .title-span {
+    display: inline-block;
+    margin-left: 4px;
+    max-width: 108px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .data-list {
+    /* padding: 12px 6px; */
+    /* background: var(--color-background-active); */
+    max-height: calc(100vh - 200px);
+    margin-bottom: 24px;
+    overflow-y: auto;
+
+    .data-title {
+      /* padding: 4px; */
+    }
+
+    .data-body {
+      margin-bottom: 8px;
+    }
+
+    .data-title-hover {
+      &:hover {
+        background: var(--color-background-input);
+        border-radius: 4px;
+      }
+    }
+
+    width: 234px;
+
+    & > div {
+      width: 218px;
+    }
+  }
+
+  .data-mock-list {
+    height: auto;
+    max-height: calc(100vh - 220px);
+  }
+
+  .icon-box {
+    width: 24px;
+    height: 24px;
+    margin: 4px;
+    text-align: center;
+    line-height: 24px;
+    border-radius: 4px;
+
+    &:hover {
+      background: var(--td-brand-color-light);
+    }
+
+    .t-icon {
+      width: 14px;
+      height: 14px;
+    }
+  }
+
+  .icon-item-box {
+    height: 30px;
+    line-height: 30px;
+    margin: 1px;
+  }
+
+  .nodata {
+    padding-top: 50px;
+
+    img {
+      width: 80px;
+    }
+
+    .gray {
+      margin-top: 8px;
+    }
+
+    .t-button {
+      height: 24px;
+      padding-left: 8px;
+      padding-right: 8px;
+    }
+  }
+
+  .form-line {
+    margin-left: 12px;
+    width: 11px;
+    /* height: 120px; */
+    /* border-bottom: 1px solid var(--color-background-input); */
+    /* border-left: 1px solid var(--color-background-input); */
+  }
+
+  .form-data-item {
+    margin-left: 4px;
+    margin-top: 4px;
+
+    /* margin-right: 12px; */
+    label {
+      /* color: #ffffff80; */
+      height: 24px;
+      line-height: 24px;
+      width: 60px;
+    }
+
+    & > div {
+      width: 118px;
+      height: 24px;
+      line-height: 24px;
+      /* background: var(--color-background-input); */
+      border-radius: 4px;
+      /* color: #e3e8f459; */
+      padding-left: 8px;
+      padding-right: 8px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .form-mock-item {
+    & > div {
+      padding: 0px;
+
+      :deep(.t-input) {
+        height: 24px !important;
+        border: 0px;
+      }
+    }
+  }
+
+  .form-swicth-item {
+    margin-left: 4px;
+
+    label {
+      /* color: #ffffff80; */
+      height: 24px;
+      width: 56px;
+    }
+  }
+
+  .data-code {
+    width: 218px;
+
+    .code-editor {
+      height: 126px;
+    }
+  }
+
+  .data-code-full {
+    position: absolute;
+    /* right: calc(50% - 250px); */
+    width: 100%;
+    height: 100vh;
+    background: var(--td-mask-active);
+    right: 0px;
+    top: 0px;
+
+    .code-editor {
+      width: 800px;
+      margin: 0 auto;
+      height: 80%;
+      margin-top: 16vh;
+    }
+
+    .data-full-icon {
+      .t-icon {
+        margin-right: calc(50% - 384px);
+      }
+    }
+  }
+
+  .data-full-icon {
+    margin-top: -20px;
+    /* z-index: 10000; */
+    position: relative;
+    display: flex;
+    justify-content: flex-end;
+    width: 100%;
+
+    .t-icon {
+      margin-right: 16px;
+      cursor: pointer;
+      height: 16px;
+      width: 16px;
+    }
+  }
+
+  ul {
+    list-style-type: none;
+  }
+}
+
+:deep(.t-tree) {
+  .t-tree__label {
+    font-size: 12px;
+    color: #bdc7db;
+  }
+  .t-tree__item[data-level='1'] {
+    padding: 0 0 0 16px;
+    .t-checkbox__label {
+      max-width: 50px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+  .t-tree__item[data-level='2'] {
+    padding: 0 0 0 16px;
+    .t-checkbox__label {
+      max-width: 50px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .t-tree__item .t-icon {
+    color: #bdc7db;
+  }
+  .t-tree__label.t-is-checked {
+    background: none;
+  }
+}
+</style>
+<style lang="postcss">
+.menu-item-a {
+  &:hover {
+    color: var(--td-brand-color);
+  }
+}
+
+.network-option {
+  height: 100%;
+  padding: 0px;
+}
+</style>

+ 322 - 0
src/views/components/Net.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="network-component">
+    <div class="form-item mt-8">
+      <label>
+        数据源名称
+      </label>
+      <t-select-input
+        v-if="mode"
+        v-model:inputValue="modelValue.name"
+        :value="modelValue.name"
+        placeholder="我的数据发送"
+        allow-input
+        clearable
+        v-model:popup-visible="popupVisible"
+        @focus="popupVisible = true"
+        @blur="popupVisible = false"
+        @input-change="onInput"
+        @clear="
+          modelValue.id = undefined;
+          modelValue._id = undefined;
+        "
+      >
+        <template #panel>
+          <ul style="padding: 4px">
+            <li
+              class="hover-background item"
+              style="line-height: 1.5; padding: 8px; border-radius: 2px"
+              v-for="(item, i) in networkList"
+              :key="item.url"
+              @click="() => onSelect(item)"
+            >
+              名称: {{ item.name }}
+              <div class="desc">地址: {{ item.url }}</div>
+
+              <span class="del" @click.stop="onDelNetWork(item, i)">
+                <delete-icon />
+                <!-- <t-icon name="delete" /> -->
+              </span>
+            </li>
+            <li
+              v-if="networkList.length >= 10"
+              style="line-height: 1.5; padding: 8px; border-radius: 2px"
+              :key="-1"
+            >
+              <div class="desc">...</div>
+            </li>
+            <li
+              v-if="!networkList.length"
+              style="line-height: 1.5; padding: 8px; border-radius: 2px"
+              :key="-1"
+            >
+              <div class="desc">暂无数据</div>
+            </li>
+          </ul>
+        </template>
+      </t-select-input>
+      <t-input v-else v-model="modelValue.name" placeholder="名称" />
+    </div>
+
+    <!-- <div class="form-item mt-8">
+      <label>通信方式</label>
+      <t-select
+        v-model="modelValue.protocol"
+        placeholder="MQTT"
+        @change="protocolChange"
+      >
+        <t-option key="mqtt" value="mqtt" label="MQTT" />
+        <t-option key="websocket" value="websocket" label="Websocket" />
+        <t-option key="http" value="http" label="HTTP" />
+      </t-select>
+    </div> -->
+    <div class="form-item mt-8">
+      <label>URL地址</label>
+      <t-input
+        :format="urlFormat"
+        :placeholder="
+          modelValue.protocol !== 'http'
+            ? isSafeProtocol()
+              ? '必须是wss协议'
+              : '必须是ws协议'
+            : '请输入'
+        "
+        v-model="modelValue.url"
+      />
+    </div>
+    <template v-if="modelValue.protocol === 'websocket'">
+      <div class="form-item mt-8">
+        <label>protocols</label>
+        <t-input v-model="modelValue.options.protocols" />
+      </div>
+    </template>
+    <template v-else-if="modelValue.protocol === 'http'">
+      <div class="form-item mt-8">
+        <label>请求方式</label>
+        <t-select v-model="modelValue.method" @change="httpMethodChange">
+          <t-option key="GET" value="GET" label="GET" />
+          <t-option key="POST" value="POST" label="POST" />
+        </t-select>
+      </div>
+      <div v-if="modelValue.type === 'subscribe'" class="form-item mt-8">
+        <label>请求间隔</label>
+        <t-input-number
+          theme="column"
+          v-model="modelValue.interval"
+          placeholder="默认1000 ms"
+        />
+      </div>
+      <div class="form-item mt-8">
+        <label>请求头</label>
+        <!-- <t-textarea
+          v-model="modelValue.headers"
+          :autosize="{ minRows: 3, maxRows: 5 }"
+          placeholder="请输入"
+        /> -->
+        <CodeEditor
+          :json="true"
+          :language="'json'"
+          v-model="modelValue.headers"
+          class="mt-4"
+          style="height: 50px"
+        />
+      </div>
+      <div class="form-item mt-8 desc">
+        <label></label>
+        支持设置动态参数,例如:{"Authorization": "Bearer ${token}"}
+      </div>
+      <div v-if="!mode && modelValue.method === 'POST'" class="form-item mt-8">
+        <label>请求体</label>
+        <!-- <t-textarea
+          v-model="modelValue.body"
+          :autosize="{ minRows: 3, maxRows: 5 }"
+          placeholder="请输入"
+        /> -->
+        <CodeEditor
+          :json="true"
+          :language="'json'"
+          v-model="modelValue.body"
+          class="mt-4"
+          style="height: 50px"
+        />
+      </div>
+      <div
+        v-if="!mode && modelValue.method === 'POST'"
+        class="form-item mt-8 desc"
+      >
+        <label></label>
+        支持设置动态参数,例如:{"value": "${value}"}
+      </div>
+    </template>
+    <template v-else>
+      <div class="form-item mt-8">
+        <label>Client Id</label>
+        <t-input v-model="modelValue.options.clientId" />
+      </div>
+      <div class="form-item mt-8">
+        <label>自动生成</label>
+        <t-switch
+          class="mt-8 ml-8"
+          v-model="modelValue.options.customClientId"
+          size="small"
+        />
+      </div>
+      <div class="form-item mt-8">
+        <label>用户名</label>
+        <t-input v-model="modelValue.options.username" />
+      </div>
+      <div class="form-item mt-8">
+        <label>密码</label>
+        <t-input v-model="modelValue.options.password" />
+      </div>
+      <div class="form-item mt-8">
+        <label>Topics</label>
+        <t-input v-model="modelValue.topics" />
+      </div>
+    </template>
+    <div class="form-item mt-8" v-if="mode">
+      <label> </label>
+      <div>
+        <t-button @click="onSave">保存到我的数据发送</t-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onBeforeMount, ref } from 'vue';
+import axios from 'axios';
+import { debounce } from '@/services/debouce';
+import { MessagePlugin } from 'tdesign-vue-next';
+import CodeEditor from '@/views/components/common/CodeEditor.vue';
+import { DeleteIcon } from 'tdesign-icons-vue-next';
+import { transformData } from '@/services/utils';
+
+const { modelValue, mode } = defineProps<{
+  modelValue: any;
+  mode?: any;
+}>();
+
+const emit = defineEmits(['update:modelValue', 'change']);
+const popupVisible = ref<boolean>(false);
+const networkList = ref<any[]>([]);
+
+onBeforeMount(() => {
+  modelValue.id = modelValue.id || modelValue._id;
+  if (mode) {
+    getNetworks();
+  }
+});
+
+const protocolChange = (protocol: string) => {
+  if (protocol === 'http') {
+    Object.assign(modelValue, {
+      httpTimeInterval: 1000,
+      headers: '',
+      method: 'GET',
+      body: '',
+    });
+  } else if (protocol === 'websocket') {
+    // modelValue.url = '';
+  } else {
+    Object.assign(modelValue, {
+      options: {
+        clientId: '',
+        username: '',
+        password: '',
+        customClientId: false,
+      },
+    });
+  }
+};
+
+const httpMethodChange = (method: string) => {
+  if (method === 'GET') {
+    modelValue.body = undefined;
+  }
+};
+
+const onSave = async () => {
+  emit('update:modelValue', modelValue);
+  emit('change', modelValue);
+
+  let ret: any;
+  const data = transformData(modelValue,'toNetwork');
+  // 保存到我的数据源
+  if (modelValue.id) {
+    ret = await axios.post(`/api/data/datasource/update`, data);
+  } else {
+    ret = await axios.post(`/api/data/datasource/add`, data);
+  }
+
+  if (ret) {
+    MessagePlugin.success('保存成功!');
+  }
+};
+
+const onInput = (text: string) => {
+  debounce(getNetworks, 300);
+};
+
+// 请求我的数据源接口
+const getNetworks = async () => {
+  const body: any = {
+    type: modelValue.type,
+    projection: "id,data,name,type",
+  };
+  if (modelValue.name) {
+    body.name = modelValue.name;
+  }
+  const ret: any = await axios.post(`/api/data/datasource/list`, body, {
+    params: {
+      current: 1,
+      pageSize: 10,
+    },
+  });
+  if (ret?.list) {
+    const list = [];
+    for (const item of ret.list) {
+      item.id = item.id || item._id;
+      list.push(transformData(item,'toMetaNetwork'));
+    }
+    networkList.value = list;
+  }
+};
+
+const onSelect = (item: any) => {
+  Object.assign(modelValue, item);
+  popupVisible.value = false;
+};
+
+const onDelNetWork = async (item: any, i: number) => {
+  const ret: any = await axios.post(`/api/data/datasource/delete`, {
+    id: item._id || item.id,
+  });
+  if (ret) {
+    networkList.value.splice(i, 1);
+  }
+};
+
+const isSafeProtocol = () => {
+  return document.location.protocol === 'https:' ? true : false;
+};
+
+const urlFormat = (val) => {
+  if (modelValue.protocol === 'mqtt') {
+    let reg = /^ws\:.*/;
+    if (isSafeProtocol()) {
+      reg = /^wss\:.*/;
+    }
+    if (reg.test(val)) {
+      return val;
+    } else {
+      return '';
+    }
+  } else {
+    return val;
+  }
+};
+</script>
+<style lang="postcss" scoped>
+.network-component {
+}
+</style>