Procházet zdrojové kódy

Merge branch 'chengtao_monitor' of shuzhihua/pms-iot-vue into master

yanghao před 3 dny
rodič
revize
2ff7666893

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 VITE_DEV=true
 
 
 # 请求路径  http://192.168.188.149:48080  https://iot.deepoil.cc
 # 请求路径  http://192.168.188.149:48080  https://iot.deepoil.cc
-VITE_BASE_URL='http://192.168.188.149:48080'
+VITE_BASE_URL='http://192.168.188.198:48080'
 
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server
 VITE_UPLOAD_TYPE=server

+ 2 - 0
package.json

@@ -33,6 +33,7 @@
     "@fullcalendar/interaction": "^6.1.17",
     "@fullcalendar/interaction": "^6.1.17",
     "@fullcalendar/vue3": "^6.1.17",
     "@fullcalendar/vue3": "^6.1.17",
     "@iconify/iconify": "^3.1.1",
     "@iconify/iconify": "^3.1.1",
+    "@kjgl77/datav-vue3": "^1.7.4",
     "@microsoft/fetch-event-source": "^2.0.1",
     "@microsoft/fetch-event-source": "^2.0.1",
     "@number-flow/vue": "^0.4.8",
     "@number-flow/vue": "^0.4.8",
     "@types/echarts": "^5.0.0",
     "@types/echarts": "^5.0.0",
@@ -79,6 +80,7 @@
     "qs": "^6.12.0",
     "qs": "^6.12.0",
     "sortablejs": "1.15.0",
     "sortablejs": "1.15.0",
     "steady-xml": "^0.1.0",
     "steady-xml": "^0.1.0",
+    "three": "^0.182.0",
     "url": "^0.11.3",
     "url": "^0.11.3",
     "v3-jsoneditor": "^0.0.6",
     "v3-jsoneditor": "^0.0.6",
     "video.js": "^7.21.5",
     "video.js": "^7.21.5",

+ 61 - 0
pnpm-lock.yaml

@@ -32,6 +32,9 @@ importers:
       '@iconify/iconify':
       '@iconify/iconify':
         specifier: ^3.1.1
         specifier: ^3.1.1
         version: 3.1.1
         version: 3.1.1
+      '@kjgl77/datav-vue3':
+        specifier: ^1.7.4
+        version: 1.7.4(vue@3.5.12(typescript@5.3.3))
       '@microsoft/fetch-event-source':
       '@microsoft/fetch-event-source':
         specifier: ^2.0.1
         specifier: ^2.0.1
         version: 2.0.1
         version: 2.0.1
@@ -170,6 +173,9 @@ importers:
       steady-xml:
       steady-xml:
         specifier: ^0.1.0
         specifier: ^0.1.0
         version: 0.1.0
         version: 0.1.0
+      three:
+        specifier: ^0.182.0
+        version: 0.182.0
       url:
       url:
         specifier: ^0.11.3
         specifier: ^0.11.3
         version: 0.11.4
         version: 0.11.4
@@ -1388,6 +1394,21 @@ packages:
     resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
     resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
 
+  '@jiaminghi/bezier-curve@0.0.9':
+    resolution: {integrity: sha512-u9xJPOEl6Dri2E9FfmJoGxYQY7vYJkURNX04Vj64tdi535tPrpkuf9Sm0lNr3QTKdHQh0DdNRsaa62FLQNQEEw==}
+
+  '@jiaminghi/c-render@0.4.3':
+    resolution: {integrity: sha512-FJfzj5hGj7MLqqqI2D7vEzHKbQ1Ynnn7PJKgzsjXaZpJzTqs2Yw5OSeZnm6l7Qj7jyPAP53lFvEQNH4o4j6s+Q==}
+
+  '@jiaminghi/charts@0.2.18':
+    resolution: {integrity: sha512-K+HXaOOeWG9OOY1VG6M4mBreeeIAPhb9X+khG651AbnwEwL6G2UtcAQ8GWCq6GzhczcLwwhIhuaHqRygwHC0sA==}
+
+  '@jiaminghi/color@1.1.3':
+    resolution: {integrity: sha512-ZY3hdorgODk4OSTbxyXBPxAxHPIVf9rPlKJyK1C1db46a50J0reFKpAvfZG8zMG3lvM60IR7Qawgcu4ZDO3+Hg==}
+
+  '@jiaminghi/transition@1.1.11':
+    resolution: {integrity: sha512-owBggipoHMikDHHDW5Gc7RZYlVuvxHADiU4bxfjBVkHDAmmck+fCkm46n2JzC3j33hWvP9nSCAeh37t6stgWeg==}
+
   '@jridgewell/gen-mapping@0.3.5':
   '@jridgewell/gen-mapping@0.3.5':
     resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
     resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
     engines: {node: '>=6.0.0'}
     engines: {node: '>=6.0.0'}
@@ -1409,6 +1430,9 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
 
+  '@kjgl77/datav-vue3@1.7.4':
+    resolution: {integrity: sha512-zYVTVKkklUxwtiNKS1qPBilm4rTW+WItfp0zVpaRAI8wgXkLSPbDR9xPq2+UcU/Jft7/DVdMfBp709E2ResuPQ==}
+
   '@lezer/common@1.3.0':
   '@lezer/common@1.3.0':
     resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==}
     resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==}
 
 
@@ -4865,6 +4889,9 @@ packages:
   text-table@0.2.0:
   text-table@0.2.0:
     resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
     resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
 
 
+  three@0.182.0:
+    resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==}
+
   through@2.3.8:
   through@2.3.8:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
 
 
@@ -6579,6 +6606,28 @@ snapshots:
     dependencies:
     dependencies:
       '@sinclair/typebox': 0.27.8
       '@sinclair/typebox': 0.27.8
 
 
+  '@jiaminghi/bezier-curve@0.0.9':
+    dependencies:
+      '@babel/runtime': 7.26.0
+
+  '@jiaminghi/c-render@0.4.3':
+    dependencies:
+      '@babel/runtime': 7.26.0
+      '@jiaminghi/bezier-curve': 0.0.9
+      '@jiaminghi/color': 1.1.3
+      '@jiaminghi/transition': 1.1.11
+
+  '@jiaminghi/charts@0.2.18':
+    dependencies:
+      '@babel/runtime': 7.26.0
+      '@jiaminghi/c-render': 0.4.3
+
+  '@jiaminghi/color@1.1.3': {}
+
+  '@jiaminghi/transition@1.1.11':
+    dependencies:
+      '@babel/runtime': 7.26.0
+
   '@jridgewell/gen-mapping@0.3.5':
   '@jridgewell/gen-mapping@0.3.5':
     dependencies:
     dependencies:
       '@jridgewell/set-array': 1.2.1
       '@jridgewell/set-array': 1.2.1
@@ -6601,6 +6650,16 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/sourcemap-codec': 1.5.0
 
 
+  '@kjgl77/datav-vue3@1.7.4(vue@3.5.12(typescript@5.3.3))':
+    dependencies:
+      '@jiaminghi/c-render': 0.4.3
+      '@jiaminghi/charts': 0.2.18
+      '@jiaminghi/color': 1.1.3
+      '@vueuse/core': 10.11.1(vue@3.5.12(typescript@5.3.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
   '@lezer/common@1.3.0': {}
   '@lezer/common@1.3.0': {}
 
 
   '@lezer/highlight@1.2.3':
   '@lezer/highlight@1.2.3':
@@ -10338,6 +10397,8 @@ snapshots:
 
 
   text-table@0.2.0: {}
   text-table@0.2.0: {}
 
 
+  three@0.182.0: {}
+
   through@2.3.8: {}
   through@2.3.8: {}
 
 
   tiny-svg@3.1.3: {}
   tiny-svg@3.1.3: {}

binární
public/model/industrialEquipment2.glb


binární
public/model/test2.glb


+ 5 - 0
src/api/pms/device/index.ts

@@ -212,5 +212,10 @@ export const IotDeviceApi = {
   // 设备报表导出
   // 设备报表导出
   exportDeviceReport: async (params) => {
   exportDeviceReport: async (params) => {
     return await request.download({ url: `/rq/report/export-excel`, params })
     return await request.download({ url: `/rq/report/export-excel`, params })
+  },
+
+  // 获取成套设备参数
+  getDeviceSetParams: async (id) => {
+    return await request.get({ url: `/rq/iot-device-group/td/${id}` })
   }
   }
 }
 }

binární
src/assets/imgs/green.png


binární
src/assets/imgs/red.png


+ 3 - 0
src/main.ts

@@ -42,6 +42,8 @@ import Logger from '@/utils/Logger'
 
 
 import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
 import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
 
 
+import DataVVue3 from '@kjgl77/datav-vue3'
+
 // 创建实例
 // 创建实例
 const setupAll = async () => {
 const setupAll = async () => {
   const app = createApp(App)
   const app = createApp(App)
@@ -63,6 +65,7 @@ const setupAll = async () => {
   setupMountedFocus(app)
   setupMountedFocus(app)
 
 
   await router.isReady()
   await router.isReady()
+  app.use(DataVVue3)
 
 
   app.use(VueDOMPurifyHTML)
   app.use(VueDOMPurifyHTML)
 
 

+ 46 - 0
src/router/modules/remaining.ts

@@ -1780,6 +1780,52 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
       }
     ]
     ]
   },
   },
