Jelajahi Sumber

fix(router): 设备模拟 与cvs 树

wangxiao 2 tahun lalu
induk
melakukan
62399fcaeb

+ 6 - 6
config/proxy.ts

@@ -10,20 +10,20 @@ module.exports = {
       changeOrigin: true,
       pathRewrite: { '^/user': '' }
     },
-    '/cvss': {
-      target: 'http://124.222.113.37:8080',
+    '/cvs': {
+      target: 'http://127.0.0.1:8888',
       changeOrigin: true,
-      pathRewrite: { '^/cvss': '' }
+      pathRewrite: { '^/cvs': '' }
     },
     '/iot': {
       target: 'http://124.222.113.37:8888',
       changeOrigin: true,
       pathRewrite: { '^/iot': '' }
     },
-    '/dataSource': {
-      target: 'http://bynt9d.natappfree.cc',
+    '/datacenter': {
+      target: 'http://152.136.36.115:7777',
       changeOrigin: true,
-      pathRewrite: { '^/dataSource': '' }
+      pathRewrite: { '^/datacenter': '' }
     }
   }
 }

+ 68 - 0
src/api/cvs/orgtree.ts

@@ -0,0 +1,68 @@
+import request from '@/service/request'
+/**
+ * 此函数发送一个 POST 请求以创建一个具有指定标签和上层组 ID 的新设备组。
+ * @param data - `data` 参数是一个包含两个属性的对象:
+ * @returns `addDeviceGroup` 函数返回一个解析为字符串的 Promise。该字符串是在使用提供的“数据”对象向“/deviceGroup”端点发出 POST 请求后服务器的响应。
+ */
+export const addDeviceGroup = (data: { groupLabel: string, upperGroupId: string }) => {
+  return request<string>({
+    url: '/deviceGroup',
+    method: 'POST',
+    data
+  })
+}
+
+/**
+ * 此函数发送 GET 请求以根据上层组 ID 参数检索设备组列表。
+ * @param params - `params` 参数是一个包含 `upperGroupId` 属性的对象。此属性是一个字符串,表示用户要为其检索子组列表的上层设备组的 ID。
+ * @returns `listDeviceGroup` 函数返回一个解析为字符串的 Promise。该字符串是使用提供的 params 对象作为查询参数向 `/deviceGroup` 端点发出 GET
+ * 请求的响应。
+ */
+export const listDeviceGroup = (params: { upperGroupId: string }) => {
+  return request<string>({
+    url: '/deviceGroup',
+    method: 'GET',
+    params
+  })
+}
+
+/**
+ * 此函数发送 POST 请求以将设备绑定到设备组。
+ * @param data - `data` 参数是一个包含两个属性的对象:
+ * @returns 函数 postGroupBindDevice 返回一个解析为字符串的 Promise。该字符串是在使用提供的“数据”对象发出 POST 请求后来自 API
+ * 端点“/deviceGroup/device”的响应。
+ */
+export const postGroupBindDevice = (data: { deviceGroupId: string, deviceId: string }[]) => {
+  return request<string>({
+    url: '/deviceGroup/device',
+    method: 'POST',
+    data
+  })
+}
+/**
+ * 此函数发送 DELETE 请求以从设备组中删除设备。
+ * @param data - `data` 参数是一个包含两个属性的对象:
+ * @returns 解析为字符串的 Promise。
+ */
+export const delGroupBindDevice = (data: { deviceGroupId: string, deviceId: string }[]) => {
+  return request<string>({
+    url: '/deviceGroup/device',
+    method: 'DELETE',
+    data
+  })
+}
+
+export const getDeviceByGroup = (params: CVS.GroupQueryParams) => {
+  return request<CVS.device[]>({
+    url: '/device/group',
+    method: 'GET',
+    params
+  })
+}
+
+export const removeDeviceByGroup = (id: string) => {
+  return request<IOT.API.DEVICE.Device[]>({
+    url: '/deviceGroup/' + id,
+    method: 'DELETE'
+  })
+}

+ 7 - 0
src/api/cvs/video.ts

@@ -67,6 +67,13 @@ export const getDevicePage = (params: COMMON.API.QueryParams & {deviceName: stri
     params
   })
 }
+export const getDeviceGroupPage = (params: CVS.QueryParams) => {
+  return request<CVS.device[]>({
+    url: '/device/group',
+    method: 'GET',
+    params
+  })
+}
 
 // export const getDeviceList = (params: COMMON.API.QueryParams & {deviceName: string}) => {
 //   return request<CVS.device[]>({

+ 0 - 0
src/api/schedule/apiCenter.ts → src/api/datacenter/apiCenter.ts


+ 0 - 0
src/api/schedule/dataLake.ts → src/api/datacenter/dataLake.ts


+ 0 - 0
src/api/schedule/dataSource.ts → src/api/datacenter/dataSource.ts


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

@@ -249,6 +249,13 @@ export const getDeviceByGroup = (params: IOT.API.DEVICE.GroupQueryParams) => {
   })
 }
 
