Преглед изворни кода

feat: 设备群组 (完成)

lvkun пре 3 година
родитељ
комит
6c30135950

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

@@ -186,3 +186,37 @@ export const listDeviceGroup = (params: { upperGroupId: string }) => {
     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: IOT.API.DEVICE.GroupQueryParams) => {
+  return request<IOT.API.DEVICE.Device[]>({
+    url: '/device/group',
+    method: 'GET',
+    params
+  })
+}

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

@@ -1,7 +1,7 @@
 import {
   addDevice, addSubDevice, delDevice, delDeviceMul, delDeviceTag,
   getDeviceById, getDeviceCount, getDeviceList, getDeviceMsgList, addDeviceMsg, getDeviceTag,
-  getSubDeviceList, updateDeviceLabel, addDeviceCmd, getDeviceCmdList, addDeviceTag, addDeviceGroup, listDeviceGroup
+  getSubDeviceList, updateDeviceLabel, addDeviceCmd, getDeviceCmdList, addDeviceTag, addDeviceGroup, listDeviceGroup, postGroupBindDevice, delGroupBindDevice, getDeviceByGroup
 } from '@/api/iot/device'
 import { DeviceMsgEnum } from '@/enum/common'
 import { message } from 'ant-design-vue'
@@ -149,9 +149,34 @@ export class DeviceContriller {
     message.success('新增分组成功')
   }
 
-  /** 设备分组 新增分组 */
+  /** 设备分组 查询分组 */
   static async listDeviceGroup (params: { upperGroupId: string }) {
-    await listDeviceGroup(params)
-    message.success('新增分组成功')
+    return await listDeviceGroup(params)
+  }
+
+  /** 设备分组 设备绑定分组 */
+  static async postGroupBindDevice (data: { deviceGroupId: string, deviceId: string }[]) {
+    await postGroupBindDevice(data)
+    message.success('绑定成功')
+  }
+
+  /**
+ * 此函数删除设备和设备组之间的绑定并显示成功消息。
+ * @param data - 参数 `data` 是一个包含两个属性的对象:
+ */
+  static async delGroupBindDevice (data: { deviceGroupId: string, deviceId: string }[]) {
+    await delGroupBindDevice(data)
+    message.success('取消绑定成功')
+  }
+
+  /**
+   * 此函数根据 TypeScript 中的组查询参数检索设备。
+   * @param params -
+   * 参数“params”的类型为“IOT.API.DEVICE.GroupQueryParams”,这可能是代码库中定义的接口或类型。它用作函数“getDeviceByGroup”的参数,该函数使用“await”关键字调用以生成函数
+   * @returns 正在使用 params 参数调用 getDeviceByGroup 函数,并使用 return
+   * 关键字返回该函数调用的结果。该函数被标记为“async”,因此它将返回一个解析为“getDeviceByGroup”函数调用结果的承诺。
+   */
+  static async getDeviceByGroup (params: IOT.API.DEVICE.GroupQueryParams) {
+    return await getDeviceByGroup(params)
   }
 }

+ 1 - 1
src/pages/Iot/device/components/msgTrack.vue

@@ -38,7 +38,7 @@
 
     </a-table>
   </a-card>
-</template>``
+</template>
 
 <script lang="ts" setup >
 import { AlertTsx } from '@/components/MicroComponents/index'

+ 508 - 48
src/pages/Iot/device/group.vue

@@ -4,86 +4,546 @@
     <a-card style="height: 100%;" >
       <template #title >
         <a-row justify="space-between" align="middle" >
-          <a-col><a-button type="primary" >+ 新增设备群组</a-button></a-col>
-          <a-col><ReloadIconTsx :loading="state.groupLoading" @reload="getDeviceGroup"/></a-col>
+          <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-tree
+      <a-spin :spinning="state.groupLoading" >
+        <a-tree
         v-model:expandedKeys="expandedKeys"
-        v-model:selectedKeys="selectedKeys"
-        v-model:checkedKeys="checkedKeys"
-        :tree-data="treeData"
+        v-model:selectedKeys="state.treeActiveKey"
+        :tree-data="state.groupTreeData"
+        defaultExpandAll
+        autoExpandParent
+        :field-names="{
+          key: 'id',
+          title: 'groupLabel',
+        }"
       >