+  // {
+  //   path: '/device_monitor',
+  //   component: Layout,
+  //   name: 'kanban',
+  //   meta: { hidden: true },
+  //   children: [
+  //     {
+  //       path: 'kanban',
+  //       name: 'DeviceKanban',
+  //       meta: {
+  //         title: '设备看板',
+  //         noCache: true,
+  //         hidden: true
+  //       },
+  //       component: () => import('@/views/pms/monitor/kanban.vue')
+  //     }
+  //   ]
+  // },
+  {
+    path: '/kanban',
+    component: Layout,
+    name: 'kanban',
+    meta: { hidden: true },
+    children: [
+      {
+        path: 'monitor/kanban',
+        name: 'Kanban',
+        meta: {
+          title: '设备看板',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/pms/monitor/kanban.vue')
+      },
+      {
+        path: 'monitor/kanbanOther',
+        name: 'KanbanOther',
+        meta: {
+          title: '设备看板',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/pms/monitor/kanban2.vue')
+      }
+    ]
+  },
   {
   {
     path: '/diy',
     path: '/diy',
     name: 'DiyCenter',
     name: 'DiyCenter',

+ 63 - 0
src/styles/index.scss

@@ -35,3 +35,66 @@
     border-left-color: var(--el-color-primary);
     border-left-color: var(--el-color-primary);
   }
   }
 }
 }
+
+/* 全屏模式样式 */
+.app-fullscreen {
+  /* 隐藏所有布局相关元素 */
+  .layout-aside,
+  .sidebar-container,
+  .el-aside,
+  .app-header,
+  .layout-header,
+  .el-header,
+  .tags-view-container,
+  .app-footer,
+  .el-footer,
+  .layout-border__right,
+  .layout-border__top,
+  .layout-border__bottom,
+  .fixed.top-0.left-0.z-10,
+  .bg-\[var\(--top-header-bg-color\)\] {
+    display: none !important;
+  }
+
+  /* 调整主应用容器 */
+  .app-wrapper,
+  .layout-container,
+  .layout-content,
+  .app-main,
+  .main-container,
+  .layout-app-main {
+    position: fixed !important;
+    top: 0 !important;
+    left: 0 !important;
+    width: 100vw !important;
+    height: 100vh !important;
+    margin: 0 !important;
+    padding: 0 !important;
+
+    overflow: hidden !important;
+  }
+
+  /* 确保内容区域完全填充 */
+  .content-container,
+  .p-\[var\(--app-content-padding\)\].w-full {
+    height: 100% !important;
+    width: 100% !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: hidden !important;
+  }
+
+  .min-h-screen {
+    height: 100vh !important;
+    width: 100vw !important;
+    margin: 0 !important;
+    padding: 0 !important;
+
+    position: fixed !important;
+    top: 0 !important;
+    left: 0 !important;
+    overflow: hidden !important;
+    max-width: 100vw !important;
+    max-height: 100vh !important;
+  }
+}

+ 40 - 38
src/utils/dict.ts