+export const removeDeviceByGroup = (id: string) => {
+  return request<IOT.API.DEVICE.Device[]>({
+    url: '/device/group/' + id,
+    method: 'DELETE'
+  })
+}
+
 /** 获取设备属性 */
 export const getDeviceAttribute = (deviceId: string) => {
   return request<IOT.API.DEVICE.Device[]>({
@@ -352,3 +359,11 @@ export const getDeviceLabelsByLabel = (deviceLabel: string) => {
     method: 'GET'
   })
 }
+
+export const deviceSimulator = (data: {deviceId: string, payload: string}) => {
+  return request<string>({
+    url: '/device/simulator',
+    method: 'POST',
+    data
+  })
+}

+ 39 - 0
src/controller/cvs/orgTree.ts

@@ -0,0 +1,39 @@
+
+import { addDeviceGroup, listDeviceGroup, postGroupBindDevice, delGroupBindDevice, getDeviceByGroup, removeDeviceByGroup } from '@/api/cvs/orgtree'
+import { message } from 'ant-design-vue'
+
+export class orgTree {
+  /** 设备分组 新增分组 */
+  static async postDeviceGroup (data: { groupLabel: string, upperGroupId: string }) {
+    const { code } = await addDeviceGroup(data)
+    if (code === 200) message.success('新增分组成功')
+  }
+
+  /** 设备分组 查询分组 */
+  static async listDeviceGroup (params: { upperGroupId: string }) {
+    return await listDeviceGroup(params)
+  }
+
+  static async getDeviceByGroup (params: IOT.API.DEVICE.GroupQueryParams) {
+    return await getDeviceByGroup(params)
+  }
+
+  /** 设备分组 设备绑定分组 */
+  static async postGroupBindDevice (data: { deviceGroupId: string, deviceId: string }[]) {
+    const { code } = await postGroupBindDevice(data)
+    if (code === 200) message.success('绑定成功')
+  }
+
+  static async removeDeviceGroup (params: string) {
+    return await removeDeviceByGroup(params)
+  }
+
+  /**
+   * 此函数删除设备和设备组之间的绑定并显示成功消息。
+   * @param data - 参数 `data` 是一个包含两个属性的对象:
+   */
+  static async delGroupBindDevice (data: { deviceGroupId: string, deviceId: string }[]) {
+    const { code } = await delGroupBindDevice(data)
+    if (code === 200) message.success('取消绑定成功')
+  }
+}

+ 30 - 2
src/controller/cvs/spaceController.ts

@@ -1,4 +1,24 @@
-import { addDevice, addSpace, delSpace, getDeviceAiRetById, getDeviceById, getDeviceChannel, getDevicePage, getDeviceRecordById, getDeviceThumbById, getGbPull, getNotGBPush, getNotGbPull, getSpace, getSpaceById, getSpaceConfigById, getSpaceList, updateSpace, updateStatus } from '@/api/cvs/video'
+import {
+  addDevice,
+  addSpace,
+  delSpace,
+  getDeviceAiRetById,
+  getDeviceById,
+  getDeviceChannel,
+  getDevicePage,
+  getDeviceRecordById,
+  getDeviceThumbById,
+  getGbPull,
+  getNotGBPush,
+  getNotGbPull,
+  getSpace,
+  getSpaceById,
+  getSpaceConfigById,
+  getSpaceList,
+  updateSpace,
+  updateStatus,
+  getDeviceGroupPage
+} from '@/api/cvs/video'
 import { message } from 'ant-design-vue'
 
 export class SpaceController {
@@ -20,7 +40,7 @@ export class SpaceController {
     ['OFFLINE', { key: 'OFFLINE', label: '离线', color: 'grey' }],
     ['ERROR', { key: 'ERROR', label: '异常', color: 'red' }],
     ['PUSHING', { key: 'PUSHING', label: '推流中', color: 'blue' }],
-    ['PULLING', { key: 'PULLING', label: '拉流中', color: 'yellow' }]
+    ['PULLING', { key: 'PULLING', label: '拉流中', color: 'blue' }]
   ])
 
   // 录像格式
@@ -30,6 +50,10 @@ export class SpaceController {
     return await getSpace(params)
   }
 
+  static async group (params: COMMON.API.QueryParams) {
+    return await getSpace(params)
+  }
+
   static async list () {
     const { code, data } = await getSpaceList('', 200)
     return data
@@ -70,6 +94,10 @@ export class SpaceController {
     return await getDevicePage(params)
   }
 
+  static async deviceGroupPage (params :CVS.QueryParams) {
+    return await getDeviceGroupPage(params)
+  }
+
   static async deviceById (id: string) {
     const { code, data } = await getDeviceById(id)
     if (code === 200) {

+ 1 - 1
src/controller/schedule/dataSource.ts → src/controller/datacenter/dataSource.ts

@@ -1,4 +1,4 @@
-import { addDataSource, dataSourceConnect, dataSourceConnectTest, delDataSource, getDataSourceById, getDataSourcePage, updateDataSource } from '@/api/schedule/dataSource'
+import { addDataSource, dataSourceConnect, dataSourceConnectTest, delDataSource, getDataSourceById, getDataSourcePage, updateDataSource } from '@/api/datacenter/dataSource'
 import { message } from 'ant-design-vue'
 
 export class DataSourceController {

+ 1 - 1
src/controller/index.ts

@@ -11,7 +11,7 @@ export { OtaController } from './iot/ota'
 
 export { UserController } from './user/index'
 
-export { DataSourceController } from './schedule/dataSource'
+export { DataSourceController } from './datacenter/dataSource'
 
 export { SpaceController } from './cvs/spaceController'
 

+ 45 - 4
src/controller/iot/device.ts

@@ -1,8 +1,41 @@
 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, getDeviceTopology, getDeviceSession, getDeviceAttributes, getDeviceSecret, getOtaByDeviceId, otaUpgradationRecordByDeviceId, upgradationOtaByDeviceId, liveById, liveControlRts, liveCustomRtsUrl, getDeviceLabelsByLabel
+  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,
+  getDeviceTopology,
+  getDeviceSession,
+  getDeviceAttributes,
+  getDeviceSecret,
+  getOtaByDeviceId,
+  otaUpgradationRecordByDeviceId,
+  upgradationOtaByDeviceId,
+  liveById,
+  liveControlRts,
+  liveCustomRtsUrl,
+  getDeviceLabelsByLabel,
+  removeDeviceByGroup, deviceSimulator
 } from '@/api/iot/device'
 import { DeviceMsgEnum, OtaStatusEnum } from '@/enum/common'
 import { message } from 'ant-design-vue'
@@ -209,6 +242,10 @@ export class DeviceContriller {
     return await listDeviceGroup(params)
   }
 
+  static async removeDeviceGroup (params: string) {
+    return await removeDeviceByGroup(params)
+  }
+
   /** 设备分组 设备绑定分组 */
   static async postGroupBindDevice (data: { deviceGroupId: string, deviceId: string }[]) {
     const { code } = await postGroupBindDevice(data)
@@ -319,4 +356,8 @@ export class DeviceContriller {
   static async labelsByLabel (deviceLabel: string) {
     return await getDeviceLabelsByLabel(deviceLabel)
   }
+
+  static async deviceSimulator (data: { deviceId: string, payload: string }) {
+    await deviceSimulator(data)
+  }
 }

+ 3 - 3
src/hooks/effect.ts

@@ -55,11 +55,11 @@ export const usePort = (title: string) => {
   if (title === '物联网') {
     setBaseUrl('/iot')
   } else if (title === '视联网') {
-    setBaseUrl('/cvss')
+    setBaseUrl('/cvs')
   } else if (title === '用户群组') {
     setBaseUrl('/user')
-  } else {
-    setBaseUrl('/user')
+  } else if (title === '数据中台') {
+    setBaseUrl('/datacenter')
   }
 }
 

+ 4 - 4
src/pages/Iot/device/group.vue

@@ -447,7 +447,7 @@ const delBind = async (ids: []) => {
   })
   await DeviceContriller.delGroupBindDevice(params)
   state.selectedRowKeys = []
-  getDeviceByGroup()
+  await getDeviceByGroup()
 }
 
 const bindDevice = async () => {
@@ -465,12 +465,12 @@ const bindDevice = async () => {
   getDeviceByGroup()
 }
 
-const ok = () => {
+const ok = async () => {
   if (state.groupOpra === 'add') {
     validate().then(async () => {
       await DeviceContriller.postDeviceGroup(groupState)
       state.groupModalVisible = false
-      getDeviceGroup()
+      await getDeviceGroup()
     })
   } else {
     if (state.selectTree.hasChildren) {
@@ -478,7 +478,7 @@ const ok = () => {
     } else {
       console.log('删除设备群组')
       state.groupModalVisible = false
-      // await DeviceContriller.dle
+      await DeviceContriller.removeDeviceGroup(state.treeActiveKey)
     }
   }
 }

+ 1 - 1
src/pages/cvs/dataServer/template.vue

@@ -84,7 +84,7 @@
             </a-radio-group>
         </a-form-item>
         <a-form-item label="图片" v-bind="validateInfos.url">
-            <UploadPro action="/cvss/file/upload" accept=".png,.jpg,.jpeg" @done="uploadDone" />
+            <UploadPro action="/cvs/file/upload" accept=".png,.jpg,.jpeg" @done="uploadDone" />
         </a-form-item>
     </a-form>
 </modal-pro>

+ 10 - 3
src/pages/cvs/edge/list.vue

@@ -7,8 +7,14 @@
     :hiddenMeunKeys="['add']"
   >
     <template #render="{column, record}" >
-      <template  v-if="column.key === 'state'" >
-        {{ record.state }}
+      <template v-if="column.key === 'clientId'" >
+        <a @click="getSys(record)">{{record.clientId}}</a>
+      </template>
+      <template v-if="column.key === 'state'" >
+        <div style="display: flex;align-items: center;" >
+          <div :style="{width: '8px',height: '8px',borderRadius: '50%', backgroundColor: (record.state==='ONLINE'?'green':'yellow')}" />
+          <div>{{record.state==='ONLINE'?'在线':'离线'}}</div>
+        </div>
       </template>
       <template  v-if="column.key === 'createAt'" >
         {{ dayjs(record.createAt).format('YYYY/MM/DD HH:mm:ss') }}
@@ -23,6 +29,7 @@
           <a @click="pushVideo(record)">视频</a>
           <a @click="reboot(record)">重启</a>
         </a-space>
+
       </template>
     </template>
   </table-pro>
@@ -47,7 +54,7 @@
 </a-card>
 </template>
 <script lang='ts' setup >
-import { AiboxController } from '@/controller'
+import { AiboxController, SpaceController } from '@/controller'
 import dayjs from 'dayjs'
 import { ref } from 'vue'
 import { useRouter } from 'vue-router'

+ 0 - 11
src/pages/cvs/edge/task.vue

@@ -14,17 +14,6 @@
       </a-space>
   </template>
   <template #extra><a-button type="primary" @click="refresh" >刷新任务</a-button></template>
-    <a-row>
-      <a-col :span="20" >
-        <a-descriptions title="设备信息" v-if="sys!.clientId">
-          <a-descriptions-item label="设备ID">{{sys?.devID}}</a-descriptions-item>
-          <a-descriptions-item label="系统版本">{{sys?.systemVersion}}</a-descriptions-item>
-          <a-descriptions-item label="软件版本">{{sys?.softVersion}}</a-descriptions-item>
-          <a-descriptions-item label="设备状态">{{sys?.devState}}</a-descriptions-item>
-        </a-descriptions>
-      </a-col>
-
-    </a-row>
     <a-table
       :columns="columns"
       :dataSource="taskList"

+ 0 - 12
src/pages/cvs/edge/video.vue

@@ -14,22 +14,10 @@
         </a-space>
     </template>
     <template #extra><a-button type="primary" @click="refresh" >刷新视频</a-button></template>
-      <a-row>
-        <a-col :span="20" >
-          <a-descriptions title="设备信息" v-if="sys!.clientId">
-            <a-descriptions-item label="设备ID">{{sys?.devID}}</a-descriptions-item>
-            <a-descriptions-item label="系统版本">{{sys?.systemVersion}}</a-descriptions-item>
-            <a-descriptions-item label="软件版本">{{sys?.softVersion}}</a-descriptions-item>
-            <a-descriptions-item label="设备状态">{{sys?.devState}}</a-descriptions-item>
-          </a-descriptions>
-        </a-col>
-
-      </a-row>
       <a-table
         :columns="columns"
         :dataSource="streamList"
       >
-
       </a-table>
     </a-card>
   </a-spin>

+ 3 - 0
src/pages/cvs/project/index.vue

@@ -16,6 +16,9 @@
       <template v-if="column.key === 'action'" >
         <a  @click="detail(record)" >详情</a>
       </template>
+      <template v-if="column.key === 'id'" >
+        <a  @click="detail(record)" >{{record.id}}</a>
+      </template>
     </template>
   </table-pro>
 </a-card>

+ 17 - 0
src/pages/cvs/video/device.vue

@@ -33,6 +33,23 @@
 
     <template #render="{column, record}" >
 
+      <template v-if="column.key === 'status'" >
+        <div style="display: flex;align-items: center;" >
+          <div :style="{width: '8px',height: '8px',borderRadius: '50%', backgroundColor:SpaceController.statusMap.get(record.status)?.color, marginRight: '5px' }" ></div>
+          <div>{{SpaceController.deviceStatusMap.get(record.status)?.label}}</div>
+        </div>
+      </template>
+
+      <template v-if="column.key === 'deviceId'">
+        <a @click="baseInfo(record)">{{record.deviceId}}</a>
+      </template>
+
+      <template v-if="column.key === 'type'">
+        <a-tag>{{SpaceController.type.find((item)=>{
+          return item.key == record.type
+        })?.label}}</a-tag>
+      </template>
+
       <template v-if="column.key === 'action'" >
         <a-space>
           <a @click="baseInfo(record)">基本信息</a>

+ 508 - 0
src/pages/cvs/video/orgtree.vue

@@ -0,0 +1,508 @@
+<template>
+  <a-card title="设备组织树">
+    <a-row :gutter="[8, 8]" >
+      <a-col :lg="5" :md="10" :xs="24">
+        <a-card style="height: 100%;" >
+          <template #title >
+            <a-row justify="space-between" align="middle" >
+              <a-col><a-button type="primary" @click="addDeviceGroup">+ 添加根设备群组</a-button></a-col>
+              <a-col><ReloadIconTsx :loading="state.groupLoading" :@reload="getDeviceGroup"/></a-col>
+            </a-row>
+          </template>
+          <a-spin :spinning="state.groupLoading" >
+            <a-tree
+              v-model:expandedKeys="expandedKeys"
+              :tree-data="state.groupTreeData"
+              defaultExpandAll
+              autoExpandParent
+              :field-names="{
+          key: 'id',
+          title: 'groupLabel',
+        }"
+            >
+              <template #title="record">
+                <a-space @click="changeTreeActiveKey(record.key)" >
+                  <div class="group-label" >   {{ record.groupLabel }} </div>
+                  <template v-if="state.treeActiveKey[0] === record.key && record.key" >
+                    <plus-circle-outlined @click="openGroupModal('add', record)"/>
+                    <delete-outlined @click="openGroupModal('del', record)"/>
+                  </template>
+                </a-space>
+              </template>
+            </a-tree>
+          </a-spin>
+        </a-card>
+      </a-col>
+      <a-col :lg="19" :md="14" :xs="24">
+        <a-card style="height: 100%;" >
+      <span v-if="!state.treeActiveKey[0]" >
+        <a-row justify="space-between" >
+          <a-col>
+            <a-space>
+              <a-select
+                allowClear
+                v-model:value="state.queryParams.searchKey"
+                style="width: 170px;"
+              >
+                <a-select-option
+                  v-for="item in searchKeys"
+                  :key="item.key"
+                  :value="item.key"
+                >
+                {{ item.name }}
+                </a-select-option>
+              </a-select>
+              <a-input-search placeholder="查询" allowClear     enter-button  @search="getDevicePage" ></a-input-search>
+            </a-space>
+          </a-col>
+          <a-col><ReloadIconTsx  :loading="state.loading" :reload="getDevicePage"/></a-col>
+        </a-row>
+          <a-table
+            style="margin-top: 20px;"
+            :loading="state.loading"
+            :columns="columns"
+            :data-source="state.groupDateSource"
+            :pagination="state.queryParams"
+            @change="changePage"
+          >
+        <template #bodyCell="{column, record}">
+          <template v-if="column.key === 'status'" >
+            <a-tag>{{record.status}}</a-tag>
+        </template>
+      <template v-if="column.key === 'type'">
+        <a-tag>{{SpaceController.type.find((item)=>{
+          return item.key == record.type
+        })?.label}}</a-tag>
+      </template>
+      <template v-if="column.key === 'deviceCount'">
+        <a @click="pushCvsDevicePage(record)" >{{record.deviceCount}}</a>
+      </template>
+        </template>
+        </a-table>
+      </span>
+          <span v-else  :key="selectGroup?.id">
+        <a-row  >
+        <a-col :span="24" >
+          <div style="font-size: 18px;font-weight: 500" >群组名称:{{state.selectGroup?.groupLabel}}</div>
+        </a-col>
+      </a-row>
+      <a-card
+        title="绑定设备"
+        :bordered="false"
+        :headStyle="{border: 'none', padding: '0px'}"
+      >
+        <a-row justify="end" >
+          <a-col>
+            <a-space>
+              <a-button @click="delBind([])" >批量解绑</a-button>
+              <a-button @click="state.drawerVisible = true" >绑定</a-button>
+              <a-button @click="getDeviceByGroup" > 刷新</a-button>
+            </a-space>
+          </a-col>
+        </a-row>
+        <div style="margin-bottom: 10px;" >已选择 {{state.selectedRowKeys.length}} 个设备, 一次最多可批量解绑100个设备。</div>
+        <a-table
+          :rowKey="record => record.deviceId"
+          :columns="groupDeviceColumns"
+          :loading='state.tableLoading'
+          :data-source="state.groupDeviceDataSource"
+          :row-selection="{
+            selectedRowKeys: state.selectedRowKeys,
+            onChange: onSelectChange,
+          }"
+        >
+          <template  #bodyCell="{column, record}" >
+            <template v-if="column.key === 'action'" >
+              <a-space>
+                <a >详情</a>
+                <a-popconfirm
+                  title="确实要取消绑定吗?"
+                  ok-text="确定"
+                  cancel-text="取消"
+                  @confirm="delBind(record.deviceId)"
+                >
+                  <a>解绑</a>
+                </a-popconfirm>
+              </a-space>
+            </template>
+          </template>
+        </a-table>
+      </a-card>
+      </span>
+        </a-card>
+      </a-col>
+    </a-row>
+    <!-- 绑定设备抽屉 -->
+    <a-drawer
+      v-model:open="state.drawerVisible"
+      size="large"
+      class="custom-class"
+      title="绑定设备"
+      placement="right"
+    >
+      <a-alert message="一个设备最多可以被添加到10个设备组中。" type="info" show-icon />
+      <a-row>
+        <a-col>
+          <a-space  style="margin-top: 20px;">
+            <a-select allowClear style="width: 100px;"  v-model:value="state.queryParams.searchKey">
+              <a-select-option v-for="item in searchKeys" :key="item.key" :value="item.key" >{{item.name}}</a-select-option>
+            </a-select>
+            <a-input  allowClear   v-model:value="state.queryParams.searchValue"  ></a-input>
+            <a-button @click="getDevicePage" > 搜索</a-button>
+          </a-space>
+        </a-col>
+        <a-col>
+          <a-table
+            :rowKey="record => record.deviceId"
+            style="margin-top: 10px;width: 688px;"
+            :loading="state.tableLoading"
+            :columns="groupDeviceColumns"
+            :data-source="state.groupDateSource"
+            :row-selection="{
+            selectedRowKeys: state.selectedRowKeys,
+            onChange: onSelectChange,
+          }"
+          >
+            <template #bodyCell="{column, record}">
+            </template>
+          </a-table>
+        </a-col>
+      </a-row>
+
+      <template #footer >
+        <a-row justify="end" >
+          <a-col>
+            <a-space>
+              <a-button type="primary" @click="bindDevice"> 绑定</a-button>
+              <a-button @click="state.drawerVisible = false" > 取消</a-button>
+            </a-space>
+          </a-col>
+        </a-row>
+      </template>
+    </a-drawer>
+
+    <!-- 添加根设备群组 -->
+    <modal-pro
+      :title="state.groupOpera === 'add' ? '添加设备群组' : '删除设备群组'"
+      :open="state.groupModalVisible"
+      @cancel="state.groupModalVisible = false"
+      @ok="ok"
+    >
+
+      <a-form v-if="state.groupOpera === 'add'" :label-col="{span: 6}" :wrapper-col="{span: 16}" >
+        <a-form-item label="父级群组名称" v-if="groupState.upperGroupId" >
+          <a-input allowClear disabled  :value="groupState.upperGroupLabel" ></a-input>
+        </a-form-item>
+        <a-form-item label="分组名"  v-bind="validateInfos.groupLabel" >
+          <a-input allowClear v-model:value="groupState.groupLabel" ></a-input>
+        </a-form-item>
+      </a-form>
+      <a-row v-else >
+        <a-col>
+          <a-alert style="width: 472px;" v-if="!state.selectTree.hasChildren " message="你确定要删除此群组吗?" type="info" show-icon />
+          <a-alert style="width: 472px;" v-else message="此群组下已创建子群组,不能直接被删除。如要继续,请先审视并删除子群组,再重试删除。" type="error" show-icon />
+        </a-col>
+      </a-row>
+    </modal-pro>
+  </a-card>
+</template>
+
+<script lang="ts" setup >
+import { ReloadIconTsx } from '@/components/MicroComponents'
+import { SpaceController } from '@/controller'
+import { computed, onMounted, reactive, ref } from 'vue'
+import { PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+import { Form } from 'ant-design-vue'
+import { useRouter } from 'vue-router'
+import { orgTree } from '@/controller/cvs/orgTree'
+
+const router = useRouter()
+
+const useForm = Form.useForm
+
+const columns = [
+  {
+    title: '设备ID',
+    dataIndex: 'deviceId',
+    key: 'deviceId'
+  },
+  {
+    title: '设备名称',
+    dataIndex: 'deviceName',
+    key: 'deviceName'
+  },
+  {
+    title: '空间类型',
+    dataIndex: 'type',
+    key: 'type'
+  },
+  {
+    title: '空间名称',
+    dataIndex: 'spaceName',
+    key: 'spaceName'
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    key: 'status'
+  }
+]
+
+const groupDeviceColumns = [
+  {
+    title: '设备ID',
+    dataIndex: 'deviceId',
+    key: 'deviceId'
+  },
+  {
+    title: '设备名称',
+    dataIndex: 'deviceName'
+  },
+  {
+    title: '所属空间',
+    dataIndex: 'spaceName'
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    key: 'action'
+  }
+]
+
+const searchKeys = [
+  { name: '设备ID', key: 'deviceId' },
+  { name: '设备名称', key: 'deviceName' }
+]
+
+interface TreeNode {
+  id: string;
+  createAt: number;
+  updateAt: number | null;
+  deleted: boolean;
+  groupLabel: string;
+  upperGroupId: string;
+  tenantId: string;
+  children?: TreeNode[];
+}
+
+function findParentNodeById (id: number, tree: any[]): any {
+  for (let i = 0; i < tree.length; i++) {
+    const node = tree[i]
+    if (node.id === id) {
+      return node
+    } else if (node.children) {
+      const parentNode = findParentNodeById(id, node.children)
+      if (parentNode) {
+        return parentNode
+      }
+    }
+  }
+  return null
+}
+
+function buildTree (data: TreeNode[], upperGroupId: string): TreeNode[] {
+  const tree: TreeNode[] = []
+  for (const node of data) {
+    if (node.upperGroupId === upperGroupId) {
+      const children = buildTree(data, node.id)
+      if (children.length) {
+        node.children = children
+      }
+      tree.push({
+        ...node,
+        key: node.id,
+        hasChildren: !!node.children?.length
+      })
+    }
+  }
+  return tree
+}
+
+const expandedKeys = ref<string[]>([])
+
+const selectGroup = computed(() => state.groupData.find(item => item.id === state.treeActiveKey[0]))
+
+const state = reactive<{
+  selectTree: Record<string, any>,
+  treeActiveKey: number[]
+  groupData: CVS.Group[],
+  groupTreeData: any,
+  groupLoading: boolean,
+  groupDateSource: CVS.device[],
+  groupDeviceDataSource: CVS.device[]
+  queryParams: any,
+  loading: boolean,
+  tableLoading: boolean
+  drawerVisible: boolean
+  selectedRowKeys: string[],
+  groupModalVisible: boolean,
+  groupOpera: 'add' | 'del',
+  selectGroup: Partial<CVS.Group>
+}>({
+  groupTreeData: [],
+  groupData: [],
+  groupOpera: 'add',
+  groupLoading: false,
+  groupDateSource: [],
+  groupDeviceDataSource: [],
+  treeActiveKey: [0],
+  selectTree: {},
+  loading: false,
+  tableLoading: false,
+  groupModalVisible: false,
+  selectGroup: {},
+  drawerVisible: false,
+  selectedRowKeys: [],
+  queryParams: {
+    page: 1,
+    pageSize: 10,
+    total: 0,
+    spaceId: '',
+    searchKey: 'deviceId',
+    searchValue: ''
+  }
+})
+
+const groupState = reactive({
+  groupLabel: '',
+  upperGroupLabel: '',
+  upperGroupId: ''
+})
+
+const { resetFields, validate, validateInfos } = useForm(groupState, reactive({
+  groupLabel: [{ required: true, message: '请填写分组名称' }]
+}))
+
+const changePage = ({ current }) => {
+  state.queryParams.page = current
+  getDevicePage()
+}
+
+const delBind = async (ids: []) => {
+  const params = (ids.length ? ids : state.selectedRowKeys).map(item => {
+    return {
+      deviceGroupId: state.treeActiveKey[0],
+      deviceId: item
+    }
+  })
+  await orgTree.delGroupBindDevice(params)
+  state.selectedRowKeys = []
+  await getDeviceByGroup()
+}
+
+const bindDevice = async () => {
+  const params = state.selectedRowKeys.map(item => {
+    return {
+      deviceId: item,
+      deviceGroupId: state.treeActiveKey[0]
+    }
+  })
+  await orgTree.postGroupBindDevice(params)
+  state.drawerVisible = false
+  state.selectedRowKeys = []
+  await getDeviceByGroup()
+}
+
+const ok = async () => {
+  if (state.groupOpera === 'add') {
+    validate().then(async () => {
+      await orgTree.postDeviceGroup(groupState)
+      state.groupModalVisible = false
+    })
+  } else {
+    if (state.selectTree.hasChildren) {
+      state.groupModalVisible = false
+    } else {
+      console.log('删除设备群组')
+      state.groupModalVisible = false
+      await orgTree.removeDeviceGroup(state.treeActiveKey[0])
+    }
+  }
+  await getDeviceGroup()
+}
+
+const onSelectChange = (rowKeys: string[]) => {
+  state.selectedRowKeys = rowKeys
+}
+
+const openGroupModal = (opra: 'add' | 'del', record: any = {}) => {
+  groupState.upperGroupLabel = ''
+  groupState.upperGroupId = ''
+  groupState.groupLabel = ''
+  state.selectTree = record
+  state.groupModalVisible = true
+  state.groupOpera = opra
+  groupState.upperGroupLabel = record.groupLabel
+  groupState.upperGroupId = record.id
+  console.log(findParentNodeById(record.upperGroupId, state.groupTreeData))
+}
+
+const changeTreeActiveKey = (id: string, e?: TouchEvent) => {
+  e && e.stopPropagation && e.stopPropagation()
+  state.treeActiveKey[0] = id
+  console.log('id1' + id)
+  console.log('id:' + state.treeActiveKey[0])
+  getDeviceByGroup()
+  console.log(state.groupData)
+  // state.selectGroup = state.groupData.find(item => item.id === id)
+}
+
+const addDeviceGroup = (e: TouchEvent) => {
+  e.stopPropagation()
+  state.groupModalVisible = true
+}
+
+const getDeviceByGroup = async () => {
+  state.tableLoading = true
+  const { data } = await orgTree.getDeviceByGroup({
+    page: 1,
+    pageSize: 200,
+    deviceGroupId: typeof state.treeActiveKey === 'object' ? state.treeActiveKey[0] : state.treeActiveKey
+  })
+  state.tableLoading = false
+  state.groupDeviceDataSource = data
+}
+
+const getDevicePage = async () => {
+  state.loading = true
+  const { data, sum } = await SpaceController.deviceGroupPage(state.queryParams)
+  state.loading = false
+  state.groupDateSource = data
+  state.queryParams.total = sum
+}
+
+const getDeviceGroup = async () => {
+  state.groupLoading = true
+  const { data } = await orgTree.listDeviceGroup({ upperGroupId: '' })
+  state.groupTreeData = [
+    {
+      groupLabel: '所有分组',
+      key: '',
+      id: '',
+      children: buildTree(data, '0')
+    }
+  ]
+  console.log('state.groupTreeData:' + state.groupTreeData)
+  state.groupData = data
+  state.groupLoading = false
+  expandedKeys.value = state.groupTreeData[0].children.map(item => item.id)
+}
+
+const pushCvsDevicePage = (record: CVS.space) => {
+  router.push({ path: '/cvs/video/device', query: { spaceId: record.spaceId } })
+}
+
+onMounted(() => {
+  getDeviceGroup()
+  getDevicePage()
+})
+
+</script>
+
+<style lang="less" scoped >
+.group-label {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 11 - 0
src/pages/cvs/video/space.vue

@@ -19,6 +19,17 @@
           <div>{{SpaceController.statusMap.get(record.status)?.label}}</div>
         </div>
       </template>
+      <template v-if="column.key === 'spaceId'" >
+        <a @click="openModal(record)" >{{record.spaceId}}</a>
+      </template>
+      <template v-if="column.key === 'type'">
+        <a-tag>{{SpaceController.type.find((item)=>{
+          return item.key == record.type
+        })?.label}}</a-tag>
+      </template>
+      <template v-if="column.key === 'deviceCount'">
+        <a @click="pushCvsDevicePage(record)" >{{record.deviceCount}}</a>
+      </template>
       <template v-if="column.key === 'action'"  >
         <a-space>
           <a @click="openModal(record)" >配置管理</a>

+ 0 - 0
src/pages/schedule/APICenter/index.vue → src/pages/datacenter/APICenter/index.vue


+ 0 - 0
src/pages/schedule/dataLake/dataTool/index.vue → src/pages/datacenter/dataLake/dataTool/index.vue


+ 0 - 0
src/pages/schedule/dataSource/manage/index.vue → src/pages/datacenter/dataSource/manage/index.vue


+ 0 - 0
src/pages/schedule/dataSource/metaTool/index.vue → src/pages/datacenter/dataSource/metaTool/index.vue


+ 37 - 44
src/router/index.ts

@@ -212,8 +212,8 @@ const iot = {
   ]
 }
 
-const schedule = {
-  path: '/schedule',
+const datacenter = {
+  path: '/datacenter',
   name: '数据中台',
   meta: {
     title: '数据中台'
@@ -230,12 +230,12 @@ const schedule = {
         {
           path: '/dataSource/manage',
           name: '数据源管理',
-          component: () => import('@/pages/schedule/dataSource/manage/index.vue')
+          component: () => import('@/pages/datacenter/dataSource/manage/index.vue')
         },
         {
           path: '/dataSource/metaTool',
           name: '元数据管理',
-          component: () => import('@/pages/schedule/dataSource/metaTool/index.vue')
+          component: () => import('@/pages/datacenter/dataSource/metaTool/index.vue')
         }
       ]
     },
@@ -248,12 +248,12 @@ const schedule = {
         {
           path: '/dataLake/dataTool',
           name: '数据湖工具',
-          component: () => import('@/pages/schedule/dataLake/dataTool/index.vue')
+          component: () => import('@/pages/datacenter/dataLake/dataTool/index.vue')
         },
         {
           path: '/dataLake/dataIsland',
           name: '数据岛',
-          component: () => import('@/pages/schedule/dataLake/dataTool/index.vue')
+          component: () => import('@/pages/datacenter/dataLake/dataTool/index.vue')
         }
       ]
     },
@@ -261,7 +261,7 @@ const schedule = {
       path: '/APICenter',
       name: '数据编排',
       icon: 'ApiOutlined',
-      component: () => import('@/pages/schedule/APICenter/index.vue')
+      component: () => import('@/pages/datacenter/APICenter/index.vue')
     }
   ]
 }
@@ -275,38 +275,29 @@ const view = {
   link: true
 }
 
-// const lowcode = {
-//   path: 'http://49.232.161.110:5556',
-//   name: '低代码',
-//   meta: {
-//     title: '低代码'
-//   },
-//   link: true
-// }
-//
-// const user = {
-//   path: '/user',
-//   name: '用户群组',
-//   meta: {
-//     title: '用户群组'
-//   },
-//   component: () => import('@/layout/layout.vue'),
-//   redirect: '/manage',
-//   children: [
-//     {
-//       path: '/manage',
-//       name: '用户管理',
-//       icon: 'TeamOutlined',
-//       component: () => import('@/pages/user/manage/index.vue')
-//     },
-//     {
-//       path: '/resource',
-//       name: '资源管理',
-//       icon: 'FolderOpenOutlined',
-//       component: () => import('@/pages/user/resource/index.vue')
-//     }
-//   ]
-// }
+const user = {
+  path: '/user',
+  name: '用户权限',
+  meta: {
+    title: '用户群组'
+  },
+  component: () => import('@/layout/layout.vue'),
+  redirect: '/manage',
+  children: [
+    {
+      path: '/manage',
+      name: '用户管理',
+      icon: 'TeamOutlined',
+      component: () => import('@/pages/user/manage/index.vue')
+    },
+    {
+      path: '/resource',
+      name: '资源管理',
+      icon: 'FolderOpenOutlined',
+      component: () => import('@/pages/user/resource/index.vue')
+    }
+  ]
+}
 
 const login = {
   path: '/login',
@@ -349,6 +340,12 @@ const cvs = {
           name: '设备',
           icon: '',
           component: () => import('@/pages/cvs/video/device.vue')
+        },
+        {
+          path: '/cvs/video/tree',
+          name: '设备组织树',
+          icon: '',
+          component: () => import('@/pages/cvs/video/orgtree.vue')
         }
       ]
     },
@@ -427,7 +424,7 @@ const cvs = {
   ]
 }
 
-const _routes = [iot, cvs, schedule, view] as any
+const _routes = [iot, cvs, datacenter, view, user] as any
 
 if (_routes[0].link) {
   window.open(_routes[0].path)
@@ -457,8 +454,4 @@ const router = createRouter({
   })
 })
 