-        <template #title="{ title, key }">
-          <a-space  >
-            <span> {{ title }} </span>
-            <plus-circle-outlined />
-            <delete-outlined />
+        <template #title="record">
+          <a-space @click="changeTreeActiveKey(record.key)" >
+            <span> {{ record.groupLabel }} </span>
+            <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="16" :xs="24">
-    <a-card style="height: 100%;" >3</a-card>
+    <a-card style="height: 100%;" >
+      <span v-if="!state.treeActiveKey" >
+        <a-row justify="space-between" >
+          <a-col>
+            <a-space>
+              <a-select
+                style="width: 170px;"
+                v-model:value="state.queryParams.modelId"
+              >
+                <a-select-option
+                  v-for="item in state.modelList"
+                  :key="item.id"
+                  :value="item.id"
+                >
+                {{ item.modelLabel }}
+                </a-select-option>
+              </a-select>
+              <a-select
+                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="查询" @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"
+        >
+        <template #bodyCell="{column, record}">
+          <template v-if="column.key === 'deviceStatus'" >
+          <a-tag :color="record.deviceStatus.color" >{{record.deviceStatus.name}}</a-tag>
+        </template>
+          <template v-if="column.key === 'deviceNodeType'" >
+          {{record.deviceNodeType == 'GATEWAY' ? '直连类型' : '非直连类型' }}
+        </template>
+        </template>
+        </a-table>
+      </span>
+      <span v-else >
+        <a-row  >
+        <a-col :span="24" >
+          <a-descriptions title="基本信息" :column="{lg: 2, md: 1, xs: 1}" >
+          <a-descriptions-item label="群组名称">{{selectGroup?.groupLabel}}</a-descriptions-item>
+          <a-descriptions-item label="群组ID">{{selectGroup?.id}}</a-descriptions-item>
+          <a-descriptions-item label="群组描述">我是假的群组描述</a-descriptions-item>
+        </a-descriptions>
+        </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.id"
+          :columns="groupDeviceColumns"
+          :loading='state.loading'
+          :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.id])"
+                >
+                  <a>解绑</a>
+                </a-popconfirm>
+              </a-space>
+            </template>
+          </template>
+        </a-table>
+      </a-card>
+      </span>
+    </a-card>
   </a-col>
  </a-row>
+
+ <!-- 绑定设备抽屉 -->
+ <a-drawer
+    v-model:visible="state.drawerVisible"
+    size="large"
+    class="custom-class"
+    title="绑定设备"
+    placement="right"
+    @after-visible-change="afterVisibleChange"
+  >
+   <a-alert message="一个设备最多可以被添加到10个设备组中。" type="info" show-icon />
+   <a-row>
+    <a-col>
+      <a-space  style="margin-top: 20px;">
+        <a-select style="width: 100px;"  v-model:value="state.queryParams.searchKey">
+          <a-select-option v-for="item in searchKeys.concat({name: '产品', key: 'modelId'})" :key="item.key" :value="item.key" >{{item.name}}</a-select-option>
+        </a-select>
+        <a-input  v-if="state.queryParams.searchKey !== 'modelId'"  v-model:value="state.queryParams.searchValue"  ></a-input>
+        <a-select v-else style="width: 170px;" v-model:value="state.queryParams.searchValue"   >
+                <a-select-option
+                  v-for="item in state.modelList"
+                  :key="item.id"
+                  :value="item.id"
+                >
+                {{ item.modelLabel }}
+                </a-select-option>
+        </a-select>
+        <a-button @click="getDevicePage" > 搜索</a-button>
+      </a-space>
+    </a-col>
+    <a-col>
+      <a-table
+          :rowKey="record => record.id"
+          style="margin-top: 10px;"
+          :loading="state.loading"
+          :columns="columns"
+          :data-source="state.groupDateSource"
+          :row-selection="{
+            selectedRowKeys: state.selectedRowKeys,
+            onChange: onSelectChange,
+          }"
+        >
+          <template #bodyCell="{column, record}">
+            <template v-if="column.key === 'deviceStatus'" >
+            <a-tag :color="record.deviceStatus.color" >{{record.deviceStatus.name}}</a-tag>
+          </template>
+            <template v-if="column.key === 'deviceNodeType'" >
+            {{record.deviceNodeType == 'GATEWAY' ? '直连类型' : '非直连类型' }}
+          </template>
+        </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.groupOpra === 'add' ? '添加设备群组' : '删除设备群组'"
+    :visible="state.groupModalVisible"
+    @cancel="state.groupModalVisible = false"
+    @ok="ok"
+  >
+
+    <a-form v-if="state.groupOpra === 'add'" :label-col="{span: 6}" :wrapper-col="{span: 16}" >
+      <a-form-item label="父级群组名称" v-if="groupState.upperGroupId" >
+        <a-input disabled  :value="groupState.upperGroupLabel" ></a-input>
+      </a-form-item>
+      <a-form-item label="分组名"  v-bind="validateInfos.groupLabel" >
+        <a-input 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>
 </template>
 
 <script lang="ts" setup >
 import { ReloadIconTsx } from '@/components/MicroComponents/index'