@@ -109,15 +109,15 @@ export const getDictLabel = (dictType: string, value: any): string => {
 export enum DICT_TYPE {
 export enum DICT_TYPE {
   PMS_MAINTAIN_METHOD = 'maintain_method',
   PMS_MAINTAIN_METHOD = 'maintain_method',
   IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
   IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
-  PMS_INSPECT_ORDER_STATUS = "pms_inspect_order_status",
-  OPERATION_FILL_ORDER_STATUS = "operation_fill_order_status",
-  OPERATION_FILL_DEVICE_STATUS = "operation_fill_device_status",
-  PMS_INSPECT_UNIT = "pms_inspect_unit",
+  PMS_INSPECT_ORDER_STATUS = 'pms_inspect_order_status',
+  OPERATION_FILL_ORDER_STATUS = 'operation_fill_order_status',
+  OPERATION_FILL_DEVICE_STATUS = 'operation_fill_device_status',
+  PMS_INSPECT_UNIT = 'pms_inspect_unit',
   PMS_MAIN_STATUS_NO = 'pms_maintain_status_no',
   PMS_MAIN_STATUS_NO = 'pms_maintain_status_no',
   PMS_MAIN_STATUS = 'pms_maintain_status',
   PMS_MAIN_STATUS = 'pms_maintain_status',
-  PMS_MAIN_TYPE = "pms_main_type",
-  PMS_MAIN_CLASSIFY = "pms_maintain_classify",
-  PMS_BOOLEAN = "pms_boolean",
+  PMS_MAIN_TYPE = 'pms_main_type',
+  PMS_MAIN_CLASSIFY = 'pms_maintain_classify',
+  PMS_BOOLEAN = 'pms_boolean',
   PMS_FAILURE_STATUS = 'pms_failure_status',
   PMS_FAILURE_STATUS = 'pms_failure_status',
   PMS_FILE_TYPE = 'pms_file_type',
   PMS_FILE_TYPE = 'pms_file_type',
   PMS_DEVICE_STATUS = 'pms_device_status',
   PMS_DEVICE_STATUS = 'pms_device_status',
@@ -128,14 +128,14 @@ export enum DICT_TYPE {
   DEPT_TYPE = 'dept_type',
   DEPT_TYPE = 'dept_type',
   TERMINAL = 'terminal', // 终端
   TERMINAL = 'terminal', // 终端
   DATE_INTERVAL = 'date_interval', // 数据间隔
   DATE_INTERVAL = 'date_interval', // 数据间隔
-// ========== supplier 模块 ==========
-  SUPPLIER_TYPE = "supplier_classification",
-  PMS_SUPPLIER_NATURE = "pms_supplier_nature",
-  SUPPLIER_COMPANY_TYPE = "supplier_type",
-  SUPPLIER_NATURE = "supplier_nature",
-  SUPPLIER_STATUS = "supplier_status",
-  SUPPLIER_SIZE = "supplier_size",
-  SUPPLIER_CERT = "supplier_certificate",
+  // ========== supplier 模块 ==========
+  SUPPLIER_TYPE = 'supplier_classification',
+  PMS_SUPPLIER_NATURE = 'pms_supplier_nature',
+  SUPPLIER_COMPANY_TYPE = 'supplier_type',
+  SUPPLIER_NATURE = 'supplier_nature',
+  SUPPLIER_STATUS = 'supplier_status',
+  SUPPLIER_SIZE = 'supplier_size',
+  SUPPLIER_CERT = 'supplier_certificate',
   // ========== SYSTEM 模块 ==========
   // ========== SYSTEM 模块 ==========
   SYSTEM_USER_SEX = 'system_user_sex',
   SYSTEM_USER_SEX = 'system_user_sex',
   SYSTEM_MENU_TYPE = 'system_menu_type',
   SYSTEM_MENU_TYPE = 'system_menu_type',
@@ -271,33 +271,35 @@ export enum DICT_TYPE {
   IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
   IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
 
 
   // ========== PMS模块  ==========
   // ========== PMS模块  ==========
-  PMS_INSPECT_WRITE = "inspect_wirte_normal",
+  PMS_INSPECT_WRITE = 'inspect_wirte_normal',
   PMS_BOM_NODE_EXT_ATTR = 'BOM_NODE_EXT_ATTR', // BOM节点扩展属性 维护 or 保养
   PMS_BOM_NODE_EXT_ATTR = 'BOM_NODE_EXT_ATTR', // BOM节点扩展属性 维护 or 保养
   PMS_MAIN_WORK_ORDER_TYPE = 'pms_main_work_order_type', // 保养工单类型
   PMS_MAIN_WORK_ORDER_TYPE = 'pms_main_work_order_type', // 保养工单类型
   PMS_MAIN_WORK_ORDER_RESULT = 'pms_main_work_order_result', // 保养工单状态
   PMS_MAIN_WORK_ORDER_RESULT = 'pms_main_work_order_result', // 保养工单状态
-  RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection',//是否数采
-  RQ_IOT_ISREPORT = 'rq_iot_isReport',//日报是否取值
+  RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection', //是否数采
+  RQ_IOT_ISREPORT = 'rq_iot_isReport', //日报是否取值
   RQ_IOT_MODEL_TEMPLATE_ATTR = 'rq_iot_model_template_attr',
   RQ_IOT_MODEL_TEMPLATE_ATTR = 'rq_iot_model_template_attr',
-  RQ_IOT_MODEL_PLUS = "rq_iot_model_plus",
-  RQ_IOT_SUM = 'rq_iot_isSum',//是否累计
-  PMS_ORDER_PROCESS_MODE = "pms_main_work_order_process_mode",  // 保养方式 0内部保养  1委外保养
+  RQ_IOT_MODEL_PLUS = 'rq_iot_model_plus',
+  RQ_IOT_SUM = 'rq_iot_isSum', //是否累计
+  PMS_ORDER_PROCESS_MODE = 'pms_main_work_order_process_mode', // 保养方式 0内部保养  1委外保养
   PMS_THING_MODEL_UNIT = 'pms_thing_model_unit', // pms属性模板单位
   PMS_THING_MODEL_UNIT = 'pms_thing_model_unit', // pms属性模板单位
-  PMS_PROJECT_TASK_SCHEDULE = 'constructionStatus',  // 日报 项目管理 任务计划
-  PMS_PROJECT_TASK_RY_SCHEDULE = 'rigStatus',  // 日报 项目管理 瑞鹰项目进度
-  PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE = 'repairStatus',  // 日报 瑞鹰 修井 项目进度
-  PMS_PROJECT_SETTLEMENT = 'rq_iot_project_settlement_method',  // 日报 项目管理 结算方式
-  PMS_PROJECT_OVERSEA_FLAG = 'rq_iot_project_oversea_flag',  // 日报 项目管理 是否海外项目
-  PMS_PROJECT_AREA = 'rq_iot_project_area',  // 日报 项目管理 海外项目所属区域
-  PMS_PROJECT_WELL_TYPE = 'rq_iot_project_well_type',  // 日报 项目管理 井型
-  PMS_PROJECT_WELL_CATEGORY = 'rq_iot_project_well_category',  // 日报 项目管理 井别
-  PMS_PROJECT_TECHNOLOGY = 'rq_iot_project_technology',  // 日报 项目管理 施工工艺
+  PMS_PROJECT_TASK_SCHEDULE = 'constructionStatus', // 日报 项目管理 任务计划
+  PMS_PROJECT_TASK_RY_SCHEDULE = 'rigStatus', // 日报 项目管理 瑞鹰项目进度
+  PMS_PROJECT_TASK_RY_REPAIR_SCHEDULE = 'repairStatus', // 日报 瑞鹰 修井 项目进度
+  PMS_PROJECT_SETTLEMENT = 'rq_iot_project_settlement_method', // 日报 项目管理 结算方式
+  PMS_PROJECT_OVERSEA_FLAG = 'rq_iot_project_oversea_flag', // 日报 项目管理 是否海外项目
+  PMS_PROJECT_AREA = 'rq_iot_project_area', // 日报 项目管理 海外项目所属区域
+  PMS_PROJECT_WELL_TYPE = 'rq_iot_project_well_type', // 日报 项目管理 井型
+  PMS_PROJECT_WELL_CATEGORY = 'rq_iot_project_well_category', // 日报 项目管理 井别
+  PMS_PROJECT_TECHNOLOGY = 'rq_iot_project_technology', // 日报 项目管理 施工工艺
   PMS_PROJECT_RY_TECHNOLOGY = 'rq_iot_project_technology_ry', // 瑞鹰施工工艺
   PMS_PROJECT_RY_TECHNOLOGY = 'rq_iot_project_technology_ry', // 瑞鹰施工工艺
-  PMS_PROJECT_WORKLOAD_UNIT = 'rq_iot_project_measure_unit',  // 日报 项目管理 工作量单位
-  PMS_PROJECT_WORK_AREA = 'rq_iot_project_work_area',     // 日报 施工区域
-  PMS_PROJECT_NPT_REASON = 'nptReason',    // 日报 非生产时间原因
-  PMS_PROJECT_RY_NPT_REASON = 'ryNptReason',  // 瑞鹰日报 非生产时间原因
-  PMS_PROJECT_WELL_CONTROL_LEVEL = 'rq_iot_well_control_level',  // 井控级别
-  PMS_PROJECT_RD_STATUS = 'rdStatus',                  // 瑞都 施工状态
-  PMS_PROJECT_CASING_PIPE_SIZE = 'rq_iot_casing_pipe_size',    // 日报 套生段产管尺寸
-  PMS_PROJECT_RD_TECHNOLOGY = 'rq_iot_project_technology_rd'   // 瑞都施工工艺
+  PMS_PROJECT_WORKLOAD_UNIT = 'rq_iot_project_measure_unit', // 日报 项目管理 工作量单位
+  PMS_PROJECT_WORK_AREA = 'rq_iot_project_work_area', // 日报 施工区域
+  PMS_PROJECT_NPT_REASON = 'nptReason', // 日报 非生产时间原因
+  PMS_PROJECT_RY_NPT_REASON = 'ryNptReason', // 瑞鹰日报 非生产时间原因
+  PMS_PROJECT_WELL_CONTROL_LEVEL = 'rq_iot_well_control_level', // 井控级别
+  PMS_PROJECT_RD_STATUS = 'rdStatus', // 瑞都 施工状态
+  PMS_PROJECT_CASING_PIPE_SIZE = 'rq_iot_casing_pipe_size', // 日报 套生段产管尺寸
+  PMS_PROJECT_RD_TECHNOLOGY = 'rq_iot_project_technology_rd', // 瑞都施工工艺
+
+  DEVICE_GROUP_TYPE = 'device_group_type'
 }
 }

+ 28 - 3
src/views/pms/device/completeSet/DeviceCompleteSet.vue

@@ -58,6 +58,11 @@
           </el-table-column>
           </el-table-column>
           <el-table-column label="部门名称" align="center" prop="deptName" />
           <el-table-column label="部门名称" align="center" prop="deptName" />
           <el-table-column label="成套名称" align="center" prop="name" />
           <el-table-column label="成套名称" align="center" prop="name" />
+          <el-table-column label="成套类型" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="scope.row.type" />
+            </template>
+          </el-table-column>
 
 
           <el-table-column label="描述" align="center" prop="remark" />
           <el-table-column label="描述" align="center" prop="remark" />
           <el-table-column label="设备数量" align="center" prop="deviceCount">
           <el-table-column label="设备数量" align="center" prop="deviceCount">
@@ -102,13 +107,31 @@
     </template>
     </template>
     <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
     <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
       <el-row :gutter="20">
       <el-row :gutter="20">
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="成套名称" prop="name">
           <el-form-item label="成套名称" prop="name">
             <el-input v-model="formData.name" placeholder="请输入成套名称" />
             <el-input v-model="formData.name" placeholder="请输入成套名称" />
           </el-form-item>
           </el-form-item>
         </el-col>
         </el-col>
 
 
-        <el-col :span="12">
+        <el-col :span="8">
+          <el-form-item label="成套类型" prop="type">
+            <el-select
+              v-model="formData.type"
+              placeholder="请选择成套类型"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.DEVICE_GROUP_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="8">
           <el-form-item :label="t('iotDevice.dept')" prop="deptId">
           <el-form-item :label="t('iotDevice.dept')" prop="deptId">
             <el-tree-select
             <el-tree-select
               v-model="formData.deptId"
               v-model="formData.deptId"
@@ -198,6 +221,7 @@ import DeptTree from '@/views/system/user/DeptTree.vue'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import * as DeptApi from '@/api/system/dept'
 import { ElMessageBox } from 'element-plus'
 import { ElMessageBox } from 'element-plus'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 const deptList = ref<Tree[]>([]) // 树形结构
 const deptList = ref<Tree[]>([]) // 树形结构
 
 
 defineOptions({ name: 'IotDeviceComplete' })
 defineOptions({ name: 'IotDeviceComplete' })
@@ -231,7 +255,8 @@ const formData = ref({
   name: '',
   name: '',
   details: [],
   details: [],
   deptId: '',
   deptId: '',
-  remark: ''
+  remark: '',
+  type: ''
 })
 })
 
 
 // 表单验证规则
 // 表单验证规则

+ 116 - 0
src/views/pms/monitor/ModelViewer.vue

@@ -0,0 +1,116 @@
+<template>
+  <div ref="container" class="w-full h-full z-999 cursor-pointer"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import * as THREE from 'three'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+const container = ref<HTMLDivElement>()
+
+let scene: THREE.Scene
+let camera: THREE.PerspectiveCamera
+let renderer: THREE.WebGLRenderer
+let controls: OrbitControls
+let model: THREE.Group
+
+const init = () => {
+  scene = new THREE.Scene()
+  // scene.background = new THREE.Color(0x0a0a0a) // 移除深色背景
+
+  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
+  camera.position.set(3, 3, 3)
+  camera.lookAt(0, 0, 0)
+
+  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+  renderer.setSize(2000, 1000)
+  renderer.shadowMap.enabled = true
+  renderer.shadowMap.type = THREE.PCFSoftShadowMap
+  container.value?.appendChild(renderer.domElement)
+
+  controls = new OrbitControls(camera, renderer.domElement)
+  controls.enableDamping = true
+  controls.dampingFactor = 0.05
+  controls.enableZoom = true
+  controls.enablePan = false
+
+  const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 2) // 增加强度从1到2
+  scene.add(hemisphereLight)
+
+  const pointLight = new THREE.PointLight(0xffffff, 2, 100) // 增加强度从1到2
+  pointLight.position.set(0, 5, 5)
+  scene.add(pointLight)
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) // 增加强度从0.8到1.5
+  directionalLight.position.set(10, 10, 5)
+  directionalLight.castShadow = true
+  scene.add(directionalLight)
+
+  // 添加额外的环境光
+  const ambientLight = new THREE.AmbientLight(0x404040, 0.6) // 添加环境光
+  scene.add(ambientLight)
+
+  const modelUrl = '/model/test2.glb'
+  console.log('模型URL:', modelUrl)
+  const loader = new GLTFLoader()
+  loader.load(
+    modelUrl,
+    (gltf) => {
+      console.log('模型加载成功:', gltf)
+      model = gltf.scene
+      model.scale.set(7, 7, 7) // 增加缩放
+      model.position.set(0, 0, 0)
+      scene.add(model)
+    },
+    (progress) => {
+      console.log('模型加载进度:', progress) // 加载进度
+    },
+    (error) => {
+      console.error('模型加载失败:', error)
+    }
+  )
+
+  const animate = () => {
+    requestAnimationFrame(animate)
+    controls.update()
+    if (model) {
+      model.rotation.y += 0.005
+    } else {
+      scene.children.forEach((child) => {
+        if (child instanceof THREE.Mesh && child.geometry instanceof THREE.BoxGeometry) {
+          child.rotation.y += 0.01
+        }
+      })
+    }
+    renderer.render(scene, camera)
+  }
+  animate()
+
+  const resize = () => {
+    if (container.value) {
+      const width = container.value.clientWidth
+      const height = container.value.clientHeight
+      camera.aspect = width / height
+      camera.updateProjectionMatrix()
+      renderer.setSize(width, height)
+    }
+  }
+  window.addEventListener('resize', resize)
+  resize()
+}
+
+onMounted(() => {
+  init()
+})
+
+onUnmounted(() => {
+  if (renderer) {
+    renderer.dispose()
+  }
+  if (controls) {
+    controls.dispose()
+  }
+})
+</script>

