Эх сурвалжийг харах

Merge remote-tracking branch 'yudao/feature/iot' into feature/iot

# Conflicts:
#	src/views/iot/product/product/detail/ThingModel/ThingModelDataSpecs.vue
#	src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelEnumTypeDataSpecs.vue
#	src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelNumberTypeDataSpecs.vue
#	src/views/iot/thinkmodel/ThinkModelForm.vue
#	src/views/iot/thinkmodel/dataSpecs/ThinkModelArrayTypeDataSpecs.vue
puhui999 8 сар өмнө
parent
commit
53c967c308

+ 14 - 1
src/api/iot/device/index.ts → src/api/iot/device/device/index.ts

@@ -55,6 +55,14 @@ export interface DeviceHistoryDataVO {
   data: string // 数据
 }
 
+// IoT 设备状态枚举
+export enum DeviceStatusEnum {
+  INACTIVE = 0, // 未激活
+  ONLINE = 1,   // 在线
+  OFFLINE = 2,  // 离线
+  DISABLED = 3  // 已禁用
+}
+
 // 设备 API
 export const DeviceApi = {
   // 查询设备分页
@@ -115,7 +123,7 @@ export const DeviceApi = {
     return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } })
   },
 
-  // 获取设备属性最数据
+  // 获取设备属性最���数据
   getDevicePropertiesLatestData: async (params: any) => {
     return await request.get({ url: `/iot/device/data/latest`, params })
   },
@@ -123,5 +131,10 @@ export const DeviceApi = {
   // 获取设备属性历史数据
   getDevicePropertiesHistoryData: async (params: any) => {
     return await request.get({ url: `/iot/device/data/history`, params })
+  },
+
+  // 获取导入模板
+  importDeviceTemplate: async () => {
+    return await request.download({ url: `/iot/device/get-import-template` })
   }
 }

+ 51 - 0
src/api/iot/plugininfo/index.ts

@@ -0,0 +1,51 @@
+import request from '@/config/axios'
+
+// IoT 插件信息 VO
+export interface PluginInfoVO {
+  id: number // 主键ID
+  pluginId: string // 插件包id
+  name: string // 插件名称
+  description: string // 描述
+  deployType: number // 部署方式
+  file: string // 插件包文件名
+  version: string // 插件版本
+  type: number // 插件类型
+  protocol: string // 设备插件协议类型
+  status: number // 状态
+  configSchema: string // 插件配置项描述信息
+  config: string // 插件配置信息
+  script: string // 插件脚本
+}
+
+// IoT 插件信息 API
+export const PluginInfoApi = {
+  // 查询IoT 插件信息分页
+  getPluginInfoPage: async (params: any) => {
+    return await request.get({ url: `/iot/plugin-info/page`, params })
+  },
+
+  // 查询IoT 插件信息详情
+  getPluginInfo: async (id: number) => {
+    return await request.get({ url: `/iot/plugin-info/get?id=` + id })
+  },
+
+  // 新增IoT 插件信息
+  createPluginInfo: async (data: PluginInfoVO) => {
+    return await request.post({ url: `/iot/plugin-info/create`, data })
+  },
+
+  // 修改IoT 插件信息
+  updatePluginInfo: async (data: PluginInfoVO) => {
+    return await request.put({ url: `/iot/plugin-info/update`, data })
+  },
+
+  // 删除IoT 插件信息
+  deletePluginInfo: async (id: number) => {
+    return await request.delete({ url: `/iot/plugin-info/delete?id=` + id })
+  },
+
+  // 导出IoT 插件信息 Excel
+  exportPluginInfo: async (params) => {
+    return await request.download({ url: `/iot/plugin-info/export-excel`, params })
+  }
+}

+ 1 - 0
src/api/iot/thinkmodel/index.ts

@@ -51,6 +51,7 @@ export enum ProductFunctionAccessModeEnum {
   READ_ONLY = 'r' // 只读
 }
 