-// router.push = async function (to: vueRouter.RouteLocationRaw): Promise<void | NavigationFailure | undefined> {
-//   router.push(to)
-// }
-
 export default router

+ 22 - 1
src/type/cvs.d.ts

@@ -1,5 +1,13 @@
 
 declare namespace CVS {
+
+  type QueryParams = {
+    page: number
+    pageSize: number
+    spaceId?: string
+    searchKey?: string
+    searchValue?: string
+  }
   interface space {
     id?: string
     description: string
@@ -46,7 +54,6 @@ declare namespace CVS {
       }
 
   }
-
   interface device {
     deviceId: number, // 设备id
     deviceName: string // 设备名称
@@ -61,6 +68,20 @@ declare namespace CVS {
     gisName: string
   }
 
+  interface Group {
+    'groupLabel': string, // 分组名字
+    'upperGroupId': string, // 上级分组id
+    'id': string, // 分组id
+  }
+
+  interface GroupQueryParams {
+    page: number,
+    pageSize: number,
+    modelId?: string,
+    searchKey?: string,
+    deviceGroupId: string,
+    searchValue?: string,
+  }
   interface Operator {
     id?: number // 序列化id
     aiId: string // 算子id

+ 1 - 1
src/type/iot.d.ts

@@ -175,7 +175,7 @@ declare namespace IOT {
       interface Group {
         'groupLabel': string, // 分组名字
         'upperGroupId': string, // 上级分组id
-
+        'id': string, // 分组id
       }
 
       interface GroupQueryParams {