+ 116 - 0
src/views/pms/monitor/ModelViewer2.vue

@@ -0,0 +1,116 @@
+<template>
+  <div ref="container" class="w-full h-full z-999 cursor-pointer"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import * as THREE from 'three'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+const container = ref<HTMLDivElement>()
+
+let scene: THREE.Scene
+let camera: THREE.PerspectiveCamera
+let renderer: THREE.WebGLRenderer
+let controls: OrbitControls
+let model: THREE.Group
+
+const init = () => {
+  scene = new THREE.Scene()
+  // scene.background = new THREE.Color(0x0a0a0a) // 移除深色背景
+
+  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
+  camera.position.set(3, 3, 3)
+  camera.lookAt(0, 0, 0)
+
+  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+  renderer.setSize(2000, 1000)
+  renderer.shadowMap.enabled = true
+  renderer.shadowMap.type = THREE.PCFSoftShadowMap
+  container.value?.appendChild(renderer.domElement)
+
+  controls = new OrbitControls(camera, renderer.domElement)
+  controls.enableDamping = true
+  controls.dampingFactor = 0.05
+  controls.enableZoom = true
+  controls.enablePan = false
+
+  const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 2) // 增加强度从1到2
+  scene.add(hemisphereLight)
+
+  const pointLight = new THREE.PointLight(0xffffff, 2, 100) // 增加强度从1到2
+  pointLight.position.set(0, 5, 5)
+  scene.add(pointLight)
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) // 增加强度从0.8到1.5
+  directionalLight.position.set(10, 10, 5)
+  directionalLight.castShadow = true
+  scene.add(directionalLight)
+
+  // 添加额外的环境光
+  const ambientLight = new THREE.AmbientLight(0x404040, 0.6) // 添加环境光
+  scene.add(ambientLight)
+
+  const modelUrl = '/model/industrialEquipment2.glb'
+  console.log('模型URL:', modelUrl)
+  const loader = new GLTFLoader()
+  loader.load(
+    modelUrl,
+    (gltf) => {
+      console.log('模型加载成功:', gltf)
+      model = gltf.scene
+      model.scale.set(7, 7, 7) // 增加缩放
+      model.position.set(0, 0, 0)
+      scene.add(model)
+    },
+    (progress) => {
+      console.log('模型加载进度:', progress) // 加载进度
+    },
+    (error) => {
+      console.error('模型加载失败:', error)
+    }
+  )
+
+  const animate = () => {
+    requestAnimationFrame(animate)
+    controls.update()
+    if (model) {
+      model.rotation.y += 0.005
+    } else {
+      scene.children.forEach((child) => {
+        if (child instanceof THREE.Mesh && child.geometry instanceof THREE.BoxGeometry) {
+          child.rotation.y += 0.01
+        }
+      })
+    }
+    renderer.render(scene, camera)
+  }
+  animate()
+
+  const resize = () => {
+    if (container.value) {
+      const width = container.value.clientWidth
+      const height = container.value.clientHeight
+      camera.aspect = width / height
+      camera.updateProjectionMatrix()
+      renderer.setSize(width, height)
+    }
+  }
+  window.addEventListener('resize', resize)
+  resize()
+}
+
+onMounted(() => {
+  init()
+})
+
+onUnmounted(() => {
+  if (renderer) {
+    renderer.dispose()
+  }
+  if (controls) {
+    controls.dispose()
+  }
+})
+</script>

+ 40 - 0
src/views/pms/monitor/data-row.vue