+// TODO @puhui999:getProductThingModelPage => getThingModelPage 哈,不用带 product 前缀
 // IoT 产品物模型 API
 export const ThinkModelApi = {
   // 查询产品物模型分页

+ 4 - 1
src/utils/dict.ts

@@ -239,5 +239,8 @@ export enum DICT_TYPE {
   IOT_PRODUCT_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
   IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
   IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
-  IOT_RW_TYPE = 'iot_rw_type' // IOT 读写类型
+  IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型
+  IOT_PLUGIN_DEPLOY_TYPE = 'iot_plugin_deploy_type', // IOT 插件部署类型
+  IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态
+  IOT_PLUGIN_TYPE = 'iot_plugin_type' // IOT 插件类型
 }

+ 1 - 1
src/views/iot/device/device/DeviceForm.vue

@@ -85,7 +85,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { DeviceApi, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
 import { DeviceGroupApi } from '@/api/iot/device/group'
 import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
 import { UploadImg } from '@/components/UploadFile'

+ 1 - 1
src/views/iot/device/device/DeviceGroupForm.vue

@@ -26,7 +26,7 @@
 </template>
 
 <script setup lang="ts">
-import { DeviceApi } from '@/api/iot/device'
+import { DeviceApi } from '@/api/iot/device/device'
 import { DeviceGroupApi } from '@/api/iot/device/group'
 
 defineOptions({ name: 'IoTDeviceGroupForm' })

+ 139 - 0
src/views/iot/device/device/DeviceImportForm.vue

@@ -0,0 +1,139 @@
+<template>
+  <Dialog v-model="dialogVisible" title="设备导入" width="400">
+    <el-upload
+      ref="uploadRef"
+      v-model:file-list="fileList"
+      :action="importUrl + '?updateSupport=' + updateSupport"
+      :auto-upload="false"
+      :disabled="formLoading"
+      :headers="uploadHeaders"
+      :limit="1"
+      :on-error="submitFormError"
+      :on-exceed="handleExceed"
+      :on-success="submitFormSuccess"
+      accept=".xlsx, .xls"
+      drag
+    >
+      <Icon icon="ep:upload" />
+      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+      <template #tip>
+        <div class="el-upload__tip text-center">
+          <div class="el-upload__tip">
+            <el-checkbox v-model="updateSupport" />
+            是否更新已经存在的设备数据
+          </div>
+          <span>仅允许导入 xls、xlsx 格式文件。</span>
+          <el-link
+            :underline="false"
+            style="font-size: 12px; vertical-align: baseline"
+            type="primary"
+            @click="importTemplate"
+          >
+            下载模板
+          </el-link>
+        </div>
+      </template>
+    </el-upload>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DeviceApi } from '@/api/iot/device/device'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import download from '@/utils/download'
+
+defineOptions({ name: 'IoTDeviceImportForm' })
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const uploadRef = ref()
+const importUrl =
+  import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/iot/device/import'
+const uploadHeaders = ref() // 上传 Header 头
+const fileList = ref([]) // 文件列表
+const updateSupport = ref(0) // 是否更新已经存在的设备数据
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+  updateSupport.value = 0
+  fileList.value = []
+  resetForm()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (fileList.value.length == 0) {
+    message.error('请上传文件')
+    return
+  }
+  // 提交请求
+  uploadHeaders.value = {
+    Authorization: 'Bearer ' + getAccessToken(),
+    'tenant-id': getTenantId()
+  }
+  formLoading.value = true
+  uploadRef.value!.submit()
+}
+
+/** 文件上传成功 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+  if (response.code !== 0) {
+    message.error(response.msg)
+    formLoading.value = false
+    return
+  }
+  // 拼接提示语
+  const data = response.data
+  let text = '上传成功数量:' + data.createDeviceNames.length + ';'
+  for (let deviceName of data.createDeviceNames) {
+    text += '< ' + deviceName + ' >'
+  }
+  text += '更新成功数量:' + data.updateDeviceNames.length + ';'
+  for (const deviceName of data.updateDeviceNames) {
+    text += '< ' + deviceName + ' >'
+  }
+  text += '更新失败数量:' + Object.keys(data.failureDeviceNames).length + ';'
+  for (const deviceName in data.failureDeviceNames) {
+    text += '< ' + deviceName + ': ' + data.failureDeviceNames[deviceName] + ' >'
+  }
+  message.alert(text)
+  formLoading.value = false
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emits('success')
+}
+
+/** 上传错误提示 */
+const submitFormError = (): void => {
+  message.error('上传失败,请您重新上传!')
+  formLoading.value = false
+}
+
+/** 重置表单 */
+const resetForm = async (): Promise<void> => {
+  // 重置上传状态和文件
+  formLoading.value = false
+  await nextTick()
+  uploadRef.value?.clearFiles()
+}
+
+/** 文件数超出提示 */
+const handleExceed = (): void => {
+  message.error('最多只能上传一个文件!')
+}
+
+/** 下载模板操作 */
+const importTemplate = async () => {
+  const res = await DeviceApi.importDeviceTemplate()
+  download.excel(res, '设备导入模版.xls')
+}
+</script>