-import { DeviceContriller } from '@/controller'
-import { onMounted, reactive, ref, watch } from 'vue'
+import { DeviceContriller, ModelController } from '@/controller'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
 import { PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons-vue'
-const treeData: TreeProps['treeData'] = [
+import { Form } from 'ant-design-vue'
+
+const useForm = Form.useForm
+
+const columns = [
+
   {
-    title: '所有分组',
-    key: '0-0',
-    children: [
-      {
-        title: 'parent 1-0',
-        key: '0-0-0',
-        children: [
-          { title: 'leaf', key: '0-0-0-0', disableCheckbox: true },
-          { title: 'leaf', key: '0-0-0-1' }
-        ]
-      },
-      {
-        title: 'parent 1-1',
-        key: '0-0-1',
-        children: [{ key: '0-0-1-0', title: 'sss' }]
-      }
-    ]
+    title: '状态',
+    dataIndex: 'deviceStatus',
+    key: 'deviceStatus'
+  },
+  {
+    title: '设备ID',
+    dataIndex: 'id'
+  },
+  {
+    title: '设备名称',
+    dataIndex: 'deviceLabel'
+  },
+  {
+    title: '设备标识码',
+    dataIndex: 'deviceCode'
+  },
+  {
+    title: '设备描述',
+    dataIndex: 'deviceDescription'
+  },
+  {
+    title: '节点类型',
+    dataIndex: 'deviceNodeType',
+    key: 'deviceNodeType'
+  },
+  {
+    title: '所属产品',
+    dataIndex: 'modelLabel'
+  },
+  {
+    title: '操作',
+    key: 'action'
   }
 ]
 
-const expandedKeys = ref<string[]>(['0-0-0', '0-0-1'])
-const selectedKeys = ref<string[]>(['0-0-0', '0-0-1'])
-const checkedKeys = ref<string[]>(['0-0-0', '0-0-1'])
-watch(expandedKeys, () => {
-  console.log('expandedKeys', expandedKeys)
-})
-watch(selectedKeys, () => {
-  console.log('selectedKeys', selectedKeys)
-})
-watch(checkedKeys, () => {
-  console.log('checkedKeys', checkedKeys)
+const groupDeviceColumns = [
+  {
+    title: '设备名称',
+    dataIndex: 'deviceLabel'
+  },
+  {
+    title: '设备标识码',
+    dataIndex: 'deviceCode'
+  },
+  {
+    title: '所属产品',
+    dataIndex: 'modelLabel'
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    key: 'action'
+  }
+]
+
+const searchKeys = [
+  // { name: '设备id', key: 'deviceId' },
+  { name: '设备名称', key: 'deviceLabel' },
+  { name: '设备标识码', key: 'deviceCode' }
+]
+
+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 selectedKeys = ref<string[]>([''])
+
+watch(
+  selectedKeys,
+  () => {
+    console.log('selectedKeys', selectedKeys.value[0])
+    state.treeActiveKey = selectedKeys.value[0]
+    // selectedKeys.value = [state.treeActiveKey]
+    getDeviceByGroup()
+  })
+
+const selectGroup = computed(() => state.groupData.find(item => item.id === state.treeActiveKey))
+
+const deviceState = reactive({
+  dataSource: [],
+  qeuryParams: {
+    searchKey: 'deviceLabel',
+    searchValue: '',
+    modelId: '',
+    page: 1,
+    pageSize: 10,
+    total: 0
+  }
 })
 
-const state = reactive({
+const state = reactive<{
+  selectTree: Record<string, any>,
+  treeActiveKey: string
+  groupData: IOT.API.DEVICE.Group[],
+  groupTreeData: any,
+  groupLoading: boolean,
+  groupDateSource: IOT.API.DEVICE.Device[],
+  modelList: IOT.API.MODEL.ModelDot[],
+  groupDeviceDataSource: IOT.API.DEVICE.Device[]
+  queryParams: any,
+  loading: boolean,
+  drawerVisible: boolean
+  selectedRowKeys: string[],
+  groupModalVisible: boolean,
+  groupOpra: 'add' | 'del'
+}>({
+  groupTreeData: [],
+  groupData: [],
+  groupOpra: 'add',
   groupLoading: false,
-  groupDateSource: []
+  groupDateSource: [],
+  groupDeviceDataSource: [],
+  treeActiveKey: '',
+  selectTree: {},
+  loading: false,
+  groupModalVisible: false,
+
+  drawerVisible: false,
+  modelList: [],
+  selectedRowKeys: [],
+  queryParams: {
+    page: 1,
+    pageSize: 10,
+    total: 0,
+    modelId: '',
+    searchKey: 'deviceLabel',
+    searchValue: ''
+  }
+})
+
+const groupState = reactive({
+  groupLabel: '',
+  upperGroupLabel: '',
+  upperGroupId: ''
 })
 
+const { resetFields, validate, validateInfos } = useForm(groupState, reactive({
+  groupLabel: [{ required: true, message: '请填写分组名称' }]
+}))
+
+const delBind = async (ids: []) => {
+  const params = (ids.length ? ids : state.selectedRowKeys).map(item => {
+    return {
+      deviceGroupId: state.treeActiveKey,
+      deviceId: item
+    }
+  })
+  await DeviceContriller.delGroupBindDevice(params)
+  state.selectedRowKeys = []
+  getDeviceByGroup()
+}
+
+const bindDevice = async () => {
+  const params = state.selectedRowKeys.map(item => {
+    return {
+      deviceId: item,
+      deviceGroupId: state.treeActiveKey
+    }
+  })
+  await DeviceContriller.postGroupBindDevice(params)
+  state.drawerVisible = false
+  state.selectedRowKeys = []
+  getDeviceByGroup()
+}
+
+const ok = () => {
+  if (state.groupOpra === 'add') {
+    validate().then(async () => {
+      await DeviceContriller.postDeviceGroup(groupState)
+      state.groupModalVisible = false
+      getDeviceGroup()
+    })
+  } else {
+    if (state.selectTree.hasChildren) {
+      state.groupModalVisible = false
+    } else {
+      console.log('删除设备群组')
+      state.groupModalVisible = false
+      // await DeviceContriller.dle
+    }
+  }
+}
+
+const onSelectChange = (rowKeys: string[]) => {
+  console.log('rowKeys:', rowKeys)
+  state.selectedRowKeys = rowKeys
+}
+
+const openGroupModal = (opra: 'add' | 'del', record: any = {}) => {
+  groupState.upperGroupLabel = ''
+  groupState.upperGroupId = ''
+  groupState.groupLabel = ''
+
+  state.selectTree = record
+
+  state.groupModalVisible = true
+  state.groupOpra = 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 = id
+  console.log(state.treeActiveKey)
+}
+
+const addDevicegroup = (e: TouchEvent) => {
+  e.stopPropagation()
+  state.groupModalVisible = true
+}
+
+const getDeviceByGroup = async () => {
+  const { data, sum } = await DeviceContriller.getDeviceByGroup({
+    page: 1,
+    pageSize: 200,
+    deviceGroupId: state.treeActiveKey
+  })
+  state.groupDeviceDataSource = data
+}
+
+const getDevicePage = async () => {
+  state.loading = true
+  const { data, sum } = await DeviceContriller.page(state.queryParams)
+  state.loading = false
+  state.groupDateSource = data
+  state.queryParams.total = sum
+}
+
+const getModelList = async () => {
+  const { data } = await ModelController.list()
+  state.modelList = data.concat({ modelLabel: '所有产品', id: '' })
+}
+
 const getDeviceGroup = async () => {
   state.groupLoading = true
-  const data = await DeviceContriller.listDeviceGroup({ upperGroupId: '' })
+  const { data } = await DeviceContriller.listDeviceGroup({ upperGroupId: '' })
+
+  state.groupTreeData = [
+    {
+      groupLabel: '所有分组',
+      key: '',
+      id: '',
+      children: buildTree(data, '0')
+    }
+  ]
+  state.groupData = data
   state.groupLoading = false
-  state.groupDateSource = data
+  console.log('expand:', state.groupTreeData[0].children.map(item => item.id))
+
+  expandedKeys.value = state.groupTreeData[0].children.map(item => item.id)
 }
 
 onMounted(() => {
-
+  getModelList()
+  getDeviceGroup()
+  getDevicePage()
 })
 
 </script>

+ 10 - 0
src/type/iot.d.ts

@@ -139,6 +139,16 @@ declare namespace IOT {
       interface Group {
         'groupLabel': string, // 分组名字
         'upperGroupId': string, // 上级分组id
+
+      }
+
+      interface GroupQueryParams {
+        page: number,
+        pageSize: number,
+        modelId?: string,
+        searchKey?: string,
+        deviceGroupId: string,
+        searchValue?: string,
       }
     }