@@ -0,0 +1,40 @@
+<template>
+  <div
+    style="border-bottom: 0.5px solid rgba(255, 255, 255, 0.1); border-radius: 5px"
+    :class="[
+      'flex items-center justify-between border-b border-cyan-500/20 hover:bg-cyan-500/10 transition-colors px-2 -mx-2 rounded',
+      compact ? 'py-1' : 'py-2'
+    ]"
+  >
+    <span class="text-cyan-200">{{ label }}</span>
+    <div class="flex items-center gap-2">
+      <span class="font-mono text-white font-bold">{{ value }}</span>
+      <button
+        v-if="button"
+        style="border-radius: 5px"
+        :class="[
+          'px-3 py-1 rounded text-white text-sm transition-all',
+          buttonColor === 'green'
+            ? 'bg-green-600 hover:bg-green-500 shadow-[0_0_10px_rgba(22,163,74,0.5)]'
+            : 'bg-red-600 hover:bg-red-500 shadow-[0_0_10px_rgba(220,38,38,0.5)]'
+        ]"
+      >
+        {{ button }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+interface Props {
+  label: string
+  value: string
+  button?: string
+  buttonColor?: 'green' | 'red'
+  compact?: boolean
+}
+
+withDefaults(defineProps<Props>(), {
+  compact: false
+})
+</script>

+ 204 - 0
src/views/pms/monitor/data.txt

@@ -0,0 +1,204 @@
+{
+    "deptName": "HY-A41",
+    "groupName": "塔河A41",
+    "totalRuntime": 25838.2,
+    "增压机": [
+        {
+            "modelName": "急停状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二套",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "瞬时流量",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "表套",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "油泵状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "累计流量",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "无油1#",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "无油2#",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "风机状态",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "氮气纯度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "曲轴油位",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "振动",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "油压",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "套压",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "润滑油温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度6",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度5",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度4",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度3",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度2",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "轴瓦温度1",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水出口压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水进口压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "润滑油压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "五级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "三级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级排气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级进气压力",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "冷却水温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "最终排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "五级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度3",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度2",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "四级排气温度1",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "三级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "二级排气温度",
+            "value": "",
+            "sort": null
+        },
+        {
+            "modelName": "一级排气温度",
+            "value": "",
+            "sort": null
+        }
+    ],
+    "wellName": "YD1-5CX,YD1-5",
+    "deviceCode": "YF520"
+}

+ 501 - 0
src/views/pms/monitor/index.vue

@@ -0,0 +1,501 @@
+<template>
+  <el-row :gutter="20">
+    <!-- 左侧部门树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1" style="border: 0" v-if="treeShow">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="contentSpan" :xs="24">
+      <ContentWrap style="border: 0">
+        <!-- 搜索工作栏 -->
+        <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-200px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <!-- 列表 -->
+      <ContentWrap style="border: 0">
+        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          <el-table-column :label="t('monitor.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="部门名称" align="center" prop="deptName" />
+          <el-table-column label="成套名称" align="center" prop="name" />
+          <el-table-column label="成套类型" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="scope.row.type" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="描述" align="center" prop="remark" />
+          <el-table-column label="设备数量" align="center" prop="deviceCount">
+            <template #default="scope">
+              {{ (scope.row.details && scope.row.details.length) || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="主设备" align="center" prop="mainDeviceName">
+            <template #default="scope">
+              {{
+                scope.row.details.filter((item) => item.ifMaster)[0]?.deviceName +
+                  ' ' +
+                  scope.row.details.filter((item) => item.ifMaster)[0]?.deviceCode || '无'
+              }}
+            </template>
+          </el-table-column>
+
+          <el-table-column :label="t('devicePerson.operation')" align="center" min-width="120px">
+            <template #default="scope">
+              <el-button link type="primary" @click="handleEdit(scope.row)"> 查看详情 </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 新增/编辑成套设备对话框 -->
+  <el-dialog :title="dialogTitle" v-model="dialogVisible" width="800px" @close="cancel">
+    <template #header>
+      <div class="my-header" style="padding-bottom: 20px">
+        <span>{{ dialogTitle }}</span>
+      </div>
+    </template>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="成套名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入成套名称" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item :label="t('iotDevice.dept')" prop="deptId">
+            <el-tree-select
+              v-model="formData.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+              @change="handleDeptChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="描述" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              placeholder="请输入描述"
+              :rows="2"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="选择设备" prop="devices">
+            <div class="transfer-container">
+              <el-transfer
+                v-model="selectedDeviceIds"
+                :data="deviceOptions"
+                :titles="['设备列表', '已选择设备']"
+                :button-texts="['移除', '添加']"
+                filterable
+                :filter-method="filterDeviceMethod"
+                filter-placeholder="请输入设备名称"
+                @change="rightDeviceChange"
+              >
+                <template #left-empty>
+                  <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
+                </template>
+                <template #right-empty>
+                  <el-empty :image-size="60" :description="isEdit ? '加载中...' : '请选择设备'" />
+                </template>
+              </el-transfer>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" v-if="selectedDevices.length > 0">
+        <el-col :span="12">
+          <el-form-item label="设置主设备" prop="mainDevice" label-width="100">
+            <el-select
+              v-model="mainDeviceId"
+              placeholder="请选择主设备"
+              clearable
+              filterable
+              @change="setMainDevice"
+            >
+              <el-option
+                v-for="device in selectedDevices"
+                :key="device.id"
+                :label="device.label"
+                :value="device.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="cancel">取 消</el-button>
+      <el-button type="primary" @click="submit">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { IotDeviceApi } from '@/api/pms/device'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
+const deptList = ref<Tree[]>([]) // 树形结构
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+defineOptions({ name: 'IotDeviceComplete' })
+
+const loading = ref(true) // 列表的加载中
+
+const { t } = useI18n()
+
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptName: undefined,
+  name: undefined,
+  deptId: undefined
+})
+const queryFormRef = ref(null) // 搜索的表单
+
+const contentSpan = ref(20)
+const treeShow = ref(true)
+
+// 对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+
+// 表单相关
+const formRef = ref()
+const formData = ref({
+  name: '',
+  details: [],
+  deptId: '',
+  remark: ''
+})
+
+// 表单验证规则
+const formRules = {
+  name: [{ required: true, message: '成套名称不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '成套编码不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
+  devices: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (selectedDeviceIds.value.length === 0) {
+          callback(new Error('请至少选择一个设备'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ],
+  mainDevice: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (!mainDeviceId.value) {
+          callback(new Error('请选择主设备'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+
+// 部门树数据
+const selectedDeptId = ref<number | string>('')
+
+// 新增时部门改变时获取设备列表
+const handleDeptChange = async (deptId) => {
+  if (deptId) {
+    selectedDeptId.value = deptId
+    getDeviceList(deptId)
+  }
+
+  // 清空主设备she 设备列表
+  selectedDevices.value = []
+  mainDeviceId.value = ''
+  selectedDeviceIds.value = []
+}
+// 获取设备列表
+const getDeviceList = async (deptId) => {
+  try {
+    const res = await IotDeviceApi.getIotDeviceSetOptions(deptId)
+    deviceOptions.value = res.map((item) => ({
+      key: item.id, // 始终使用id作为key
+      label: `${item.deviceName} (${item.deviceCode})`,
+      ...item
+    }))
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+// 设备选择相关
+const deviceOptions = ref<any[]>([])
+const selectedDeviceIds = ref([])
+const selectedDevices = ref([])
+const mainDeviceId = ref('')
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotDeviceApi.getIotDeviceSetList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 首页处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+// 设备过滤方法
+const filterDeviceMethod = (query, item) => {
+  return item.label.toLowerCase().includes(query.toLowerCase())
+}
+
+// 更新已选择的设备列表
+const updateSelectedDevices = () => {
+  // 根据 selectedDeviceIds 从 deviceOptions 中找到对应的设备
+  selectedDevices.value = deviceOptions.value.filter((item) =>
+    selectedDeviceIds.value.includes(item.key)
+  )
+
+  console.log('selectedDevices>>>>>>>>>>>>>>>>', selectedDevices.value)
+
+  // 构建 details 数据
+  formData.value.details = selectedDevices.value.map((item) => ({
+    deviceId: item.id,
+    deviceName: item.deviceName,
+    deviceCode: item.deviceCode,
+    deptId: item.deptId,
+    ifMaster: item.id === mainDeviceId.value ? true : false
+  }))
+
+  // 如果主设备不在当前选择中,则清空主设备
+  if (
+    mainDeviceId.value &&
+    !selectedDevices.value.some((device) => device.id === mainDeviceId.value)
+  ) {
+    mainDeviceId.value = ''
+  }
+}
+
+const rightDeviceChange = (val) => {
+  selectedDeviceIds.value = val
+  updateSelectedDevices()
+
+  // 手动触发验证
+  if (formRef.value) {
+    formRef.value.validateField('devices')
+    if (val.length > 0) {
+      formRef.value.validateField('mainDevice')
+    }
+  }
+}
+
+// 设置主设备
+const setMainDevice = (val) => {
+  mainDeviceId.value = val
+  // 更新 details 中的 ifMaster 字段
+  console.log('selectedDevices.value>>>>>>>>>>>>>>>>', selectedDevices.value)
+
+  formData.value.details = selectedDevices.value.map((item) => ({
+    deviceId: item.id,
+    deviceName: item.deviceName,
+    deviceCode: item.deviceCode,
+    deptId: item.deptId,
+    ifMaster: item.id === mainDeviceId.value ? true : false
+  }))
+
+  console.log('formData.value.details>>>>>>>>>>>>>>>>', formData.value.details)
+
+  // 手动触发验证
+  if (formRef.value) {
+    formRef.value.validateField('mainDevice')
+  }
+}
+
+// 显示新增对话框
+const handleAdd = () => {
+  isEdit.value = false
+  dialogTitle.value = '新增成套设备'
+  resetForm()
+  dialogVisible.value = true
+}
+
+// 显示编辑对话框
+const handleEdit = (row) => {
+  if (row.type === '1') {
+    router.push({ path: '/kanban/monitor/kanban', query: { deviceSetId: row.id } })
+  } else {
+    router.push({ path: '/kanban/monitor/kanbanOther', query: { deviceSetId: row.id } })
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    details: [],
+    deptId: '',
+    remark: ''
+  }
+  selectedDeviceIds.value = []
+  selectedDevices.value = []
+  deviceOptions.value = []
+  mainDeviceId.value = ''
+}
+
+// 取消操作
+const cancel = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+
+// 提交表单
+const submit = async () => {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+    try {
+      const data = {
+        ...formData.value
+      }
+
+      console.log('提交数据:', data)
+
+      if (isEdit.value) {
+        await IotDeviceApi.updateIotDeviceSet(data)
+        ElMessage.success('编辑成功')
+      } else {
+        await IotDeviceApi.createIotDeviceSet(data)
+        ElMessage.success('新增成功')
+      }
+
+      dialogVisible.value = false
+      getList()
+    } catch (error) {
+      console.error(error)
+    }
+  })
+}
+
+onMounted(async () => {
+  getList()
+
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+
+<style scoped>
+.transfer-container {
+  display: flex;
+  flex-direction: column;
+}
+
+.transfer-footer {
+  margin-top: 8px;
+  text-align: right;
+}
+
+::deep(.el-transfer-panel) {
+  width: 300px;
+}
+
+::deep(.el-tree--highlight-current) {
+  height: 200px !important;
+}
+
+/* 调整穿梭框面板主体高度 */
+::deep(.el-transfer-panel__body) {
+  height: 850px !important;
+}
+
+/* 如果需要进一步调整,可以分别设置左右面板的高度 */
+::deep(.el-transfer-panel .el-transfer-panel__list) {
+  height: 850px !important; /* 调整列表区域高度 */
+}
+
+.el-transfer {
+  --el-transfer-panel-body-height: 500px;
+}
+</style>

+ 442 - 0
src/views/pms/monitor/kanban.vue

@@ -0,0 +1,442 @@
+<template>
+  <div
+    class="bg-gradient-to-br from-slate-950 via-blue-950 to-slate-900 text-cyan-100 p-6 relative"
+    :class="{ 'fullscreen-layout': isFullscreen }"
+  >
+    <!-- Animated background grid -->
+    <div class="absolute inset-0 opacity-20">
+      <div class="absolute inset-0 grid-pattern"></div>
+    </div>
+
+    <!-- Scanning line effect -->
+    <div class="absolute inset-0 pointer-events-none">
+      <div class="scan-line"></div>
+    </div>
+
+    <!-- Header -->
+    <div class="relative z-10 mb-6">
+      <div class="flex items-center justify-center border-b-2 border-cyan-500/30 pb-4 gap-8">
+        <h1 class="text-2xl font-bold text-cyan-300 flex-6 tracking-wider">
+          {{ dataInfo.groupName }}
+        </h1>
+
+        <div class="flex flex-4 items-center gap-8 text-sm">
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">部门名称</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deptName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">井号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.wellName }}</span>
+          </div>
+          <div
+            @click="toggleFullscreen"
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="inline-block skew-x-[12deg]">{{
+              isFullscreen ? '退出全屏' : '全屏'
+            }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="relative z-10 grid grid-cols-12 gap-4 mt-5 p-6">
+      <!-- Left panels - 合并为一个大面板 -->
+      <div class="col-span-3">
+        <div class="panel h-full flex flex-col">
+          <!-- PSA核心参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>PSA核心参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in psaData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 空气处理参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>空气处理撬参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in airTreatmentData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 1800中压机参数 -->
+          <div class="flex-1">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>中压机参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-5 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in mediumPressureData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="col-span-6 space-y-4">
+        <div class="panel relative overflow-hidden">
+          <div class="relative h-[450px] flex items-center justify-center">
+            <ModelViewer />
+          </div>
+        </div>
+
+        <!-- 1050空压机参数 -->
+        <div class="panel">
+          <div class="mb-2">
+            <dv-decoration7>
+              <h3 class="text-cyan-300 text-lg font-bold px-1"> 空压机参数 </h3>
+            </dv-decoration7>
+          </div>
+          <div class="grid grid-cols-5 gap-4">
+            <div v-for="(compressor, i) in compressorData" :key="i" class="space-y-2 text-center">
+              <div class="text-cyan-300 font-bold mb-2">{{ `空压机${i + 1}` }}</div>
+              <div class="text-sm" v-for="item in compressor" :key="item.modelName">
+                <div class="text-cyan-200">{{ item.modelName }}</div>
+                <div
+                  v-if="item.modelName.includes('运行状态') || item.modelName.includes('加载状态')"
+                  class="text-white font-mono flex items-center justify-center"
+                >
+                  <span
+                    :class="['w-3 h-3 rounded-full inline-block mr-2', getStatusClass(item.value)]"
+                  ></span>
+                  {{ getStatusText(item.value) }}
+                </div>
+                <div v-else class="text-white font-mono">
+                  {{ formatValue(item.value, getUnitFromModelName(item.modelName)) }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Right panel -->
+      <div class="col-span-3">
+        <div class="panel h-full">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">液驱压缩机参数</h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden">
+            <DataRow
+              v-for="item in liquidDrivenData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick } from 'vue'
+import DataRow from './data-row.vue'
+import ModelViewer from './ModelViewer.vue'
+import { IotDeviceApi } from '@/api/pms/device'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+defineOptions({
+  name: 'Kanban'
+})
+
+let dataInfo = ref<any>({})
+let psaData = ref<any[]>([])
+let airTreatmentData = ref<any[]>([])
+let mediumPressureData = ref<any[]>([])
+let liquidDrivenData = ref<any[]>([])
+let compressorData = ref<any[][]>([])
+const isFullscreen = ref(false)
+
+// 从modelName中提取单位
+const getUnitFromModelName = (modelName: string) => {
+  if (!modelName) return ''
+
+  // 根据不同的参数类型返回单位
+  if (modelName.includes('压力') || modelName.includes('压力P')) return 'MPa'
+  if (modelName.includes('温度') || modelName.includes('T')) return '℃'
+  if (modelName.includes('流量')) return 'Nm³'
+  if (modelName.includes('时间')) return '小时'
+  if (modelName.includes('氮气纯度')) return '%'
+  if (modelName.includes('环境温度')) return '℃'
+  if (modelName.includes('喷油')) return modelName.includes('温度') ? '℃' : 'MPa'
+
+  return '' // 默认无单位
+}
+
+// 获取状态类名
+const getStatusClass = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true'
+    ? 'status-active'
+    : 'status-inactive'
+}
+
+// 格式化数值显示
+const formatValue = (value: string, unit: string) => {
+  if (
+    value === undefined ||
+    value === null ||
+    value === '' ||
+    value === 'null' ||
+    (value === '0' && unit === '')
+  ) {
+    return '--'
+  }
+  return unit ? `${value} ${unit}` : value
+}
+
+// 根据modelName获取值
+const getValueByModelName = (data: any[], modelName: string) => {
+  const item = data.find((i) => i.modelName === modelName)
+  return item ? item.value : '0'
+}
+
+const toggleFullscreen = async () => {
+  if (!document.fullscreenElement) {
+    // 进入全屏
+    document.body.classList.add('app-fullscreen')
+
+    // 等待 DOM 更新后请求全屏
+    await nextTick()
+    document.documentElement.requestFullscreen()
+  } else {
+    // 退出全屏
+    document
+      .exitFullscreen()
+      .then(() => {
+        document.body.classList.remove('app-fullscreen')
+      })
+      .catch((err) => {
+        console.error('退出全屏失败:', err)
+        document.body.classList.remove('app-fullscreen')
+      })
+  }
+}
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement
+  if (!document.fullscreenElement) {
+    document.body.classList.remove('app-fullscreen')
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+// 添加 onUnmounted 来清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+const getStatusText = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true' ? '运行中' : '停止'
+}
+
+onMounted(async () => {
+  try {
+    const res = await IotDeviceApi.getDeviceSetParams(route.query.deviceSetId)
+
+    // 设置基础信息
+    dataInfo.value = {
+      groupName: res.groupName,
+      deptName: res.deptName,
+      wellName: res.wellName
+    }
+
+    // 设置各个设备的数据
+    psaData.value = res.PSA
+    airTreatmentData.value = res['空气处理撬']
+    mediumPressureData.value = res['中压机']
+    liquidDrivenData.value = res['液驱机']
+
+    // 设置空压机数据(空压机1到空压机5)
+    compressorData.value = []
+    for (let i = 1; i <= 5; i++) {
+      const compressorKey = `空压${i}`
+      if (res[compressorKey]) {
+        compressorData.value.push(res[compressorKey])
+      } else {
+        compressorData.value.push([])
+      }
+    }
+  } catch (error) {
+    console.error('获取设备参数失败:', error)
+  }
+})
+</script>
+
+<style scoped>
+/* @keyframes scan {
+  0% {
+    top: 0;
+  }
+  100% {
+    top: 100%;
+  }
+} */
+
+@keyframes gridMove {
+  0% {
+    transform: translate(0, 0);
+  }
+  100% {
+    transform: translate(50px, 50px);
+  }
+}
+
+.grid-pattern {
+  background-image: linear-gradient(rgba(6, 182, 212, 0.3) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(6, 182, 212, 0.3) 1px, transparent 1px);
+  background-size: 50px 50px;
+  animation: gridMove 20s linear infinite;
+}
+
+.scan-line {
+  position: absolute;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(to right, transparent, rgb(6, 182, 212), transparent);
+  opacity: 0.3;
+  animation: scan 4s linear infinite;
+}
+
+.scan-line-fast {
+  position: absolute;
+  width: 100%;
+  height: 4px;
+  background: linear-gradient(to right, transparent, rgba(6, 182, 212, 0.6), transparent);
+  animation: scan 3s linear infinite;
+}
+
+.panel {
+  background: linear-gradient(to bottom right, rgba(15, 23, 42, 0.8), rgba(30, 58, 138, 0.5));
+  backdrop-filter: blur(4px);
+  border: 2px solid rgba(6, 182, 212, 0.4);
+  padding: 1rem;
+  border-radius: 0.5rem;
+  box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
+  transition: all 0.3s;
+  min-height: 0; /* 允许flex项目收缩到其内容以下 */
+}
+
+.panel:hover {
+  box-shadow: 0 0 30px rgba(6, 182, 212, 0.5);
+}
+
+.panel-title {
+  color: rgb(103, 232, 249);
+  font-size: 1.125rem;
+  font-weight: bold;
+  border-bottom: 1px solid rgba(6, 182, 212, 0.3);
+  padding-bottom: 0.5rem;
+  flex-shrink: 0;
+}
+
+.status-indicator {
+  width: 2rem;
+  height: 2rem;
+  border-radius: 50%;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+.status-active {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 15px rgba(34, 197, 94, 0.8);
+}
+
+.status-inactive {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 15px rgba(239, 68, 68, 0.8);
+}
+
+.btn {
+  padding: 0.25rem 0.75rem;
+  border-radius: 0.25rem;
+  color: white;
+  transition: all 0.3s;
+  border: none;
+  cursor: pointer;
+  font-weight: 500;
+}
+
+.btn-green {
+  background: rgb(22, 163, 74);
+  box-shadow: 0 0 10px rgba(22, 163, 74, 0.5);
+}
+
+.btn-green:hover {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 20px rgba(22, 163, 74, 0.8);
+  transform: translateY(-1px);
+}
+
+.btn-red {
+  background: rgb(220, 38, 38);
+  box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
+}
+
+.btn-red:hover {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 20px rgba(220, 38, 38, 0.8);
+  transform: translateY(-1px);
+}
+
+/* 全屏模式下组件特定样式 */
+.fullscreen-layout {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  padding-top: 20px !important;
+  padding-left: 10px !important;
+  padding-right: 10px !important;
+  padding-bottom: 150px !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  overflow: auto !important;
+  overflow-x: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* Internet Explorer 10+ */
+}
+</style>

+ 540 - 0
src/views/pms/monitor/kanban2.vue

@@ -0,0 +1,540 @@
+<template>
+  <div
+    class="bg-gradient-to-br from-slate-950 via-blue-950 to-slate-900 text-cyan-100 p-6 relative"
+    :class="{ 'fullscreen-layout': isFullscreen }"
+  >
+    <!-- Animated background grid -->
+    <div class="absolute inset-0 opacity-20">
+      <div class="absolute inset-0 grid-pattern"></div>
+    </div>
+
+    <!-- Scanning line effect -->
+    <div class="absolute inset-0 pointer-events-none">
+      <div class="scan-line"></div>
+    </div>
+
+    <!-- Header -->
+    <div class="relative z-10 mb-6">
+      <div class="flex items-center justify-center border-b-2 border-cyan-500/30 pb-4 gap-8">
+        <h1 class="text-2xl font-bold text-cyan-300 flex-6 tracking-wider">
+          {{ dataInfo.groupName }}
+        </h1>
+
+        <div class="flex flex-4 items-center gap-8 text-sm">
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">部门名称</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deptName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">井号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.wellName }}</span>
+          </div>
+          <div
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="pr-4">增加机编号</span>
+            <span class="inline-block skew-x-[12deg]">{{ dataInfo.deviceCode }}</span>
+          </div>
+          <div
+            @click="toggleFullscreen"
+            style="border: 0.5px solid #085b77"
+            class="px-4 py-2 bg-cyan-500/10 border border-cyan-500/30 skew-x-[-12deg] hover:bg-cyan-500/20 transition-all cursor-pointer"
+          >
+            <span class="inline-block skew-x-[12deg]">{{
+              isFullscreen ? '退出全屏' : '全屏'
+            }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="relative z-10 grid grid-cols-12 gap-4 mt-5 p-6">
+      <!-- Left panels - 固定高度 -->
+      <div class="col-span-3">
+        <div class="panel h-full flex flex-col">
+          <!-- 增压机参数 -->
+          <div class="flex-1 border-b border-cyan-500/30 pb-2 mb-2">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>增压机参数</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in psaData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+
+          <!-- 累计运行时间 -->
+          <div class="flex-1">
+            <div class="flex justify-between items-center panel-title mb-2">
+              <h4>累计运行时间</h4>
+              <dv-decoration3 style="width: 120px; height: 30px" />
+            </div>
+            <div class="space-y-3 overflow-y-auto overflow-x-hidden flex-grow">
+              <DataRow
+                v-for="item in airTreatmentData"
+                :key="item.modelName"
+                :label="item.modelName"
+                :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="col-span-6 space-y-4">
+        <!-- 3D模型面板 -->
+        <div class="panel h-[60%] relative overflow-hidden">
+          <div class="relative h-full flex items-center justify-center">
+            <ModelViewer />
+          </div>
+        </div>
+
+        <!-- 状态参数面板 -->
+        <div class="panel h-[40%]">
+          <div class="mb-2">
+            <dv-decoration7>
+              <h3 class="text-cyan-300 text-lg font-bold px-1"> 状态参数 </h3>
+            </dv-decoration7>
+          </div>
+          <div class="grid grid-cols-5 gap-4 h-[calc(100%-2rem)]">
+            <div
+              v-for="(group, i) in compressorData"
+              :key="i"
+              class="space-y-2 text-center h-full overflow-y-auto"
+            >
+              <div class="text-sm" v-for="item in group" :key="item.modelName">
+                <div class="text-cyan-200">{{ item.modelName }}</div>
+                <div class="text-white font-mono flex items-center justify-center">
+                  <span
+                    :class="['w-3 h-3 rounded-full inline-block mr-2', getStatusClass(item.value)]"
+                  ></span>
+                  {{ getStatusDisplayText(item.modelName, item.value) }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Right panel - 氮气参数和井口参数平分高度 -->
+      <div class="col-span-3">
+        <div class="panel h-[50%] mb-2">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">氮气参数</h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden h-[calc(100%-3rem)]">
+            <DataRow
+              v-for="item in liquidDrivenData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+        <div class="panel h-[50%]">
+          <div class="flex items-center justify-between mb-4 border-b border-cyan-500/30 pb-2">
+            <h3 class="text-cyan-300 text-lg font-bold">井口参数 </h3>
+            <dv-decoration8 :reverse="true" style="width: 110px; height: 20px" />
+          </div>
+          <div class="space-y-2 text-sm overflow-y-auto overflow-x-hidden h-[calc(100%-3rem)]">
+            <DataRow
+              v-for="item in wellheadData"
+              :key="item.modelName"
+              :label="item.modelName"
+              :value="formatValue(item.value, getUnitFromModelName(item.modelName))"
+              :compact="true"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick } from 'vue'
+import DataRow from './data-row.vue'
+import ModelViewer from './ModelViewer2.vue'
+import { IotDeviceApi } from '@/api/pms/device'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+defineOptions({
+  name: 'Kanban'
+})
+
+let dataInfo = ref<any>({})
+let psaData = ref<any[]>([])
+let airTreatmentData = ref<any[]>([])
+let mediumPressureData = ref<any[]>([])
+let liquidDrivenData = ref<any[]>([])
+let wellheadData = ref<any[]>([]) // 井口参数
+let compressorData = ref<any[][]>([])
+const isFullscreen = ref(false)
+
+// 从modelName中提取单位
+const getUnitFromModelName = (modelName: string) => {
+  if (!modelName) return ''
+
+  // 根据不同的参数类型返回单位
+  if (modelName.includes('压力') || modelName.includes('压力P')) return 'MPa'
+  if (modelName.includes('温度') || modelName.includes('T')) return '℃'
+  if (modelName.includes('流量')) return 'Nm³'
+  if (modelName.includes('时间')) return '小时'
+  if (modelName.includes('氮气纯度')) return '%'
+  if (modelName.includes('环境温度')) return '℃'
+  if (modelName.includes('喷油')) return modelName.includes('温度') ? '℃' : 'MPa'
+  if (modelName.includes('轴瓦温度')) return '℃'
+  if (modelName.includes('润滑油温度')) return '℃'
+  if (modelName.includes('排气温度')) return '℃'
+  if (modelName.includes('进气压力')) return 'MPa'
+  if (modelName.includes('排气压力')) return 'MPa'
+  if (modelName.includes('累计流量')) return 'm³'
+  if (modelName.includes('瞬时流量')) return 'm³/h'
+
+  return '' // 默认无单位
+}
+
+// 判断是否为状态类参数
+const isStatusParameter = (paramName: string) => {
+  const statusKeywords = ['无油1#', '无油2#', '曲轴油位', '油泵状态', '风机状态', '急停状态']
+  return statusKeywords.some((keyword) => paramName.includes(keyword))
+}
+
+// 获取状态类参数的显示文本
+const getStatusDisplayText = (paramName: string, value: string | number) => {
+  if (value === '' || value === null || value === undefined) {
+    return '--'
+  }
+
+  // 根据不同参数类型返回不同的状态文本
+  if (paramName.includes('急停状态')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '急停'
+  } else if (paramName.includes('油泵状态') || paramName.includes('风机状态')) {
+    return value === '1' || value === 1 || value === 'true' ? '运行' : '停止'
+  } else if (paramName.includes('无油')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '缺油'
+  } else if (paramName.includes('曲轴油位')) {
+    return value === '1' || value === 1 || value === 'true' ? '正常' : '低油位'
+  } else {
+    return value === '1' || value === 1 || value === 'true' ? '开启' : '关闭'
+  }
+}
+
+// 获取状态类名
+const getStatusClass = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true'
+    ? 'status-active'
+    : 'status-inactive'
+}
+
+// 格式化数值显示
+const formatValue = (value: string, unit: string) => {
+  if (
+    value === undefined ||
+    value === null ||
+    value === '' ||
+    value === 'null' ||
+    (value === '0' && unit === '')
+  ) {
+    return '--'
+  }
+  return unit ? `${value} ${unit}` : value
+}
+
+const toggleFullscreen = async () => {
+  if (!document.fullscreenElement) {
+    // 进入全屏
+    document.body.classList.add('app-fullscreen')
+
+    // 等待 DOM 更新后请求全屏
+    await nextTick()
+    document.documentElement.requestFullscreen()
+  } else {
+    // 退出全屏
+    document
+      .exitFullscreen()
+      .then(() => {
+        document.body.classList.remove('app-fullscreen')
+      })
+      .catch((err) => {
+        console.error('退出全屏失败:', err)
+        document.body.classList.remove('app-fullscreen')
+      })
+  }
+}
+
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement
+  if (!document.fullscreenElement) {
+    document.body.classList.remove('app-fullscreen')
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+// 添加 onUnmounted 来清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+const getStatusText = (statusValue: string | number) => {
+  return statusValue === '1' || statusValue === 1 || statusValue === 'true' ? '运行中' : '停止'
+}
+
+onMounted(async () => {
+  try {
+    const res = await IotDeviceApi.getDeviceSetParams(route.query.deviceSetId)
+
+    // 设置基础信息
+    dataInfo.value = {
+      groupName: res.groupName,
+      deptName: res.deptName,
+      wellName: res.wellName,
+      deviceCode: res.deviceCode
+    }
+
+    // 分离增压机参数、状态参数、氮气参数和井口参数
+    const allCompressorParams = res['增压机'] || []
+
+    // 定义状态参数关键词
+    const statusKeywords = [
+      '无油1#',
+      '无油2#',
+      '曲轴油位',
+      '油泵状态',
+      '风机状态',
+      '急停状态',
+      '振动'
+    ]
+
+    // 定义氮气参数关键词
+    const gasKeywords = ['氮气纯度', '瞬时流量', '累计流量']
+
+    // 定义井口参数关键词 - 使用精确匹配
+    const wellheadKeywords = ['油压', '套压', '表套', '二套']
+
+    // 过滤出状态参数
+    const statusParams = allCompressorParams.filter((item) =>
+      statusKeywords.includes(item.modelName)
+    )
+
+    // 过滤出氮气参数
+    const gasParams = allCompressorParams.filter((item) => gasKeywords.includes(item.modelName))
+
+    // 过滤出井口参数 - 使用精确匹配
+    const wellheadParams = allCompressorParams.filter((item) =>
+      wellheadKeywords.includes(item.modelName)
+    )
+
+    // 过滤出非状态参数、非氮气参数和非井口参数(真正的增压机参数)
+    const nonStatusGasWellheadParams = allCompressorParams.filter(
+      (item) =>
+        !statusKeywords.includes(item.modelName) &&
+        !gasKeywords.includes(item.modelName) &&
+        !wellheadKeywords.includes(item.modelName)
+    )
+
+    // 设置各部分数据
+    psaData.value = nonStatusGasWellheadParams // 增压机参数(排除状态参数、氮气参数和井口参数)
+    mediumPressureData.value = statusParams // 状态参数
+
+    // 显示累计运行时间
+    airTreatmentData.value = [
+      {
+        modelName: '累计运行时间',
+        value: res.totalRuntime || 0
+      }
+    ]
+
+    // 氮气参数
+    liquidDrivenData.value = gasParams
+
+    // 井口参数
+    wellheadData.value = wellheadParams
+
+    // 将状态参数分组显示
+    const chunkArray = (arr: any[], size: number) => {
+      const result = []
+      for (let i = 0; i < arr.length; i += size) {
+        result.push(arr.slice(i, i + size))
+      }
+      return result
+    }
+
+    // 将状态参数分成多组
+    compressorData.value = chunkArray(statusParams, 5)
+  } catch (error) {
+    console.error('获取设备参数失败:', error)
+  }
+})
+</script>
+
+<style scoped>
+/* @keyframes scan {
+  0% {
+    top: 0;
+  }
+  100% {
+    top: 100%;
+  }
+} */
+
+@keyframes gridMove {
+  0% {
+    transform: translate(0, 0);
+  }
+  100% {
+    transform: translate(50px, 50px);
+  }
+}
+
+.grid-pattern {
+  background-image: linear-gradient(rgba(6, 182, 212, 0.3) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(6, 182, 212, 0.3) 1px, transparent 1px);
+  background-size: 50px 50px;
+  animation: gridMove 20s linear infinite;
+}
+
+.scan-line {
+  position: absolute;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(to right, transparent, rgb(6, 182, 212), transparent);
+  opacity: 0.3;
+  animation: scan 4s linear infinite;
+}
+
+.scan-line-fast {
+  position: absolute;
+  width: 100%;
+  height: 4px;
+  background: linear-gradient(to right, transparent, rgba(6, 182, 212, 0.6), transparent);
+  animation: scan 3s linear infinite;
+}
+
+.panel {
+  background: linear-gradient(to bottom right, rgba(15, 23, 42, 0.8), rgba(30, 58, 138, 0.5));
+  backdrop-filter: blur(4px);
+  border: 2px solid rgba(6, 182, 212, 0.4);
+  padding: 1rem;
+  border-radius: 0.5rem;
+  box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
+  transition: all 0.3s;
+  min-height: 0; /* 允许flex项目收缩到其内容以下 */
+}
+
+.panel:hover {
+  box-shadow: 0 0 30px rgba(6, 182, 212, 0.5);
+}
+
+.panel-title {
+  color: rgb(103, 232, 249);
+  font-size: 1.125rem;
+  font-weight: bold;
+  border-bottom: 1px solid rgba(6, 182, 212, 0.3);
+  padding-bottom: 0.5rem;
+  flex-shrink: 0;
+}
+
+.status-indicator {
+  width: 2rem;
+  height: 2rem;
+  border-radius: 50%;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+.status-active {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 15px rgba(34, 197, 94, 0.8);
+}
+
+.status-inactive {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 15px rgba(239, 68, 68, 0.8);
+}
+
+.btn {
+  padding: 0.25rem 0.75rem;
+  border-radius: 0.25rem;
+  color: white;
+  transition: all 0.3s;
+  border: none;
+  cursor: pointer;
+  font-weight: 500;
+}
+
+.btn-green {
+  background: rgb(22, 163, 74);
+  box-shadow: 0 0 10px rgba(22, 163, 74, 0.5);
+}
+
+.btn-green:hover {
+  background: rgb(34, 197, 94);
+  box-shadow: 0 0 20px rgba(22, 163, 74, 0.8);
+  transform: translateY(-1px);
+}
+
+.btn-red {
+  background: rgb(220, 38, 38);
+  box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
+}
+
+.btn-red:hover {
+  background: rgb(239, 68, 68);
+  box-shadow: 0 0 20px rgba(220, 38, 38, 0.8);
+  transform: translateY(-1px);
+}
+
+/* 全屏模式下组件特定样式 */
+.fullscreen-layout {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  padding-top: 20px !important;
+  padding-left: 10px !important;
+  padding-right: 10px !important;
+  padding-bottom: 150px !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  overflow: auto !important;
+  overflow-x: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* Internet Explorer 10+ */
+}
+
+/* 在现有样式中添加 */
+.panel-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.panel-content {
+  flex: 1;
+  overflow-y: auto;
+}
+</style>