+ 1 - 2
src/views/iot/device/device/detail/DeviceDataDetail.vue

@@ -53,10 +53,9 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { DeviceApi, DeviceHistoryDataVO, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceHistoryDataVO, DeviceVO } from '@/api/iot/device/device'
 import { ProductVO } from '@/api/iot/product/product'
 import { beginOfDay, dateFormatter, endOfDay, formatDate } from '@/utils/formatTime'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 const props = defineProps<{ product: ProductVO; device: DeviceVO }>()
 

+ 7 - 13
src/views/iot/device/device/detail/DeviceDetailsHeader.vue

@@ -35,24 +35,22 @@
   <DeviceForm ref="formRef" @success="emit('refresh')" />
 </template>
 <script setup lang="ts">
-import { ref } from 'vue'
-import DeviceForm from '@/views/iot/device/DeviceForm.vue'
+import DeviceForm from '@/views/iot/device/device/DeviceForm.vue'
 import { ProductVO } from '@/api/iot/product/product'
-import { DeviceVO } from '@/api/iot/device'
-import { useRouter } from 'vue-router'
+import { DeviceVO } from '@/api/iot/device/device'
 
 const message = useMessage()
 const router = useRouter()
 
-// 操作修改
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
+const emit = defineEmits(['refresh'])
+
+/** 操作修改 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
-const emit = defineEmits(['refresh'])
-
 /** 复制到剪贴板方法 */
 const copyToClipboard = async (text: string) => {
   try {
@@ -63,11 +61,7 @@ const copyToClipboard = async (text: string) => {
   }
 }
 
-/**
- * 跳转到产品详情页面
- *
- * @param productId 产品 ID
- */
+/** 跳转到产品详情页面 */
 const goToProductDetail = (productId: number) => {
   router.push({ name: 'IoTProductDetail', params: { id: productId } })
 }

+ 1 - 3
src/views/iot/device/device/detail/DeviceDetailsInfo.vue

@@ -79,16 +79,14 @@
   </ContentWrap>
 </template>
 <script setup lang="ts">
-import { ref } from 'vue'
 import { DICT_TYPE } from '@/utils/dict'
 import { ProductVO } from '@/api/iot/product/product'
 import { formatDate } from '@/utils/formatTime'
-import { DeviceVO } from '@/api/iot/device'
+import { DeviceVO } from '@/api/iot/device/device'
 
 const message = useMessage() // 消息提示
 
 const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
-
 const emit = defineEmits(['refresh']) // 定义 Emits
 
 const activeNames = ref(['basicInfo']) // 展示的折叠面板

+ 1 - 1
src/views/iot/device/device/detail/DeviceDetailsModel.vue

@@ -79,7 +79,7 @@
 </template>
 <script setup lang="ts">
 import { ProductVO } from '@/api/iot/product/product'
-import { DeviceApi, DeviceDataVO, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceDataVO, DeviceVO } from '@/api/iot/device/device'
 import { dateFormatter } from '@/utils/formatTime'
 import DeviceDataDetail from './DeviceDataDetail.vue'
 

+ 6 - 6
src/views/iot/device/device/detail/index.vue

@@ -21,17 +21,17 @@
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import { DeviceApi, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
 import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
-import DeviceDetailsHeader from '@/views/iot/device/detail/DeviceDetailsHeader.vue'
-import DeviceDetailsInfo from '@/views/iot/device/detail/DeviceDetailsInfo.vue'
-import DeviceDetailsModel from '@/views/iot/device/detail/DeviceDetailsModel.vue'
+import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
+import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
+import DeviceDetailsModel from './DeviceDetailsModel.vue'
 
 defineOptions({ name: 'IoTDeviceDetail' })
 
 const route = useRoute()
 const message = useMessage()
-const id = route.params.id // 编号
+const id = Number(route.params.id) // 将字符串转换为数字
 const loading = ref(true) // 加载中
 const product = ref<ProductVO>({} as ProductVO) // 产品详情
 const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
@@ -42,7 +42,6 @@ const getDeviceData = async (id: number) => {
   loading.value = true
   try {
     device.value = await DeviceApi.getDevice(id)
-    console.log(product.value)
     await getProductData(device.value.productId)
   } finally {
     loading.value = false
@@ -64,5 +63,6 @@ onMounted(async () => {
     return
   }
   await getDeviceData(id)
+  activeTab.value = route.query.tab as string
 })
 </script>

+ 56 - 13
src/views/iot/device/device/index.vue

@@ -123,6 +123,9 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+        <el-button type="warning" plain @click="handleImport" v-hasPermi="['iot:device:import']">
+          <Icon icon="ep:upload" /> 导入
+        </el-button>
         <el-button
           type="primary"
           plain
@@ -150,14 +153,45 @@
     <template v-if="viewMode === 'card'">
       <el-row :gutter="16">
         <el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
-          <el-card class="h-full transition-colors" :body-style="{ padding: '0' }">
-            <div class="p-4">
+          <el-card
+            class="h-full transition-colors relative overflow-hidden"
+            :body-style="{ padding: '0' }"
+          >
+            <!-- 添加渐变背景层 -->
+            <div
+              class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
+              :class="[
+                item.status === DeviceStatusEnum.ONLINE
+                  ? 'bg-gradient-to-b from-[#eefaff] to-transparent'
+                  : 'bg-gradient-to-b from-[#fff1f1] to-transparent'
+              ]"
+            >
+            </div>
+            <div class="p-4 relative">
               <!-- 标题区域 -->
               <div class="flex items-center mb-3">
                 <div class="mr-2.5 flex items-center">
                   <el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
                 </div>
-                <div class="text-[16px] font-600">{{ item.deviceName }}</div>
+                <div class="text-[16px] font-600 flex-1">{{ item.deviceName }}</div>
+                <!-- 添加设备状态标签 -->
+                <div class="inline-flex items-center">
+                  <div
+                    class="w-1 h-1 rounded-full mr-1.5"
+                    :class="
+                      item.status === DeviceStatusEnum.ONLINE
+                        ? 'bg-[var(--el-color-success)]'
+                        : 'bg-[var(--el-color-danger)]'
+                    "
+                  >
+                  </div>
+                  <el-text
+                    class="!text-xs font-bold"
+                    :type="item.status === DeviceStatusEnum.ONLINE ? 'success' : 'danger'"
+                  >
+                    {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATUS, item.status) }}
+                  </el-text>
+                </div>
               </div>
 
               <!-- 信息区域 -->
@@ -186,7 +220,7 @@
               <!-- 分隔线 -->
               <el-divider class="!my-3" />
 
-              <!-- 按钮 -->
+              <!-- 按钮 -->
               <div class="flex items-center px-0">
                 <el-button
                   class="flex-1 !px-2 !h-[32px] text-[13px]"
@@ -211,10 +245,10 @@
                   class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
                   type="info"
                   plain
-                  @click="openLog(item.id)"
+                  @click="openModel(item.id)"
                 >
                   <Icon icon="ep:tickets" class="mr-1" />
-                  日志
+                  数据
                 </el-button>
                 <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
                 <el-button
@@ -290,7 +324,7 @@
           >
             查看
           </el-button>
-          <el-button link type="primary" @click="openLog(scope.row.id)"> 日志 </el-button>
+          <el-button link type="primary" @click="openModel(scope.row.id)"> 日志 </el-button>
           <el-button
             link
             type="primary"
@@ -324,17 +358,20 @@
   <DeviceForm ref="formRef" @success="getList" />
   <!-- 分组表单组件 -->
   <DeviceGroupForm ref="groupFormRef" @success="getList" />
+  <!-- 导入表单组件 -->
+  <DeviceImportForm ref="importFormRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import { DeviceApi, DeviceVO } from '@/api/iot/device'
+import { DeviceApi, DeviceVO, DeviceStatusEnum } from '@/api/iot/device/device'
 import DeviceForm from './DeviceForm.vue'
 import { ProductApi, ProductVO } from '@/api/iot/product/product'
 import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
 import download from '@/utils/download'
 import DeviceGroupForm from './DeviceGroupForm.vue'
+import DeviceImportForm from './DeviceImportForm.vue'
 
 /** IoT 设备列表 */
 defineOptions({ name: 'IoTDevice' })
@@ -342,7 +379,7 @@ defineOptions({ name: 'IoTDevice' })
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
-const loading = ref(true) // 列表加载中
+const loading = ref(true) // 列表加载中
 const list = ref<DeviceVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
@@ -452,9 +489,15 @@ const openGroupForm = () => {
   groupFormRef.value.open(selectedIds.value)
 }
 
-/** 打开日志 */
-const openLog = (id: number) => {
-  push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'log' } })
+/** 打开物模型数据 */
+const openModel = (id: number) => {
+  push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } })
+}
+
+/** 设备导入 */
+const importFormRef = ref()
+const handleImport = () => {
+  importFormRef.value.open()
 }
 
 /** 初始化 **/

+ 106 - 0
src/views/iot/plugininfo/PluginInfoForm.vue

@@ -0,0 +1,106 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="插件名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入插件名称" />
+      </el-form-item>
+      <el-form-item label="部署方式" prop="deployType">
+        <el-select v-model="formData.deployType" placeholder="请选择部署方式">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { PluginInfoApi, PluginInfoVO } from '@/api/iot/plugininfo'
+
+/** IoT 插件信息 表单 */
+defineOptions({ name: 'PluginInfoForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  deployType: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '插件名称不能为空', trigger: 'blur' }],
+  deployType: [{ required: true, message: '部署方式不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await PluginInfoApi.getPluginInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as PluginInfoVO
+    if (formType.value === 'create') {
+      await PluginInfoApi.createPluginInfo(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await PluginInfoApi.updatePluginInfo(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    deployType: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 44 - 0
src/views/iot/plugininfo/detail.vue

@@ -0,0 +1,44 @@
+<template>
+  <ContentWrap>
+    <el-descriptions title="插件详情" :column="2" border>
+      <el-descriptions-item label="插件名称">{{ pluginInfo.name }}</el-descriptions-item>
+      <el-descriptions-item label="组件ID">{{ pluginInfo.pluginId }}</el-descriptions-item>
+      <el-descriptions-item label="Jar包">{{ pluginInfo.file }}</el-descriptions-item>
+      <el-descriptions-item label="版本号">{{ pluginInfo.version }}</el-descriptions-item>
+      <el-descriptions-item label="部署方式">
+        <dict-tag :type="DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE" :value="pluginInfo.deployType" />
+      </el-descriptions-item>
+      <el-descriptions-item label="状态">
+        <dict-tag :type="DICT_TYPE.IOT_PLUGIN_STATUS" :value="pluginInfo.status" />
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-button type="primary" @click="goBack">返回</el-button>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { PluginInfoApi, PluginInfoVO } from '@/api/iot/plugininfo'
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+const pluginInfo = ref<PluginInfoVO>({})
+
+const getPluginInfo = async (id: number) => {
+  const data = await PluginInfoApi.getPluginInfo(id)
+  pluginInfo.value = data
+}
+
+const goBack = () => {
+  router.back()
+}
+
+onMounted(() => {
+  const id = Number(route.params.id)
+  if (id) {
+    getPluginInfo(id)
+  }
+})
+</script>

+ 257 - 0
src/views/iot/plugininfo/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="插件名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入插件名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          @change="handleQuery"
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PLUGIN_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['iot:plugin-info:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+      <!-- 视图切换按钮 -->
+      <el-form-item class="float-right !mr-0 !mb-0">
+        <el-button-group>
+          <el-button :type="viewType === 'card' ? 'primary' : 'default'" @click="viewType = 'card'">
+            <Icon icon="ep:grid" />
+          </el-button>
+          <el-button
+            :type="viewType === 'table' ? 'primary' : 'default'"
+            @click="viewType = 'table'"
+          >
+            <Icon icon="ep:list" />
+          </el-button>
+        </el-button-group>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap v-if="viewType === 'table'">
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="插件名称" align="center" prop="name" />
+      <el-table-column label="组件id" align="center" prop="pluginId" />
+      <el-table-column label="jar包" align="center" prop="file" />
+      <el-table-column label="版本号" align="center" prop="version" />
+      <el-table-column label="部署方式" align="center" prop="deployType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE" :value="scope.row.deployType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.IOT_PLUGIN_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['iot:plugin-info:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['iot:plugin-info:delete']"
+          >
+            删除
+          </el-button>
+          <el-button
+            link
+            type="info"
+            @click="viewDetail(scope.row.id)"
+          >
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 卡片视图 -->
+  <ContentWrap v-else>
+    <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
+      <el-card
+        v-for="item in list"
+        :key="item.pluginId"
+        class="cursor-pointer hover:shadow-lg transition-shadow"
+      >
+        <div class="flex items-center mb-4">
+          <div class="flex-1">
+            <div class="font-bold text-lg">{{ item.name }}</div>
+            <div class="text-gray-500 text-sm">组件ID: {{ item.pluginId }}</div>
+          </div>
+        </div>
+        <div class="text-sm text-gray-500">
+          <div>Jar包: {{ item.file }}</div>
+          <div>版本号: {{ item.version }}</div>
+          <div
+            >部署方式: <dict-tag :type="DICT_TYPE.IOT_PLUGIN_DEPLOY_TYPE" :value="item.deployType"
+          /></div>
+          <div>状态: <dict-tag :type="DICT_TYPE.IOT_PLUGIN_STATUS" :value="item.status" /></div>
+        </div>
+        <div class="flex justify-end mt-4">
+          <el-button
+            link
+            type="primary"
+            @click.stop="openForm('update', item.id)"
+            v-hasPermi="['iot:plugin-info:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click.stop="handleDelete(item.id)"
+            v-hasPermi="['iot:plugin-info:delete']"
+          >
+            删除
+          </el-button>
+        </div>
+      </el-card>
+    </div>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <PluginInfoForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { PluginInfoApi, PluginInfoVO } from '@/api/iot/plugininfo'
+import PluginInfoForm from './PluginInfoForm.vue'
+
+/** IoT 插件信息 列表 */
+defineOptions({ name: 'PluginInfo' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<PluginInfoVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const viewType = ref<'card' | 'table'>('table') // 视图类型,默认为表格视图
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await PluginInfoApi.getPluginInfoPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await PluginInfoApi.deletePluginInfo(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 查看详情操作 */
+const viewDetail = (id: number) => {
+  router.push({ path: `/iot/plugininfo/detail/${id}` })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 1 - 1
src/views/iot/product/product/detail/index.vue

@@ -18,7 +18,7 @@
 </template>
 <script lang="ts" setup>
 import { ProductApi, ProductVO } from '@/api/iot/product/product'
-import { DeviceApi } from '@/api/iot/device'
+import { DeviceApi } from '@/api/iot/device/device'
 import ProductDetailsHeader from './ProductDetailsHeader.vue'
 import ProductDetailsInfo from './ProductDetailsInfo.vue'
 import ProductTopic from './ProductTopic.vue'

+ 1 - 0
src/views/iot/thinkmodel/index.vue

@@ -1,3 +1,4 @@
+<!-- TODO 目录,应该是 thinkModel 哈。 -->
 <template>
   <ContentWrap>
     <!-- 搜索工作栏 -->