Kaynağa Gözat

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue
yuanchao 2 ay önce
ebeveyn
işleme
05681d5738
52 değiştirilmiş dosya ile 1672 ekleme ve 162 silme
  1. 2 1
      index.html
  2. 3 1
      package.json
  3. 1 0
      public/images/ding300.svg
  4. 1 0
      public/images/dingblue.svg
  5. 1 0
      public/images/dinggreen.svg
  6. 1 0
      public/images/dingou.svg
  7. 1 0
      public/images/dingout.svg
  8. 6 0
      src/api/pms/device/index.ts
  9. 3 0
      src/api/pms/iotmaintenancebom/index.ts
  10. BIN
      src/assets/imgs/yf.png
  11. 5 1
      src/components/language/LangInput.vue
  12. 10 2
      src/locales/en.ts
  13. 9 1
      src/locales/zh-CN.ts
  14. 2 4
      src/views/Home/Index.vue
  15. 5 3
      src/views/Login/Login.vue
  16. 6 2
      src/views/pms/bom/BomForm.vue
  17. 2 0
      src/views/pms/bom/MaterialList.vue
  18. 1 0
      src/views/pms/device/IotDeviceForm.vue
  19. 171 15
      src/views/pms/device/allotlog/ConfigDeviceAllot.vue
  20. 8 11
      src/views/pms/device/allotlog/DeviceAllot.vue
  21. 23 3
      src/views/pms/device/allotlog/DeviceAllotLogDrawer.vue
  22. 5 4
      src/views/pms/device/monitor/TdDeviceInfo.vue
  23. 1 1
      src/views/pms/device/monitor/TdDeviceLabel.vue
  24. 50 5
      src/views/pms/device/personlog/ConfigDevicePerson.vue
  25. 7 14
      src/views/pms/device/personlog/DevicePerson.vue
  26. 25 5
      src/views/pms/device/personlog/DevicePersonLogDrawer.vue
  27. 58 4
      src/views/pms/device/statuslog/ConfigDeviceStatus.vue
  28. 7 17
      src/views/pms/device/statuslog/DeviceStatus.vue
  29. 23 3
      src/views/pms/device/statuslog/DeviceStatusLogDrawer.vue
  30. 6 3
      src/views/pms/devicetemplate/TemplateForm.vue
  31. 1 1
      src/views/pms/failure/IotFailureReportForm.vue
  32. 2 0
      src/views/pms/inspect/plan/InspectRouteList.vue
  33. 2 2
      src/views/pms/iotlockstock/index.vue
  34. 285 0
      src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue
  35. 46 23
      src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue
  36. 43 3
      src/views/pms/iotmainworkorder/IotMainWorkOrder.vue
  37. 21 2
      src/views/pms/iotmainworkorder/IotMainWorkOrderDetail.vue
  38. 11 2
      src/views/pms/iotmainworkorder/WorkOrderMaterial.vue
  39. 2 2
      src/views/pms/iotsapstock/index.vue
  40. 4 2
      src/views/pms/maintain/IotMaintain.vue
  41. 1 1
      src/views/pms/maintain/IotMaintainDetail.vue
  42. 1 1
      src/views/pms/maintain/index.vue
  43. 19 2
      src/views/pms/maintenance/MainPlanDeviceList.vue
  44. 341 0
      src/views/pms/map/DeviceMonitorDrawer.vue
  45. 376 0
      src/views/pms/map/Map.vue
  46. 20 6
      src/views/pms/material/MaterialForm.vue
  47. 2 2
      src/views/pms/material/MaterialGroupTree.vue
  48. 39 6
      src/views/pms/materialgroup/MaterialGroupForm.vue
  49. 8 2
      src/views/pms/materialgroup/index.vue
  50. 3 3
      src/views/system/dept/DeptForm.vue
  51. 1 1
      src/views/system/user/UserAssignRoleForm.vue
  52. 1 1
      src/views/system/user/UserForm.vue

+ 2 - 1
index.html

@@ -3,7 +3,8 @@
   <head>
       <script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
       <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"></script>
-    <meta charset="UTF-8" />
+      <script src="https://api.map.baidu.com/api?v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0"></script>
+      <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />

+ 3 - 1
package.json

@@ -34,6 +34,7 @@
     "@fullcalendar/vue3": "^6.1.17",
     "@iconify/iconify": "^3.1.1",
     "@microsoft/fetch-event-source": "^2.0.1",
+    "@types/echarts": "^5.0.0",
     "@videojs-player/vue": "^1.0.0",
     "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
@@ -52,7 +53,7 @@
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
     "driver.js": "^1.3.1",
-    "echarts": "^5.5.0",
+    "echarts": "^5.6.0",
     "echarts-wordcloud": "^2.1.0",
     "element-plus": "2.9.1",
     "fast-xml-parser": "^4.3.2",
@@ -80,6 +81,7 @@
     "video.js": "^7.21.5",
     "vue": "3.5.12",
     "vue-dompurify-html": "^4.1.4",
+    "vue-echarts": "^7.0.3",
     "vue-i18n": "9.10.2",
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",

+ 1 - 0
public/images/ding300.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749536872427" class="icon" viewBox="0 0 1129 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12543" xmlns:xlink="http://www.w3.org/1999/xlink" width="33.076171875" height="30"><path d="M828.027586 330.151724c-35.310345-82.97931-105.931034-146.537931-187.144827-171.255172-22.951724-7.062069-47.668966-10.593103-72.386207-10.593104-86.510345 0-171.255172 44.137931-224.22069 118.289655-60.027586 81.213793-82.97931 187.144828-33.544828 278.951725 40.606897 74.151724 204.8 286.013793 248.937932 346.041379 75.917241-105.931034 171.255172-201.268966 238.344827-314.262069 30.013793-51.2 42.372414-63.558621 49.434483-114.758621 5.296552-42.372414 0-86.510345-19.42069-132.413793zM564.965517 609.103448c-95.337931 0-173.02069-77.682759-173.020689-173.020689s77.682759-173.02069 173.020689-173.02069 173.02069 77.682759 173.02069 173.02069-77.682759 173.02069-173.02069 173.020689z" fill="#1296db" p-id="12544"></path><path d="M564.965517 436.082759m-82.97931 0a82.97931 82.97931 0 1 0 165.958621 0 82.97931 82.97931 0 1 0-165.958621 0Z" fill="#1296db" p-id="12545"></path></svg>

+ 1 - 0
public/images/dingblue.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749695532183" class="icon" viewBox="0 0 1129 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6359" width="52.921875" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M828.027586 330.151724c-35.310345-82.97931-105.931034-146.537931-187.144827-171.255172-22.951724-7.062069-47.668966-10.593103-72.386207-10.593104-86.510345 0-171.255172 44.137931-224.22069 118.289655-60.027586 81.213793-82.97931 187.144828-33.544828 278.951725 40.606897 74.151724 204.8 286.013793 248.937932 346.041379 75.917241-105.931034 171.255172-201.268966 238.344827-314.262069 30.013793-51.2 42.372414-63.558621 49.434483-114.758621 5.296552-42.372414 0-86.510345-19.42069-132.413793zM564.965517 609.103448c-95.337931 0-173.02069-77.682759-173.020689-173.020689s77.682759-173.02069 173.020689-173.02069 173.02069 77.682759 173.02069 173.02069-77.682759 173.02069-173.02069 173.020689z" fill="#1afa29" p-id="6360"></path><path d="M564.965517 436.082759m-82.97931 0a82.97931 82.97931 0 1 0 165.958621 0 82.97931 82.97931 0 1 0-165.958621 0Z" fill="#1afa29" p-id="6361"></path></svg>

+ 1 - 0
public/images/dinggreen.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749695532183" class="icon" viewBox="0 0 1129 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6359" width="52.921875" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M828.027586 330.151724c-35.310345-82.97931-105.931034-146.537931-187.144827-171.255172-22.951724-7.062069-47.668966-10.593103-72.386207-10.593104-86.510345 0-171.255172 44.137931-224.22069 118.289655-60.027586 81.213793-82.97931 187.144828-33.544828 278.951725 40.606897 74.151724 204.8 286.013793 248.937932 346.041379 75.917241-105.931034 171.255172-201.268966 238.344827-314.262069 30.013793-51.2 42.372414-63.558621 49.434483-114.758621 5.296552-42.372414 0-86.510345-19.42069-132.413793zM564.965517 609.103448c-95.337931 0-173.02069-77.682759-173.020689-173.020689s77.682759-173.02069 173.020689-173.02069 173.02069 77.682759 173.02069 173.02069-77.682759 173.02069-173.02069 173.020689z" fill="#2aa515" p-id="6360"></path><path d="M564.965517 436.082759m-82.97931 0a82.97931 82.97931 0 1 0 165.958621 0 82.97931 82.97931 0 1 0-165.958621 0Z" fill="#2aa515" p-id="6361"></path></svg>

+ 1 - 0
public/images/dingou.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749695532183" class="icon" viewBox="0 0 1129 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6359" width="35.28125" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M828.027586 330.151724c-35.310345-82.97931-105.931034-146.537931-187.144827-171.255172-22.951724-7.062069-47.668966-10.593103-72.386207-10.593104-86.510345 0-171.255172 44.137931-224.22069 118.289655-60.027586 81.213793-82.97931 187.144828-33.544828 278.951725 40.606897 74.151724 204.8 286.013793 248.937932 346.041379 75.917241-105.931034 171.255172-201.268966 238.344827-314.262069 30.013793-51.2 42.372414-63.558621 49.434483-114.758621 5.296552-42.372414 0-86.510345-19.42069-132.413793zM564.965517 609.103448c-95.337931 0-173.02069-77.682759-173.020689-173.020689s77.682759-173.02069 173.020689-173.02069 173.02069 77.682759 173.02069 173.02069-77.682759 173.02069-173.02069 173.020689z" fill="#d4237a" p-id="6360"></path><path d="M564.965517 436.082759m-82.97931 0a82.97931 82.97931 0 1 0 165.958621 0 82.97931 82.97931 0 1 0-165.958621 0Z" fill="#d4237a" p-id="6361"></path></svg>

+ 1 - 0
public/images/dingout.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749706700481" class="icon" viewBox="0 0 1129 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7358" xmlns:xlink="http://www.w3.org/1999/xlink" width="52.921875" height="48"><path d="M828.027586 330.151724c-35.310345-82.97931-105.931034-146.537931-187.144827-171.255172-22.951724-7.062069-47.668966-10.593103-72.386207-10.593104-86.510345 0-171.255172 44.137931-224.22069 118.289655-60.027586 81.213793-82.97931 187.144828-33.544828 278.951725 40.606897 74.151724 204.8 286.013793 248.937932 346.041379 75.917241-105.931034 171.255172-201.268966 238.344827-314.262069 30.013793-51.2 42.372414-63.558621 49.434483-114.758621 5.296552-42.372414 0-86.510345-19.42069-132.413793zM564.965517 609.103448c-95.337931 0-173.02069-77.682759-173.020689-173.020689s77.682759-173.02069 173.020689-173.02069 173.02069 77.682759 173.02069 173.02069-77.682759 173.02069-173.02069 173.020689z" fill="#d4237a" p-id="7359"></path><path d="M564.965517 436.082759m-82.97931 0a82.97931 82.97931 0 1 0 165.958621 0 82.97931 82.97931 0 1 0-165.958621 0Z" fill="#d4237a" p-id="7360"></path></svg>

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

@@ -36,10 +36,16 @@ export interface IotDeviceVO {
   name: string // bom节点名称
   code: string // bom节点编码
   devicePersons: string // 设备责任人 逗号分隔
+  location: string
+  lat: number
+  lng: number
 }
 
 // 设备台账 API
 export const IotDeviceApi = {
+  getMapDevice: async () => {
+    return await request.get({ url: `/rq/iot-device/map`})
+  },
   // 查询设备台账分页
   getIotDevicePage: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/page`, params })

+ 3 - 0
src/api/pms/iotmaintenancebom/index.ts

@@ -13,6 +13,9 @@ export interface IotMaintenanceBomVO {
   runningTimeRule: number   // 保养规则-运行时间(0启用 1停用)
   lastRunningTime: number // 上次保养运行时长(小时)
   nextRunningTime: number // 下次保养运行时长(小时)
+  timePeriod: number // 时间周期(小时)
+  kilometerCycle: number // 公里数周期(千米)
+  naturalDatePeriod: number // 自然日周期(天)
   timePeriodLead: number  // 运行时长周期提前量 H
   lastRunningKilometers: number // 上次保养运行公里数(千米)
   nextRunningKilometers: number // 下次保养运行公里数(千米)

BIN
src/assets/imgs/yf.png


+ 5 - 1
src/components/language/LangInput.vue

@@ -63,7 +63,11 @@ const displayValue = computed({
     return props.modelValue.split('~~')[0] || ''
   },
   set(val) {
-    emit('update:modelValue', val + '~~' + englishValue.value)
+    if (englishValue.value !== null&&englishValue.value !== undefined&&englishValue.value !== '') {
+      emit('update:modelValue', val + '~~' + englishValue.value)
+    }else {
+      emit('update:modelValue', val)
+    }
   }
 })
 

+ 10 - 2
src/locales/en.ts

@@ -602,6 +602,10 @@ export default {
     check:'Check',
     details:'Details'
   },
+  deviceAllot:{
+    setUp:'SetUp',
+    adjustmentRecords:'AllotRecords',
+  },
   devicePerson:{
     deviceName:'DeviceName',
     nameHolder:'Please enter DeviceName',
@@ -624,7 +628,8 @@ export default {
     dept:'Department',
     rp:'ResponsiblePerson',
     operation:'Operation',
-    adjustmentRecords:'AdjustmentRecords'
+    adjustmentRecords:'AdjustmentRecords',
+    filterDevicePlaceholder: "Filter DeviceCode or DeviceName"
   },
   configPerson:{
     deviceList:'DeviceList',
@@ -633,7 +638,8 @@ export default {
     rpHolder:'Please select the Department',
     reasonForAdjustment:'ReasonForAdjustment',
     rfaHolder:'Please enter reason',
-    adjustmentRecords:'AdjustmentRecords'
+    adjustmentRecords:'AdjustmentRecords',
+    selectPersons: 'Please select Person'
   },
   deviceStatus:{
     deviceName:'DeviceName',
@@ -683,6 +689,7 @@ export default {
     status:'Status',
     shutDown:'ShutDown',
     failureTime:'FailureTime',
+    dealTime:'DealTime',
     start:'StartTime',
     end:'EndTime',
     solve:'Solve',
@@ -709,6 +716,7 @@ export default {
     faultySystem:'FaultySystem',
     faultySystemHolder:'Please enter the fault system',
     failureTime:'FailureTime',
+    dealTime:'DealTime',
     failureTimeHolder:'Please select failure time',
     shutDown:'ShutDown',
     picture: 'Picture',

+ 9 - 1
src/locales/zh-CN.ts

@@ -596,6 +596,10 @@ export default {
     check:'查看',
     details:'详情'
   },
+  deviceAllot:{
+    setUp:'调拨设备',
+    adjustmentRecords:'调拨记录',
+  },
   devicePerson:{
     deviceName:'设备名称',
     nameHolder:'请输入设备名称',
@@ -619,6 +623,7 @@ export default {
     rp:'责任人',
     operation:'操作',
     adjustmentRecords:'调整记录',
+    filterDevicePlaceholder: "输入设备编码或名称过滤"
   },
   configPerson:{
     deviceList:'设备列表',
@@ -627,7 +632,8 @@ export default {
     rpHolder:'请选择所属部门',
     reasonForAdjustment:'调整原因',
     rfaHolder:'请输入调整原因',
-    adjustmentRecords:'调整记录'
+    adjustmentRecords:'调整记录',
+    selectPersons: '请选择责任人'
   },
   deviceStatus:{
     deviceName:'设备名称',
@@ -678,6 +684,7 @@ export default {
     status:'状态',
     shutDown:'是否停机',
     failureTime:'故障时间',
+    dealTime:'解决时间',
     start:'开始时间',
     end:'结束时间',
     solve:'是否解决',
@@ -703,6 +710,7 @@ export default {
     faultySystem:'故障系统',
     faultySystemHolder:'请输入故障系统',
     failureTime:'故障时间',
+    dealTime:'解决时间',
     failureTimeHolder:'请选择故障时间',
     shutDown:'是否停机',
     picture: '图片',

+ 2 - 4
src/views/Home/Index.vue

@@ -279,6 +279,7 @@ const getStats = async () => {
   })
   IotStatApi.getDeviceStatusCount().then((res) => {
     typeData.value = res
+    debugger
     initCharts()
   })
   IotStatApi.getSafeCount().then((res) => {
@@ -333,10 +334,7 @@ const initCharts = () => {
         labelLine: {
           show: false
         },
-        data: Object.entries(typeData.value).map(([name, value]) => ({
-          name,
-          value
-        }))
+        data: typeData.value
       }
     ]
   })

+ 5 - 3
src/views/Login/Login.vue

@@ -19,8 +19,9 @@
             enter-active-class="animate__animated animate__bounceInLeft"
             tag="div"
           >
-            <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
-            <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
+<!--            <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />-->
+<!--            <img key="1" alt="" class="w-350px" src="@/assets/imgs/yf.jpg" />-->
+<!--            <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>-->
             <div key="3" class="mt-5 text-14px font-normal text-white">
               {{ t('login.message') }}
             </div>
@@ -99,10 +100,11 @@ $prefix-cls: #{$namespace}-login;
       z-index: -1;
       width: 100%;
       height: 100%;
-      background-image: url('@/assets/svgs/login-bg.svg');
+      background-image: url('@/assets/imgs/yf.png');
       background-position: center;
       background-repeat: no-repeat;
       content: '';
+      background-size: cover;                 /* 关键:图片覆盖整个容器 */
     }
   }
 }

+ 6 - 2
src/views/pms/bom/BomForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" :close-on-click-modal="false">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -16,6 +16,8 @@
           :default-expanded-keys="firstLevelKeys"
           placeholder="请选择设备分类"
           value-key="id"
+          filterable
+          clearable
           @node-click="handleDeviceCategoryTreeNodeClick"
         />
       </el-form-item>
@@ -26,6 +28,8 @@
           :props="defaultProps"
           check-strictly
           default-expand-all
+          filterable
+          clearable
           placeholder="请选择上级BOM"
           value-key="bomId"
         />
@@ -109,7 +113,7 @@ const formRules = reactive<FormRules>({
 })
 const formRef = ref() // 表单 Ref
 const bomTree = ref() // BOM 树形结构
-const deviceCategoryTree = ref()  // 设备分类树
+const deviceCategoryTree = ref<Tree[]>([])  // 设备分类树
 const firstLevelKeys = ref([])
 
 const queryParams = reactive({

+ 2 - 0
src/views/pms/bom/MaterialList.vue

@@ -28,6 +28,7 @@
         </el-form-item>
         <el-form-item :label="t('workOrderMaterial.materialGroup')" prop="materialGroupId" style="margin-left: 5px">
           <el-tree-select
+            filterable
             v-model="queryParams.materialGroupId"
             :data="materialGroupList"
             :props="defaultProps"
@@ -35,6 +36,7 @@
             node-key="id"
             :placeholder="t('workOrderMaterial.groupHolder')"
             class="!w-220px"
+            clearable
           />
         </el-form-item>
         <el-form-item>

+ 1 - 0
src/views/pms/device/IotDeviceForm.vue

@@ -34,6 +34,7 @@
                 :props="defaultProps"
                 check-strictly
                 node-key="id"
+                filterable
                 placeholder="请选择所在部门"
               />
 <!--              <el-tree-select-->

+ 171 - 15
src/views/pms/device/allotlog/ConfigDeviceAllot.vue

@@ -18,15 +18,26 @@
               @node-click="handleDeptDeviceTreeNodeClick"
             />
           </div>
+
+          <!-- 设备搜索框 -->
+          <div class="filter-input">
+            <el-input
+              v-model="deviceFilterText"
+              :placeholder="t('devicePerson.filterDevicePlaceholder')"
+              clearable
+              prefix-icon="Search"
+            />
+          </div>
+
           <el-scrollbar height="500px">
             <el-checkbox-group v-model="selectedDevices">
               <div
-                v-for="device in simpleDevices"
+                v-for="device in filteredDevices"
                 :key="device.id"
                 class="checkbox-item"
               >
                 <el-checkbox :label="device.id">
-                  {{ device.deviceCode }} ({{ device.deviceName }}) - {{ device.deptName }}
+                  {{ device.deviceCode }} ({{ device.deviceName }}) - {{ device.deptName }} —— {{ device.devicePersons }}
                 </el-checkbox>
               </div>
             </el-checkbox-group>
@@ -47,24 +58,56 @@
 
     <!-- 暂存关联列表 -->
     <div class="submit-area">
-      <div class="card">
-        <el-input
-          v-model="formData.reason"
-          :placeholder="t('configDevice.rfaHolder')"
-          class="reason-input"
-          type="textarea"
-          :rows="3"
-          @input="updateTempRelations"
-        />
+      <div class="card selection-area">
+        <div class="control-row">
+          <!-- 左侧人员选择 -->
+          <div class="control-group">
+            <label class="control-title">{{ t('devicePerson.rp') }}</label>
+            <div class="person-selector">
+              <el-select
+                v-model="selectedPersons"
+                multiple
+                filterable
+                :placeholder="t('configPerson.selectPersons')"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="person in simpleUsers"
+                  :key="person.id"
+                  :label="person.nickname"
+                  :value="person.id"
+                />
+              </el-select>
+            </div>
+          </div>
+
+          <div class="control-group">
+            <label class="control-title">{{ t('configDevice.reasonForAdjustment') }}</label>
+            <div class="reason-input-wrapper">
+              <el-input
+                v-model="formData.reason"
+                :placeholder="t('configDevice.rfaHolder')"
+                class="reason-input"
+                type="textarea"
+                :rows="4"
+                resize="none"
+                @input="updateTempRelations"
+              />
+            </div>
+          </div>
+        </div>
       </div>
+
       <el-button
         type="primary"
         size="large"
+        style="min-width: 180px;"
         @click="submitRelations"
-        :disabled="tempRelations.length === 0"
+        :disabled="tempRelations.length === 0 || !formData.reason.trim() || selectedPersons.length === 0"
       >
         {{ t('iotMaintain.save') }}
       </el-button>
+
       <div class="temp-list card" v-if="false">
         <h3>{{ t('configPerson.adjustmentRecords') }}</h3>
         <el-table :data="tempRelations" style="width: 100%">
@@ -94,9 +137,13 @@ import { ref, computed } from 'vue'
 import { ElMessage } from 'element-plus'
 import {defaultProps, handleTree} from "@/utils/tree";
 import * as DeptApi from "@/api/system/dept";
+import * as UserApi from "@/api/system/user";
 import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
 import DeptTree2 from "@/views/pms/device/DeptTree2.vue";
 import { useRouter } from 'vue-router'
+import { useTagsViewStore } from "@/store/modules/tagsView";
+import {UserVO} from "@/api/system/user";
+
 const router = useRouter()
 const { t } = useI18n() // 国际化
 defineOptions({ name: 'ConfigDeviceAllot' })
@@ -104,6 +151,7 @@ const selectedDeptId = ref<number | string>('')
 const simpleDevices = ref<IotDeviceVO[]>([])
 const deptList = ref<Tree[]>([]) // 树形结构
 const selectedDevices = ref<number[]>([]) // 改为数组存储多选
+const { delView } = useTagsViewStore() // 视图操作
 
 const formData = ref({
   id: undefined,
@@ -126,6 +174,9 @@ const deptTreeRef = ref<InstanceType<typeof DeptTree2>>()
 
 const emit = defineEmits(['success', 'node-click']) // 定义 success 树点击 事件,用于操作成功后的回调
 
+const simpleUsers = ref<UserVO[]>([])     // 人员下拉列表选项
+const selectedPersons = ref<number[]>([]) // 存储选中的人员ID
+
 // 响应式数据
 const tempRelationsMap = ref(new Map<number, {
   deviceId: number
@@ -181,6 +232,16 @@ watch([selectedDevices, selectedDeptId], () => {
   updateTempRelations();
 }, {deep: true, immediate: true, debounce: 100})
 
+// 监听部门变化
+watch(selectedDeptId, (newVal) => {
+  if (newVal) {
+    loadDeptPersons(newVal as number)
+  } else {
+    simpleUsers.value = []
+  }
+  selectedPersons.value = [] // 切换部门时清空已选人员
+})
+
 // 修改部门变更处理方法
 const handleDeptChange = (deptId) => {
   if (!deptId) {
@@ -192,7 +253,7 @@ const handleDeptChange = (deptId) => {
     // 重置部门选择
     selectedDeptId.value = ''
     deptTreeRef.value?.treeRef?.setCurrentKey(undefined) // 清除树的选择状态
-    deptTreeRef.value?.treeRef?.setCurrentNode(null)
+    deptTreeRef.value?.treeRef?.setCurrentNode(undefined)
     return
   }
   selectedDeptId.value = deptId
@@ -219,6 +280,26 @@ const getDeviceList = async () => {
   }
 }
 
+// 选择部门后 加载部门人员
+const loadDeptPersons = async (deptId: number) => {
+  if (!deptId) {
+    simpleUsers.value = []
+    return
+  }
+  try {
+    // 调用API获取部门人员
+    const params = { deptId: deptId, pageNo: 1, pageSize: 10 }
+    const data = await UserApi.simpleUserList(params)
+    simpleUsers.value = data.map(user => ({
+      id: user.id,
+      nickname: user.nickname || user.username
+    }))
+  } catch (error) {
+    console.error('获取部门人员失败:', error)
+    simpleUsers.value = []
+  }
+}
+
 // 计算选中的设备原部门集合
 const originDeptIds = computed(() =>
   selectedDevices.value
@@ -237,6 +318,22 @@ const validateDeptSelection = (deptId: number) => {
   return true
 }
 
+// 设备过滤文本
+const deviceFilterText = ref('')
+
+// 计算属性:过滤设备列表
+const filteredDevices = computed(() => {
+  const searchText = deviceFilterText.value.toLowerCase().trim()
+  if (!searchText) return simpleDevices.value
+
+  return simpleDevices.value.filter(device => {
+    return (
+      (device.deviceCode || '').toLowerCase().includes(searchText) ||
+      (device.deviceName || '').toLowerCase().includes(searchText)
+    )
+  })
+})
+
 const removeTempRelation = (deviceId: number) => {
   tempRelationsMap.value.delete(deviceId)
   selectedDevices.value = selectedDevices.value.filter(id => id !== deviceId)
@@ -256,13 +353,15 @@ const submitRelations = async () => {
     const submitData = tempRelations.value.map(r => ({
       deviceId: r.deviceId,
       deptId: r.deptId,
-      reason: r.reason
+      reason: r.reason,
+      personIds: selectedPersons.value // 添加人员ID列表
     }))
     await IotDeviceApi.saveDeviceAllot(submitData)
     // 模拟API调用
     console.log('提交数据:', submitData)
     ElMessage.success('提交成功')
     tempRelations.value = []
+    delView(unref(router.currentRoute.value))
     router.back()
   } catch (error) {
     ElMessage.error('提交失败,请重试')
@@ -328,7 +427,7 @@ onMounted(async () => {
 }
 
 .submit-area {
-  margin-top: 5px;
+  margin-top: 20px;
   text-align: center;
 }
 
@@ -388,4 +487,61 @@ h3 {
   min-height: 400px;/* 统一最小高度 */
 }
 
+.filter-input {
+  margin-bottom: 15px;
+}
+
+.no-data {
+  padding: 20px;
+  text-align: center;
+  color: #999;
+}
+
+.control-row {
+  display: flex;
+  width: 100%;
+  gap: 20px;
+}
+
+/* 调整文本域高度 */
+.reason-input-wrapper :deep(.el-textarea__inner) {
+  height: 100% !important;
+  min-height: 100px;
+}
+
+/* 响应式调整 */
+@media (max-width: 992px) {
+  .control-row {
+    flex-direction: column;
+    gap: 15px;
+  }
+
+  .control-group {
+    width: 100%;
+  }
+}
+
+.selection-area {
+  display: flex;
+}
+
+.control-group {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.control-title {
+  display: block;
+  margin-bottom: 8px;
+  font-weight: bold;
+  color: #606266;
+}
+
+.person-selector,
+.reason-input-wrapper {
+  flex: 1;
+  min-height: 100px;
+}
 </style>

+ 8 - 11
src/views/pms/device/allotlog/DeviceAllot.vue

@@ -110,7 +110,7 @@
               @click="openForm('create', undefined, queryParams.deptId)"
               v-hasPermi="['rq:iot-device:create']"
             >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('devicePerson.setUp') }}
+              <Icon icon="ep:plus" class="mr-5px" /> {{ t('deviceAllot.setUp') }}
             </el-button>
             <el-button
               type="success"
@@ -163,7 +163,7 @@
                 @click="handleView(scope.row.id)"
                 v-hasPermi="['rq:iot-device:query']"
               >
-                {{ t('deviceStatus.adjustmentRecords') }}
+                {{ t('deviceAllot.adjustmentRecords') }}
               </el-button>
             </template>
           </el-table-column>
@@ -233,14 +233,7 @@ const treeShow = ref(true)
 const currentDeviceId = ref() // 设备id
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
-const shou = (tree) =>{
-  treeShow.value = !tree
-  if (tree) {
-    contentSpan.value = 20
-  } else {
-    contentSpan.value = 24
-  }
-}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -274,9 +267,13 @@ const resetQuery = () => {
 }
 
 /** 查看设备调拨详情 */
-const handleView = async (deviceId) => {
+const handleView = async (deviceId: number) => {
   currentDeviceId.value = deviceId
   drawerVisible.value = true
+  // 强制重新加载数据
+  nextTick(() => {
+    showDrawer.value?.loadDeviceAllots(deviceId)
+  })
   showDrawer.value.openDrawer()
 }
 

+ 23 - 3
src/views/pms/device/allotlog/DeviceAllotLogDrawer.vue

@@ -31,7 +31,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="loadDeviceAllots(props.deviceId)"
+          @pagination="handlePagination"
         />
       </div>
     </template>
@@ -40,7 +40,7 @@
 </template>
 <script setup lang="ts">
 const { t } = useI18n() // 国际化
-import { ref, watch, defineOptions, defineEmits } from 'vue'
+import { ref, watch, defineOptions, defineEmits, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
 import {dateFormatter} from "@/utils/formatTime";
 import {IotDeviceAllotLogApi} from "@/api/pms/iotdeviceallotlog";
@@ -75,6 +75,16 @@ const props = defineProps({
   deviceId: Number
 })
 
+// 使用 toRefs 解构响应式属性
+const { modelValue, deviceId } = toRefs(props)
+
+// 监听抽屉打开事件
+watch(modelValue, (isOpen) => {
+  if (isOpen && deviceId.value) {
+    loadDeviceAllots(deviceId.value)
+  }
+})
+
 // 监听bom树节点ID变化
 watch(() => props.deviceId, async (newVal) => {
   if (newVal) {
@@ -83,7 +93,7 @@ watch(() => props.deviceId, async (newVal) => {
 })
 
 // 加载设备的状态调整记录
-const loadDeviceAllots = async (deviceId) => {
+const loadDeviceAllots = async (deviceId: number) => {
   queryParams.deviceId = deviceId
   queryParams.pageNo = 1
   try {
@@ -99,6 +109,15 @@ const loadDeviceAllots = async (deviceId) => {
   }
 }
 
+// 修改分页处理
+const handlePagination = (pagination: any) => {
+  queryParams.pageNo = pagination.page
+  queryParams.pageSize = pagination.limit
+  if (props.deviceId) {
+    loadDeviceAllots(props.deviceId)
+  }
+}
+
 // 打开抽屉
 const openDrawer = () => {
   drawerVisible.value = true
@@ -113,6 +132,7 @@ const closeDrawer = () => {
 const handleClose = () => {
   emit('update:modelValue', false)
   deviceAllots.value = []
+  total.value = 0
 }
 
 defineExpose({ openDrawer, closeDrawer, loadDeviceAllots }) // 暴露方法给父组件

+ 5 - 4
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -158,8 +158,11 @@ const formatTime = timestamp => {
 }
 const result = ref([])
 const getChart = async (range) =>{
-
-  result.value = await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1])
+  loading.value = true
+  await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1]).then(res=>{
+    result.value = res
+      loading.value = false
+  })
 }
 // 初始化图表
 const renderChart = async () => {
@@ -224,8 +227,6 @@ onMounted(async () => {
   })
   await getChart(dateRange.value)
   await renderChart()
-
-
 })
 </script>
 <style scoped lang="scss">

+ 1 - 1
src/views/pms/device/monitor/TdDeviceLabel.vue

@@ -9,7 +9,7 @@
       @click="selectTag(index)"
     >
       <span class="property">{{ tag.modelName }}</span>
-      <span class="value">{{ tag.valueType }}</span>
+      <span class="value">{{ tag.value }}</span>
     </div>
   </div>
 </template>

+ 50 - 5
src/views/pms/device/personlog/ConfigDevicePerson.vue

@@ -18,10 +18,20 @@
               @node-click="handleDeptDeviceTreeNodeClick"
             />
           </div>
+          <!-- 设备搜索框 -->
+          <div class="filter-input">
+            <el-input
+              v-model="deviceFilterText"
+              :placeholder="t('devicePerson.filterDevicePlaceholder')"
+              clearable
+              prefix-icon="Search"
+            />
+          </div>
+
           <el-scrollbar height="400px">
             <el-checkbox-group v-model="selectedDevices">
               <div
-                v-for="device in simpleDevices"
+                v-for="device in filteredDevices"
                 :key="device.id"
                 class="radio-item"
               >
@@ -52,7 +62,7 @@
             />
           </div>
 
-          <el-scrollbar height="400px">
+          <el-scrollbar height="450px">
             <el-checkbox-group v-model="selectedUsers" @change="handleUserSelectionChange">
               <div
                 v-for="user in simpleUsers"
@@ -88,7 +98,7 @@
         <el-table :data="tempRelations" style="width: 100%">
           <el-table-column prop="deviceNames" :label="t('devicePerson.deviceName')" width="200" />
           <el-table-column prop="userNames" :label="t('devicePerson.rp')" />
-          <el-table-column prop="reason" :label="t('devicePerson.reasonForAdjustment')" />
+          <el-table-column prop="reason" :label="t('configPerson.reasonForAdjustment')" />
           <el-table-column :label="t('devicePerson.operation')" width="120">
             <template #default="{ row }">
               <el-button
@@ -107,7 +117,7 @@
             type="primary"
             size="large"
             @click="submitRelations"
-            :disabled="tempRelations.length === 0"
+            :disabled="isSaveDisabled"
           >
             {{ t('iotMaintain.save') }}
           </el-button>
@@ -129,12 +139,15 @@ import {simpleUserList, UserVO} from "@/api/system/user";
 import { useRouter } from 'vue-router'
 const router = useRouter()
 const { t } = useI18n() // 国际化
+import { useTagsViewStore } from "@/store/modules/tagsView";
+
 defineOptions({ name: 'ConfigDevicePerson' })
 
 const simpleDevices = ref<IotDeviceVO[]>([])
 const simpleUsers = ref<UserVO[]>([])
 
 const deptList = ref<Tree[]>([]) // 树形结构
+const { delView } = useTagsViewStore() // 视图操作
 
 const formData = ref({
   id: undefined,
@@ -225,6 +238,12 @@ const handleDeptDeviceTreeNodeClick = async (row: { [key: string]: any }) => {
   await getDeviceList()
 }
 
+// 新增计算属性:判断保存按钮是否禁用
+const isSaveDisabled = computed(() => {
+  // 当没有调整记录或调整原因为空时禁用按钮
+  return tempRelations.value.length === 0 || !formData.value.reason.trim();
+});
+
 /** 获得 部门下的设备 列表 */
 const getDeviceList = async () => {
   try {
@@ -259,6 +278,22 @@ const getUserList = async () => {
   }
 }
 
+// 设备过滤文本
+const deviceFilterText = ref('')
+
+// 计算属性:过滤设备列表
+const filteredDevices = computed(() => {
+  const searchText = deviceFilterText.value.toLowerCase().trim()
+  if (!searchText) return simpleDevices.value
+
+  return simpleDevices.value.filter(device => {
+    return (
+      (device.deviceCode || '').toLowerCase().includes(searchText) ||
+      (device.deviceName || '').toLowerCase().includes(searchText)
+    )
+  })
+})
+
 // 新增输入处理方法
 const handleReasonInput = (value: string) => {
   formData.value.reason = value
@@ -334,9 +369,9 @@ const submitRelations = async () => {
     })
     await IotDevicePersonApi.saveDevicePersonRelation(submitData)
     // 模拟API调用
-    console.log('提交数据:', submitData)
     ElMessage.success('提交成功')
     tempRelations.value = []
+    delView(unref(router.currentRoute.value))
     router.back()
   } catch (error) {
     ElMessage.error('提交失败,请重试')
@@ -421,4 +456,14 @@ h3 {
   margin-left: 3px;
   vertical-align: middle;
 }
+
+.filter-input {
+  margin-bottom: 15px;
+}
+
+.no-data {
+  padding: 20px;
+  text-align: center;
+  color: #999;
+}
 </style>

+ 7 - 14
src/views/pms/device/personlog/DevicePerson.vue

@@ -194,7 +194,6 @@ const { push } = useRouter() // 路由跳转
 
 const loading = ref(true) // 列表的加载中
 const ifShow = ref(false)
-const isDetail = ref(false) // 是否查看详情
 const list = ref<IotDeviceVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
@@ -234,14 +233,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const contentSpan = ref(20)
 const treeShow = ref(true)
-const shou = (tree) =>{
-  treeShow.value = !tree
-  if (tree) {
-    contentSpan.value = 20
-  } else {
-    contentSpan.value = 24
-  }
-}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -319,17 +311,18 @@ const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-const handleUpload = (id: number) => {
-  push({ name: 'DeviceUpload', params: { id } })
-}
-
 /** 查看设备责任人调整详情 */
 const currentDeviceId = ref() // 设备id
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
-const handleView = async (deviceId) => {
+
+const handleView = async (deviceId: number) => {
   currentDeviceId.value = deviceId
   drawerVisible.value = true
+  // 强制重新加载数据
+  nextTick(() => {
+    showDrawer.value?.loadDevicePersons(deviceId)
+  })
   showDrawer.value.openDrawer()
 }
 

+ 25 - 5
src/views/pms/device/personlog/DevicePersonLogDrawer.vue

@@ -31,7 +31,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="loadDeviceStatuses(props.deviceId)"
+          @pagination="handlePagination"
         />
       </div>
     </template>
@@ -40,7 +40,7 @@
 </template>
 <script setup lang="ts">
 const { t } = useI18n() // 国际化
-import { ref, watch, defineOptions, defineEmits } from 'vue'
+import { ref, watch, defineOptions, defineEmits, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
 import {dateFormatter} from "@/utils/formatTime";
 import {DICT_TYPE} from "@/utils/dict";
@@ -76,15 +76,25 @@ const props = defineProps({
   deviceId: Number
 })
 
+// 使用 toRefs 解构响应式属性
+const { modelValue, deviceId } = toRefs(props)
+
+// 监听抽屉打开事件
+watch(modelValue, (isOpen) => {
+  if (isOpen && deviceId.value) {
+    loadDevicePersons(deviceId.value)
+  }
+})
+
 // 监听bom树节点ID变化
 watch(() => props.deviceId, async (newVal) => {
   if (newVal) {
-    await loadDeviceStatuses(newVal)
+    await loadDevicePersons(newVal)
   }
 })
 
 // 加载设备的状态调整记录
-const loadDeviceStatuses = async (deviceId) => {
+const loadDevicePersons = async (deviceId: number) => {
   queryParams.deviceId = deviceId
   queryParams.pageNo = 1
   try {
@@ -100,6 +110,15 @@ const loadDeviceStatuses = async (deviceId) => {
   }
 }
 
+// 修改分页处理
+const handlePagination = (pagination: any) => {
+  queryParams.pageNo = pagination.page
+  queryParams.pageSize = pagination.limit
+  if (props.deviceId) {
+    loadDevicePersons(props.deviceId)
+  }
+}
+
 // 打开抽屉
 const openDrawer = () => {
   drawerVisible.value = true
@@ -114,9 +133,10 @@ const closeDrawer = () => {
 const handleClose = () => {
   emit('update:modelValue', false)
   devicePersons.value = []
+  total.value = 0
 }
 
-defineExpose({ openDrawer, closeDrawer, loadDeviceStatuses }) // 暴露方法给父组件
+defineExpose({ openDrawer, closeDrawer, loadDevicePersons }) // 暴露方法给父组件
 
 </script>
 

+ 58 - 4
src/views/pms/device/statuslog/ConfigDeviceStatus.vue

@@ -18,10 +18,21 @@
               @node-click="handleDeptDeviceTreeNodeClick"
             />
           </div>
+
+          <!-- 设备搜索框 -->
+          <div class="filter-input">
+            <el-input
+              v-model="deviceFilterText"
+              :placeholder="t('devicePerson.filterDevicePlaceholder')"
+              clearable
+              prefix-icon="Search"
+            />
+          </div>
+
           <el-scrollbar height="400px">
             <el-checkbox-group v-model="selectedDevices"  @change="handleDeviceChange">
               <div
-                v-for="device in simpleDevices"
+                v-for="device in filteredDevices"
                 :key="device.id"
                 class="checkbox-item"
               >
@@ -105,7 +116,7 @@
           type="primary"
           size="large"
           @click="submitRelations"
-          :disabled="tempRelations.length === 0"
+          :disabled="isSaveDisabled"
         >
           {{ t('iotMaintain.save') }}
         </el-button>
@@ -123,7 +134,9 @@ import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
 import {simpleUserList, UserVO} from "@/api/system/user";
 import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
 import { useRouter } from 'vue-router'
+import { useTagsViewStore } from "@/store/modules/tagsView";
 const router = useRouter()
+
 const { t } = useI18n() // 国际化
 defineOptions({ name: 'ConfigDeviceStatus' })
 
@@ -137,6 +150,8 @@ const currentDevice = computed(() => {
   return simpleDevices.value.find(d => d.id === selectedDevice.value)
 })
 
+const { delView } = useTagsViewStore() // 视图操作
+
 const formData = ref({
   id: undefined,
   deviceCode: undefined,
@@ -198,7 +213,7 @@ watch(selectedDevices, (newVal, oldVal) => {
 
 // 修改保存方法
 const saveCurrentRelation = () => {
-  if (!formData.value.deviceStatus || !formData.value.reason) return
+  if (!formData.value.deviceStatus /*|| !formData.value.reason*/) return
 
   const statusDict = getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)
     .find(d => d.value === formData.value.deviceStatus)
@@ -243,6 +258,12 @@ const getDeviceList = async () => {
   }
 }
 
+// 添加计算属性:判断保存按钮是否禁用
+const isSaveDisabled = computed(() => {
+  // 当没有调整记录或调整原因为空时禁用按钮
+  return tempRelations.value.length === 0 || !formData.value.reason.trim();
+});
+
 // 添加状态变更处理
 const handleStatusChange = () => {
   if (selectedDevices.value.length > 0) {
@@ -263,6 +284,22 @@ const isMultiDevice = (row: any) => {
   return selectedDevices.value.includes(row.deviceId) && selectedDevices.value.length > 1
 }
 
+// 设备过滤文本
+const deviceFilterText = ref('')
+
+// 计算属性:过滤设备列表
+const filteredDevices = computed(() => {
+  const searchText = deviceFilterText.value.toLowerCase().trim()
+  if (!searchText) return simpleDevices.value
+
+  return simpleDevices.value.filter(device => {
+    return (
+      (device.deviceCode || '').toLowerCase().includes(searchText) ||
+      (device.deviceName || '').toLowerCase().includes(searchText)
+    )
+  })
+})
+
 // 修改删除方法
 const removeTempRelation = (deviceId: number) => {
   // 从暂存列表删除
@@ -282,16 +319,24 @@ const removeTempRelation = (deviceId: number) => {
 // 提交时处理数据
 const submitRelations = async () => {
   try {
+    // 检查是否有空的原因
+    const hasEmptyReason = tempRelations.value.some(
+      item => !item.reason?.trim()
+    )
+    if (hasEmptyReason) {
+      ElMessage.error('请填写所有设备的调整原因')
+      return
+    }
     // 转换数据结构
     const submitData = tempRelations.value.map(r => ({
       deviceId: r.deviceId,
       status: r.status,
       reason: r.reason
     }))
-
     await IotDeviceApi.saveDeviceStatuses(submitData)
     ElMessage.success('提交成功')
     tempRelations.value = []
+    delView(unref(router.currentRoute.value))
     router.back()
   } catch (error) {
     ElMessage.error('提交失败')
@@ -402,4 +447,13 @@ h3 {
   vertical-align: middle;
 }
 
+.filter-input {
+  margin-bottom: 15px;
+}
+
+.no-data {
+  padding: 20px;
+  text-align: center;
+  color: #999;
+}
 </style>

+ 7 - 17
src/views/pms/device/statuslog/DeviceStatus.vue

@@ -111,7 +111,7 @@
               @click="openForm('create', undefined, queryParams.deptId)"
               v-hasPermi="['rq:iot-device:create']"
             >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('devicePerson.setUp') }}
+              <Icon icon="ep:plus" class="mr-5px" /> {{ t('deviceStatus.setUp') }}
             </el-button>
             <el-button
               type="success"
@@ -205,7 +205,6 @@ const { push } = useRouter() // 路由跳转
 const drawerVisible = ref<boolean>(false)
 const loading = ref(true) // 列表的加载中
 const ifShow = ref(false)
-const isDetail = ref(false) // 是否查看详情
 const list = ref<IotDeviceVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const currentDeviceId = ref() // 设备id
@@ -246,14 +245,7 @@ const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 const contentSpan = ref(20)
 const treeShow = ref(true)
-const shou = (tree) =>{
-  treeShow.value = !tree
-  if (tree) {
-    contentSpan.value = 20
-  } else {
-    contentSpan.value = 24
-  }
-}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -285,12 +277,14 @@ const resultOptions = computed(() => [
 const showDrawer = ref()
 
 /** 查看设备状态调整详情 */
-const handleView = async (deviceId) => {
+const handleView = async (deviceId: number) => {
   currentDeviceId.value = deviceId
   drawerVisible.value = true
+  // 强制重新加载数据
+  nextTick(() => {
+    showDrawer.value?.loadDeviceStatuses(deviceId)
+  })
   showDrawer.value.openDrawer()
-  // 强制刷新物料数据
-  // await showDrawer.value.loadMaterials(nodeId)
 }
 
 /** 处理部门被点击 */
@@ -342,10 +336,6 @@ const handleDetail = (id: number) => {
   push({ name: 'DeviceDetailInfo', params: { id } })
 }
 
-const handleUpload = (id: number) => {
-  push({ name: 'DeviceUpload', params: { id } })
-}
-
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 23 - 3
src/views/pms/device/statuslog/DeviceStatusLogDrawer.vue

@@ -39,7 +39,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="loadDeviceStatuses(props.deviceId)"
+          @pagination="handlePagination"
         />
       </div>
     </template>
@@ -48,7 +48,7 @@
 </template>
 <script setup lang="ts">
 
-import { ref, watch, defineOptions, defineEmits } from 'vue'
+import { ref, watch, defineOptions, defineEmits, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
 import * as IotDeviceStatusLogApi from '@/api/pms/iotdevicestatuslog'
 import {dateFormatter} from "@/utils/formatTime";
@@ -56,6 +56,7 @@ import {DICT_TYPE} from "@/utils/dict";
 const drawerVisible = ref<boolean>(false)
 const emit = defineEmits(['update:modelValue', 'add', 'delete'])
 const { t } = useI18n() // 国际化
+
 defineOptions({
   name: 'DeviceStatusLogDrawer'
 })
@@ -84,6 +85,16 @@ const props = defineProps({
   deviceId: Number
 })
 
+// 使用 toRefs 解构响应式属性
+const { modelValue, deviceId } = toRefs(props)
+
+// 监听抽屉打开事件
+watch(modelValue, (isOpen) => {
+  if (isOpen && deviceId.value) {
+    loadDeviceStatuses(deviceId.value)
+  }
+})
+
 // 监听bom树节点ID变化
 watch(() => props.deviceId, async (newVal) => {
   if (newVal) {
@@ -92,7 +103,7 @@ watch(() => props.deviceId, async (newVal) => {
 })
 
 // 加载设备的状态调整记录
-const loadDeviceStatuses = async (deviceId) => {
+const loadDeviceStatuses = async (deviceId: number) => {
   queryParams.deviceId = deviceId
   queryParams.pageNo = 1
   try {
@@ -108,6 +119,15 @@ const loadDeviceStatuses = async (deviceId) => {
   }
 }
 
+// 修改分页处理
+const handlePagination = (pagination: any) => {
+  queryParams.pageNo = pagination.page
+  queryParams.pageSize = pagination.limit
+  if (props.deviceId) {
+    loadDeviceStatuses(props.deviceId)
+  }
+}
+
 // 打开抽屉
 const openDrawer = () => {
   drawerVisible.value = true

+ 6 - 3
src/views/pms/devicetemplate/TemplateForm.vue

@@ -15,8 +15,10 @@
               :data="deviceCategoryTree"
               :props="defaultProps"
               check-strictly
-              default-expand-all
-              value-key="deviceCategoryId"
+              filterable
+              clearable
+              value-key="id"
+              :default-expanded-keys="firstLevelKeys"
               placeholder="请选择设备分类"
               @node-click="handleDeviceCategoryTreeNodeClick"
             />
@@ -61,7 +63,7 @@ defineOptions({ name: 'DeviceTemplateForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-
+const firstLevelKeys = ref([])
 const treeStore = useTreeStore();
 const localDeviceCategoryId = ref(null);  // 通过store存储的设备分类id 由父组件传递过来
 const dialogVisible = ref(false) // 弹窗的是否展示
@@ -142,6 +144,7 @@ const getDeviceCategoryTree = async () => {
   let categoryTree: Tree = { id: 0, name: '顶级设备分类', children: [] }
   categoryTree.children = handleTree(res)
   deviceCategoryTree.value.push(categoryTree)
+  firstLevelKeys.value = deviceCategoryTree.value.map(node => node.id);
 }
 
 /** 处理 设备分类 树 被点击 */

+ 1 - 1
src/views/pms/failure/IotFailureReportForm.vue

@@ -62,7 +62,7 @@
       </el-form-item>
         </el-col>
         <el-col :span="12" v-if="formData.ifDeal">
-          <el-form-item :label="t('faultForm.failureTime')" prop="dealTime">
+          <el-form-item :label="t('faultForm.dealTime')" prop="dealTime">
             <el-date-picker
               @change="endTimeBlur"
               :disabled="disabled"

+ 2 - 0
src/views/pms/inspect/plan/InspectRouteList.vue

@@ -58,6 +58,7 @@
           :reserve-selection="false"
           :selectable="(row) => !row.disabled"
         />
+        <el-table-column label="设备编码" align="center" prop="deviceCode" />
         <el-table-column label="设备名称" align="center" prop="deviceName" />
         <el-table-column label="路线名称" align="center" prop="routeName" />
         <el-table-column
@@ -195,6 +196,7 @@ defineExpose({ open })
 const { wsCache } = useCache()
 const getList = async () => {
   loading.value = true
+  list.value = []
   try {
     const data = await IotInspectRouteApi.getDeviceIotInspectRoutePage(queryParams)
     debugger

+ 2 - 2
src/views/pms/iotlockstock/index.vue

@@ -9,7 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="工厂" prop="factoryId">
-        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
+        <el-select v-model="queryParams.factoryId" clearable filterable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -31,7 +31,7 @@
       </el-form-item>
       -->
       <el-form-item label="成本中心" prop="costCenterId">
-        <el-select v-model="queryParams.costCenterId" clearable placeholder="请选择" class="!w-240px">
+        <el-select v-model="queryParams.costCenterId" clearable filterable placeholder="请选择" class="!w-240px">
           <el-option
             v-for="item in costCenterList"
             :key="item.id"

+ 285 - 0
src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue

@@ -0,0 +1,285 @@
+<template>
+  <Dialog v-model="dialogVisible"
+          title="保养BOM明细"
+          style="width: 1500px; max-height: 800px" @close="handleClose" >
+    <ContentWrap>
+      <el-table
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+      >
+        <el-table-column label="设备编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" />
+        <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" />
+        <el-table-column label="保养项" align="center" prop="name" />
+        <!--
+        <el-table-column label="运行里程" align="center" prop="mileageRule" >
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.mileageRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="运行时间" align="center" prop="runningTimeRule" >
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.runningTimeRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="自然日期" align="center" prop="naturalDateRule" >
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.naturalDateRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column> -->
+
+        <template v-if="showTimeColumns">
+          <el-table-column label="上次保养运行时长(H)" align="center" prop="lastRunningTime" />
+          <el-table-column label="运行时间周期(H)" align="center" prop="nextRunningTime" />
+          <el-table-column label="距离下次保养运行时长(H)" align="center" prop="timePeriod" />
+        </template>
+
+        <template v-if="showMileageColumns">
+          <el-table-column label="上次保养里程数(KM)" align="center" prop="lastRunningKilometers" />
+          <el-table-column label="运行里程周期(KM)" align="center" prop="nextRunningKilometers" />
+          <el-table-column label="距离下次保养公里数(KM)" align="center" prop="kilometerCycle" />
+        </template>
+
+        <template v-if="showNaturalDateColumns">
+          <el-table-column label="上次保养自然日期" align="center" prop="lastNaturalDate"  width="220">
+            <template #default="scope">
+              <el-date-picker
+                v-model="scope.row.lastNaturalDate"
+                type="date"
+                placeholder="选择日期"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+                style="width: 60%"
+                :disabled="true"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column label="自然日周期(D)" align="center" prop="nextNaturalDate" />
+          <el-table-column label="下次保养自然日期" align="center" prop="naturalDatePeriod" />
+        </template>
+      </el-table>
+      <!-- 分页
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      /> -->
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DictDataVO } from '@/api/system/dict/dict.data'
+import * as ModelApi from '@/api/pms/model'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import dayjs from 'dayjs'
+import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
+import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
+import {propTypes} from "@/utils/propTypes";
+
+const emit = defineEmits(['close']) // 定义 success 事件,用于操作成功后的回调
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const queryFormRef = ref() // 搜索的表单
+const list = ref<IotMaintenanceBomVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  workOrderId: undefined,
+  planId: undefined,
+  deviceId: undefined
+})
+
+const props = defineProps({
+  flag: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
+})
+
+const selectedRow = ref(null)
+
+// 处理单选逻辑
+const selectRow = (row) => {
+  selectedRow.value = selectedRow.value?.id === row.id ? null : row
+  emit('choose', row)
+  dialogVisible.value = false
+}
+
+// 点击整行选中
+const handleRowClick = (row) => {
+  selectRow(row)
+}
+const open = async (id?: number, flag?: string, deviceId?: number) => {
+  await nextTick() // 确保DOM更新完成
+  queryParams.deviceId = deviceId
+  if('workOrder' === flag) {
+    // 加载保养工单 BOM
+    queryParams.workOrderId = id
+    queryParams.planId = undefined
+    await getWorkOrderList()
+  } else if ('plan' === flag) {
+    queryParams.planId = id
+    queryParams.workOrderId = undefined
+    await getPlanList()
+  }
+  dialogVisible.value = true
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+const getWorkOrderList = async () => {
+  loading.value = true
+  try {
+    const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams)
+    // 格式化日期字段 - 新增代码
+    data.forEach(item => {
+      if (item.lastNaturalDate) {
+        // 将时间戳转换为 YYYY-MM-DD 格式
+        item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
+      } else {
+        // 处理空值情况
+        item.lastNaturalDate = ''
+      }
+      // 计算下次保养运行时长 H
+      item.timePeriod = calculateTimePeriod(item);
+      // 计算下次保养公里数 KM
+      item.kilometerCycle = calculateKiloPeriod(item);
+      // 计算下次保养日期
+      item.naturalDatePeriod = calculateNextNaturalDate(item)
+    })
+    list.value = data
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const getPlanList = async () => {
+  loading.value = true
+  try {
+    const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
+    // 格式化日期字段 - 新增代码
+    data.forEach(item => {
+      if (item.lastNaturalDate) {
+        // 将时间戳转换为 YYYY-MM-DD 格式
+        item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
+      } else {
+        // 处理空值情况
+        item.lastNaturalDate = ''
+      }
+      // 计算下次保养运行时长 H
+      item.timePeriod = calculateTimePeriod(item);
+      // 计算下次保养公里数 KM
+      item.kilometerCycle = calculateKiloPeriod(item);
+      // 计算下次保养日期
+      item.naturalDatePeriod = calculateNextNaturalDate(item)
+    })
+    list.value = data
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleClose = () => {
+  // 重置状态避免多个弹窗出现
+  dialogVisible.value = false
+  loading.value = false
+  list.value = []
+
+  // 通知父组件弹窗已关闭
+  emit('close')
+}
+
+// 计算 距离下次保养运行时长 H
+const calculateTimePeriod = (item: IotMaintenanceBomVO) => {
+  if (item.runningTimeRule === 0) {
+    const next = Number(item.nextRunningTime) || 0;
+    const totalRun = Number(item.totalRunTime) || 0;
+    const lastRun = Number(item.lastRunningTime) || 0;
+    return next - (totalRun - lastRun);
+  }
+  return item.timePeriod; // 保持原始值
+};
+
+// 计算 距离下次保养公里数 KM
+const calculateKiloPeriod = (item: IotMaintenanceBomVO) => {
+  if (item.mileageRule === 0) {
+    const next = Number(item.nextRunningKilometers) || 0;
+    const totalRun = Number(item.totalMileage) || 0;
+    const lastRun = Number(item.lastRunningKilometers) || 0;
+    return next - (totalRun - lastRun);
+  }
+  return item.kilometerCycle; // 保持原始值
+};
+
+// 计算下次保养日期
+const calculateNextNaturalDate = (item: IotMaintenanceBomVO): string => {
+  if (item.naturalDateRule !== 0 || !item.lastNaturalDate || !item.nextNaturalDate) {
+    return '-'
+  }
+  return dayjs(item.lastNaturalDate).add(item.nextNaturalDate, 'day').format('YYYY-MM-DD')
+}
+
+// 新增计算属性:控制时间相关列的显示
+const showTimeColumns = computed(() => {
+  return list.value.some(item => item.runningTimeRule === 0);
+});
+
+// 新增计算属性:控制里程相关列的显示
+const showMileageColumns = computed(() => {
+  return list.value.some(item => item.mileageRule === 0);
+});
+
+// 新增计算属性:自然日期相关列的显示
+const showNaturalDateColumns = computed(() => {
+  return list.value.some(item => item.naturalDateRule === 0);
+});
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  // getList()
+}
+const choose = (row: DictDataVO) => {
+  emit('choose', row)
+  dialogVisible.value = false
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+/** 初始化 **/
+// onMounted(async () => {
+//   await getList()
+//   // 查询字典(精简)列表
+// })
+</script>
+<style lang="scss">
+.no-label-radio .el-radio__label {
+  display: none;
+}
+.no-label-radio .el-radio__inner {
+  margin-right: 0;
+}
+</style>

+ 46 - 23
src/views/pms/iotmainworkorder/IotDeviceMainAlarm.vue

@@ -73,9 +73,12 @@
           </el-table-column>
           <el-table-column label="距离保养" align="center">
             <template #default="scope">
-          <span :class="getDistanceClass(scope.row.mainDistance)">
-            {{ scope.row.mainDistance }}
-          </span>
+              <template v-if="hasMaintenancePlan(scope.row.mainDistance)">
+                <span :class="getDistanceClass(scope.row.mainDistance)">
+                  {{ scope.row.mainDistance }}
+                </span>
+              </template>
+              <span v-else>无保养计划</span>
             </template>
           </el-table-column>
           <el-table-column label="所在部门" align="center" prop="deptName" />
@@ -85,17 +88,17 @@
             </template>
           </el-table-column>
           <el-table-column label="操作" align="center" min-width="120px">
-            <!-- <template #default="scope">
-
+            <template #default="scope">
               <el-button
                 link
                 type="primary"
-                @click="handleView(scope.row.id)"
+                @click="openBomForm(scope.row)"
                 v-hasPermi="['rq:iot-device:query']"
+                v-if="hasMaintenancePlan(scope.row.mainDistance)"
               >
-                调整记录
+                详情
               </el-button>
-            </template> -->
+            </template>
           </el-table-column>
         </el-table>
         <!-- 分页 -->
@@ -108,6 +111,7 @@
       </ContentWrap>
     </el-col>
   </el-row>
+  <DeviceAlarmBomList ref="modelFormRef" :flag = "flag" />
 </template>
 
 <script setup lang="ts">
@@ -118,17 +122,18 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import DeptTree from '@/views/system/user/DeptTree.vue'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import DeviceAlarmBomList from "@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue";
 
 /** 设备台账 列表 */
-defineOptions({ name: 'IotDevicePerson' })
+defineOptions({ name: 'IotDeviceMainAlarm' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const { push } = useRouter() // 路由跳转
-
+const modelFormRef = ref()
 const loading = ref(true) // 列表的加载中
 const ifShow = ref(false)
-const isDetail = ref(false) // 是否查看详情
+
 const list = ref<IotDeviceVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
@@ -165,17 +170,11 @@ const queryParams = reactive({
   setFlag: ''
 })
 const queryFormRef = ref() // 搜索的表单
+const flag = ref() // 查询保养计划或保养工单的标识
 const exportLoading = ref(false) // 导出的加载中
 const contentSpan = ref(20)
 const treeShow = ref(true)
-const shou = (tree) =>{
-  treeShow.value = !tree
-  if (tree) {
-    contentSpan.value = 20
-  } else {
-    contentSpan.value = 24
-  }
-}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -245,6 +244,12 @@ const getDistanceClass = (distance: number | string | null) => {
   return '';
 };
 
+// 判断是否有保养计划
+const hasMaintenancePlan = (mainDistance: any) => {
+  // 检查:非null、非undefined、非空字符串
+  return mainDistance != null && mainDistance !== '';
+};
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
@@ -286,10 +291,28 @@ const handleUpload = (id: number) => {
 const currentDeviceId = ref() // 设备id
 const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
-const handleView = async (deviceId) => {
-  currentDeviceId.value = deviceId
-  drawerVisible.value = true
-  showDrawer.value.openDrawer()
+
+const detail = (row: any) => {
+  // 如果有工单ID,跳转到保养工单详情页
+  if (row.workOrderId) {
+    push({ name: 'IotMainWorkOrderDetail', params: { id: row.workOrderId } })
+  } else if (row.planId) {
+    // 如果有计划ID,跳转到保养计划详情页
+    push({ name: 'IotMaintenancePlanDetail', params: { id: row.planId } })
+  }else {
+    // 两者都为空的情况处理
+    message.warning('当前设备无保养工单或保养计划')
+  }
+}
+
+const openBomForm = async (row) => {
+  if (row.workOrderId) {
+    flag.value = 'workOrder';
+    modelFormRef.value.open(row.workOrderId, flag.value, row.id)
+  } else if (row.planId) {
+    flag.value = 'plan';
+    modelFormRef.value.open(row.planId, flag.value, row.id)
+  }
 }
 
 /** 导出按钮操作 */

+ 43 - 3
src/views/pms/iotmainworkorder/IotMainWorkOrder.vue

@@ -343,6 +343,21 @@
             style="width: 60%"
           />
         </el-form-item>
+        <el-form-item
+          label="推迟原因"
+          prop="delayReason"
+          v-if="configDialog.current?.mileageRule === 0 ||
+              configDialog.current?.runningTimeRule === 0 ||
+              configDialog.current?.naturalDateRule === 0"
+        >
+          <el-input
+            v-model="configDialog.form.delayReason"
+            type="textarea"
+            :rows="2"
+            placeholder="请填写推迟原因"
+            style="width: 60%"
+          />
+        </el-form-item>
       </div>
 
       <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
@@ -549,7 +564,9 @@ const configDialog = reactive({
     // 提前量
     kiloCycleLead: 0,
     timePeriodLead: 0,
-    naturalDatePeriodLead: 0
+    naturalDatePeriodLead: 0,
+    // 推迟原因
+    delayReason: ''
   }
 })
 
@@ -581,7 +598,9 @@ const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
     // 提前量
     kiloCycleLead: row.kiloCycleLead || 0,
     timePeriodLead: row.timePeriodLead || 0,
-    naturalDatePeriodLead: row.naturalDatePeriodLead || 0
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
+    // 推迟原因
+    delayReason: row.delayReason || ''
   }
   configDialog.visible = true
 }
@@ -824,6 +843,20 @@ const submitForm = async () => {
   }
 }
 
+const validateDelayReason = (rule: any, value: any, callback: any) => {
+  const form = configDialog.form
+  const hasDelay =
+    (form.delayKilometers > 0) ||
+    (form.delayDuration > 0) ||
+    (form.delayNaturalDate > 0)
+
+  if (hasDelay && (!value || value.trim() === '')) {
+    callback(new Error('请填写推迟原因'))
+  } else {
+    callback()
+  }
+}
+
 // 新增表单校验规则
 const configFormRules = reactive({
   nextRunningKilometers: [{
@@ -855,7 +888,11 @@ const configFormRules = reactive({
     required: true,
     message: '提前量必须填写',
     trigger: 'blur'
-  }]
+  }],
+  // 新增推迟原因验证规则
+  delayReason: [
+    { validator: validateDelayReason, trigger: ['blur', 'change'] }
+  ]
 })
 
 /** 校验表格数据 */
@@ -930,6 +967,9 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
+
+
+
 onMounted(async () => {
   materialList.value = []
   const deptId = useUserStore().getUser.deptId

+ 21 - 2
src/views/pms/iotmainworkorder/IotMainWorkOrderDetail.vue

@@ -350,6 +350,21 @@
             :disabled="true"
           />
         </el-form-item>
+        <el-form-item
+          label="推迟原因"
+          prop="delayReason"
+          v-if="configDialog.current?.mileageRule === 0 ||
+              configDialog.current?.runningTimeRule === 0 ||
+              configDialog.current?.naturalDateRule === 0"
+        >
+          <el-input
+            v-model="configDialog.form.delayReason"
+            type="textarea"
+            :rows="2"
+            :disabled = true
+            style="width: 60%"
+          />
+        </el-form-item>
       </div>
 
       <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
@@ -555,7 +570,9 @@ const configDialog = reactive({
     // 提前量
     kiloCycleLead: 0,
     timePeriodLead: 0,
-    naturalDatePeriodLead: 0
+    naturalDatePeriodLead: 0,
+    // 推迟原因
+    delayReason: ''
   }
 })
 
@@ -587,7 +604,9 @@ const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
     // 提前量
     kiloCycleLead: row.kiloCycleLead || 0,
     timePeriodLead: row.timePeriodLead || 0,
-    naturalDatePeriodLead: row.naturalDatePeriodLead || 0
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
+    // 推迟原因
+    delayReason: row.delayReason || ''
   }
   configDialog.visible = true
 }

+ 11 - 2
src/views/pms/iotmainworkorder/WorkOrderMaterial.vue

@@ -164,6 +164,8 @@ const emit = defineEmits<{
   (e: 'choose', value: WorkOrderBomMaterialApi.IotMainWorkOrderBomMaterialVO[]): void
   (e: 'close'): void
 }>()
+// 跟踪新添加的行的索引
+const lastAddedIndex = ref<number | null>(null)
 const dialogVisible = ref(false) // 弹窗的是否展示
 const loading = ref(true) // 列表的加载中
 const queryFormRef = ref() // 搜索的表单
@@ -188,7 +190,8 @@ const handleView = () => {
   drawerVisible.value = true
   showDrawer.value.openDrawer()
 }
-const addMateriall = ref(null)
+const addMaterial = ref(null)
+
 const handleChildSubmit = (formData) => {
   const modified = removeOnesFromKeys(formData)
   modified.materialName = modified.name;
@@ -196,10 +199,16 @@ const handleChildSubmit = (formData) => {
   modified.materialSource = '手动添加';
   modified.quantity = modified.depleteCount;
   modified.unitPrice = modified.price;
-  addMateriall.value = modified;
+  addMaterial.value = modified;
   list.value.unshift(modified)
   total.value = total.value+1
+
+  // 将新添加的行添加到选中列表
+  selectedRows.value.push(modified)
+  // 设置新添加行的索引
+  lastAddedIndex.value = 0;
 }
+
 const removeOnesFromKeys = (obj: Record<string, any>) => {
   return Object.keys(obj).reduce(
     (acc, key) => {

+ 2 - 2
src/views/pms/iotsapstock/index.vue

@@ -9,7 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="工厂" prop="factoryId">
-        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
+        <el-select v-model="queryParams.factoryId" clearable filterable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -19,7 +19,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="库存地点" prop="storageLocationId">
-        <el-select v-model="queryParams.storageLocationId" clearable placeholder="请选择" class="!w-240px">
+        <el-select v-model="queryParams.storageLocationId" clearable filterable placeholder="请选择" class="!w-240px">
           <el-option
             v-for="item in storageLocationList"
             :key="item.id"

+ 4 - 2
src/views/pms/maintain/IotMaintain.vue

@@ -509,9 +509,11 @@ const selectChoose = (selectedMaterial) => {
   console.log('添加到本地列表的数据:', materialList.value)
 }
 const maintainChoose = (formData) => {
-  console.log('接收到的数据eee:', JSON.stringify(formData))
   formData.forEach((item) => {
-    list.value.push(item)
+    const index = list.value.findIndex((li) => item.name === li.name)
+    if (index==-1){
+      list.value.push(item)
+    }
   })
 }
 

+ 1 - 1
src/views/pms/maintain/IotMaintainDetail.vue

@@ -220,6 +220,7 @@
     @update:model-value="(val) => (drawerVisible = val)"
     :node-id="currentBomNodeId"
     :materials="materialList"
+    :detail="true"
   />
 </template>
 <script setup lang="ts">
@@ -322,7 +323,6 @@ const endRule = [
 ]
 const handleViewNew = (nodeId, bomId) => {
   drawerVisible.value = true
-  // showDrawer.value.openDrawer()
   const queryParams = {
     pageNo: 1,
     pageSize: 100,

+ 1 - 1
src/views/pms/maintain/index.vue

@@ -125,7 +125,7 @@
               type="primary"
               plain
               @click="openForm('create')"
-              v-hasPermi="['rq:iot-device:create']"
+              v-hasPermi="['rq:iot-maintain:create']"
               >
               <Icon icon="ep:plus" class="mr-5px" />  {{ t('maintain.added') }}
             </el-button>

+ 19 - 2
src/views/pms/maintenance/MainPlanDeviceList.vue

@@ -112,7 +112,9 @@ const handleRowClick = (row) => {
 }
 const open = async () => {
   dialogVisible.value = true
-  // queryParams.assetClass = classify
+  // 重置为初始参数(保留分页设置)
+  Object.assign(queryParams, initialQueryParams)
+  queryFormRef.value?.resetFields()
   await getList()
 }
 defineExpose({ open })
@@ -130,6 +132,19 @@ const getList = async () => {
   }
 }
 
+const initialQueryParams = {
+  pageNo: 1,
+  pageSize: 10,
+  label: '',
+  status: undefined,
+  deptId: undefined,
+  assetClass: undefined,
+  deviceName: undefined,
+  deviceCode: undefined,
+  name: undefined,
+  code: undefined
+}
+
 // 多选 切换行选中状态
 const toggleRow = (row) => {
   const index = selectedRows.value.findIndex(item => item.id === row.id);
@@ -180,7 +195,9 @@ const choose = (row: DictDataVO) => {
 }
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  // 重置为初始参数
+  Object.assign(queryParams, initialQueryParams)
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 </script>

+ 341 - 0
src/views/pms/map/DeviceMonitorDrawer.vue

@@ -0,0 +1,341 @@
+<template>
+  <el-drawer
+    title="设备运行监控"
+    :append-to-body="true"
+    :model-value="modelValue"
+    @update:model-value="$emit('update:modelValue', $event)"
+    :show-close="false"
+    :size="computedSize"
+    :before-close="handleClose"
+  >
+    <template v-if="true">
+      <ContentWrap v-loading="formLoading">
+        <ContentWrap>
+          <el-form style="height: 89px; margin-left: 20px">
+            <el-row style="display: flex; flex-direction: row">
+              <el-col :span="8">
+                <el-form-item prop="deviceCode">
+                  <template #label>
+                    <span class="custom-label">资产编码:</span>
+                  </template>
+                  <span class="custom-label">{{ formData.deviceCode }}</span>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item prop="deviceName">
+                  <template #label>
+                    <span class="custom-label">设备类别:</span>
+                  </template>
+                  <span class="custom-label">{{ formData.deviceName }}</span>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item prop="dept">
+                  <template #label>
+                    <span class="custom-label">所在部门:</span>
+                  </template>
+                  <span class="custom-label">{{ formData.dept }}</span>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item prop="ifInline">
+                  <template #label>
+                    <span class="custom-label">是否在线:</span>
+                  </template>
+                  <template #default>
+                    <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="formData.ifInline" />
+                  </template>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item prop="lastInlineTime">
+                  <template #label>
+                    <span class="custom-label">最后数据时间:</span>
+                  </template>
+                  <span class="custom-label">{{ formData.lastInlineTime }}</span>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </ContentWrap>
+        <ContentWrap>
+          <el-row>
+            <el-col :span="24">
+              <TdDeviceLabel :tags="specs" @select="labelSelect" tag-width="32%" />
+            </el-col>
+          </el-row>
+        </ContentWrap>
+        <ContentWrap>
+          <div class="chart-container">
+            <!-- 图表容器 -->
+            <el-date-picker
+              v-model="dateRange"
+              type="datetimerange"
+              :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+              start-placeholder="起始日期时间"
+              end-placeholder="结束日期时间"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              @change="handleDateChange"
+            />
+            <div v-loading="loading" style="height: 100%" ref="chartContainer"></div>
+          </div>
+        </ContentWrap>
+      </ContentWrap>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import {defineEmits, defineOptions, ref, watch} from 'vue'
+import { DICT_TYPE } from '@/utils/dict'
+import TdDeviceLabel from '@/views/pms/device/monitor/TdDeviceLabel.vue'
+import dayjs from "dayjs";
+import {IotStatApi} from "@/api/pms/stat";
+import * as echarts from "echarts";
+import {IotDeviceApi} from "@/api/pms/device";
+import {propTypes} from "@/utils/propTypes";
+
+const drawerVisible = ref<boolean>(false)
+const emit = defineEmits(['update:modelValue', 'add', 'delete'])
+
+defineOptions({
+  name: 'DeviceMonitorDrawer'
+})
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  bomId: '',
+  name: '',
+  code: ''
+})
+
+const windowWidth = ref(window.innerWidth)
+// 动态计算百分比
+const computedSize = computed(() => {
+  return windowWidth.value > 1200 ? '60%' : '80%'
+})
+
+const formLoading = ref(false)
+const total = ref(0) // 列表的总页数
+const materials = ref([])
+
+const props = defineProps({
+  id: propTypes.number.def(undefined),
+  dept: undefined,
+  deviceCode: undefined,
+  deviceName: undefined,
+  ifLine: undefined,
+  lastLineTime: undefined
+})
+
+const formData = ref({
+  id: undefined,
+  deptName: '',
+  deviceCode: '',
+  deviceName: '',
+  ifInline: undefined,
+  lastInlineTime: ''
+})
+const specs = ref([])
+
+// 响应式数据
+const startTime = ref('')
+const endTime = ref('')
+const topicName = ref([])
+const loading = ref(false)
+const topic = ref('')
+
+const handleDateChange = async (val) => {
+  if (val && val.length === 2) {
+    await getChart(val)
+    await renderChart()
+
+  }
+}
+const defaultEnd = dayjs()
+const defaultStart = defaultEnd.subtract(1, 'day')
+const dateRange = ref([
+  defaultStart.format('YYYY-MM-DD HH:mm:ss'),
+  defaultEnd.format('YYYY-MM-DD HH:mm:ss')
+])
+const labelSelect =async (row) =>{
+  topic.value = row.identifier
+  topicName.value = row.modelName
+  await getChart(dateRange.value)
+  await renderChart()
+}
+
+const chartContainer = ref(null)
+let chartInstance = null
+
+// 时间格式化(HH:mm)
+const formatTime = timestamp => {
+  return new Date(timestamp)
+    .toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit',second:'2-digit' })
+    .slice(0, 5)
+}
+const result = ref([])
+const getChart = async (range) =>{
+  loading.value = true
+  debugger
+  await IotStatApi.getDeviceInfoChart(props.deviceCode, topic.value, range[0], range[1]).then(res=>{
+    result.value = res
+    loading.value = false
+  })
+}
+// 初始化图表
+const renderChart = async () => {
+  debugger
+  if (!chartContainer.value) return
+
+  // 销毁旧实例
+  if (chartInstance) chartInstance.dispose()
+
+  chartInstance = markRaw(echarts.init(chartContainer.value))
+  const option = {
+    title:{
+      text: topicName.value+'数据趋势',
+      left:'center',
+    },
+    tooltip: { trigger: 'axis', },
+    xAxis: {
+      type: 'category',
+      // data: result.map(d => formatTime(d.timestamp)),
+      data: result.value.map(item => Object.keys(item)[0]),
+      axisLabel: { rotate: 45 }, // X轴标签旋转防止重叠
+      inverse: true,
+    },
+    yAxis: { type: 'value' },
+    dataZoom: [{
+      type: 'slider',
+      xAxisIndex: 0,
+      start: 0,  // 初始显示范围开始位置
+      end: 100   // 初始显示范围结束位置:ml-citation{ref="7" data="citationList"}
+    }],
+    series: [{
+      // data: result.map(d => d.value),
+      data: result.value.map(item => {
+        const key = Object.keys(item)[0]; // 获取当前元素的key
+        return item[key][0][topic.value]; // 提取value数组中第一个对象的属性
+      }),
+      type: 'line',
+      smooth: true,
+      areaStyle: {} // 显示区域填充
+    }]
+  }
+
+  chartInstance.setOption(option)
+
+  // 窗口自适应
+  window.addEventListener('resize', () => chartInstance.resize())
+}
+watch(() => props.id, async (newVal) => {
+  if (newVal) {
+    await init()
+  }
+})
+const init =async () =>{
+  drawerVisible.value = true
+  formLoading.value = true
+
+  formData.value.deviceCode = props.deviceCode
+  formData.value.deviceName = props.deviceName
+  formData.value.lastInlineTime = props.lastLineTime
+  formData.value.ifInline = props.ifLine
+  formData.value.dept = props.dept
+  debugger
+  await IotDeviceApi.getIotDeviceTds(props.id).then(res => {
+    specs.value = res
+    specs.value = specs.value.sort((a, b) => {
+      return b.modelOrder - a.modelOrder
+    })
+    formLoading.value = false
+    topic.value = specs.value[0].identifier
+    topicName.value = specs.value[0].modelName
+  })
+  await getChart(dateRange.value)
+  debugger
+  await renderChart()
+}
+// 打开抽屉
+const openDrawer = async () => {
+  if (props.id) {
+    await init()
+  }
+}
+
+// 关闭抽屉
+const closeDrawer = () => {
+  drawerVisible.value = false
+}
+
+// 关闭抽屉
+const handleClose = () => {
+  emit('update:modelValue', false)
+  materials.value = []
+}
+
+defineExpose({ openDrawer, closeDrawer }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped>
+.container {
+  width: 100%;
+  margin: 20px auto;
+  padding: 24px;
+  //background: #f8f9fa;
+  border-radius: 12px;
+}
+.chart-container {
+  width: 100%;
+  height: 600px;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
+}
+
+.date-controls {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-bottom: 20px;
+}
+
+input[type="datetime-local"] {
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  transition: border-color 0.2s;
+}
+
+input[type="datetime-local"]:focus {
+  border-color: #409eff;
+  outline: none;
+}
+
+.separator {
+  color: #606266;
+}
+
+.query-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: opacity 0.2s;
+}
+
+.query-btn:hover {
+  opacity: 0.8;
+}
+
+.custom-label{
+  font-size: 17px;
+  font-weight: bold;
+}
+</style>

+ 376 - 0
src/views/pms/map/Map.vue

@@ -0,0 +1,376 @@
+<template>
+  <div class="map-container">
+    <div id="baidu-map" ref="mapContainer"></div>
+    <div class="map-controls">
+      <button @click="zoomIn">放大</button>
+      <button @click="zoomOut">缩小</button>
+      <button @click="toggleMapType"
+      >切换地图类型({{ mapType === 'BMAP_NORMAL_MAP' ? '地图' : '卫星' }})</button>
+    </div>
+  </div>
+  <DeviceMonitorDrawer :model-value="drawerVisible" @update:model-value="(val) => (drawerVisible = val)" :id="deviceId" :deviceName="deviceName" :lastLineTime="lastLineTime"
+                       :ifLine="ifLine" :dept="dept" :deviceCode="deviceCode"
+                       ref="showDrawer" />
+</template>
+
+<script setup lang="ts">
+import { onBeforeUnmount, onMounted, ref } from 'vue'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import DeviceMonitorDrawer from '@/views/pms/map/DeviceMonitorDrawer.vue'
+import * as DeptApi from '@/api/system/dept'
+
+interface Cluster {
+  lng: number
+  lat: number
+  count: number
+  devices?: IotDeviceVO[]
+}
+defineOptions({ name: 'DeviceMap' })
+const showDrawer = ref()
+const drawerVisible = ref<boolean>(false)
+const mapContainer = ref<HTMLElement | null>(null)
+const map = ref<any>(null)
+const mapType = ref<'BMAP_NORMAL_MAP' | 'BMAP_SATELLITE_MAP'>('BMAP_NORMAL_MAP')
+const selectedDevice = ref<IotDeviceVO | null>(null)
+const hoverDevice = ref<IotDeviceVO | null>(null)
+const clusters = ref<Cluster[]>([])
+const deviceId = ref()
+const deviceName = ref('')
+const lastLineTime = ref('')
+const ifLine = ref()
+const dept = ref('')
+const deviceCode = ref('')
+
+// 设备数据示例
+const devices = ref<IotDeviceVO[]>()
+
+// 初始化地图
+const initMap = () => {
+  if (!mapContainer.value) return
+
+  const script = document.createElement('script')
+  script.src = `https://api.map.baidu.com/api?v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0&type=webgl`
+  script.async = true
+  script.onload = () => {
+    if ((window as any).BMap) {
+      map.value = new (window as any).BMap.Map(mapContainer.value)
+      const point = new (window as any).BMap.Point(104.114129, 37.550339)
+      map.value.centerAndZoom(point, 6)
+
+      map.value.enableScrollWheelZoom(true)
+      map.value.setMapType((window as any)[mapType.value])
+
+      map.value.addControl(new (window as any).BMap.NavigationControl())
+      map.value.addControl(new (window as any).BMap.ScaleControl())
+
+      initDeviceMarkers()
+      map.value.addEventListener('zoomend', () => {
+        initDeviceMarkers()
+      })
+    } else {
+      console.error('百度地图API加载失败')
+    }
+  }
+  script.onerror = () => {
+    console.error('百度地图API加载失败')
+  }
+  document.head.appendChild(script)
+}
+
+const initDeviceMarkers = () => {
+  if (!map.value) return
+
+  map.value.clearOverlays()
+
+  const zoomLevel = map.value.getZoom()
+  debugger
+  if (zoomLevel > 9) {
+    // 高缩放级别下显示单个设备标记
+    devices.value.forEach((device) => {
+      const point = new (window as any).BMap.Point(device.lng, device.lat)
+      const marker = createDeviceMarker(device, point)
+      map.value.addOverlay(marker)
+    })
+  } else {
+    // 低缩放级别下进行聚合
+    clusters.value = clusterDevices(devices.value, map.value)
+    clusters.value.forEach((cluster) => {
+      if (cluster.count === 1) {
+        // 只有一个设备时显示设备图标
+        const device = cluster.devices?.[0]
+        if (device) {
+          const point = new (window as any).BMap.Point(device.lng, device.lat)
+          const marker = createDeviceMarker(device, point)
+          map.value.addOverlay(marker)
+        }
+      } else if (cluster.count > 1) {
+        // 多个设备时显示聚合标签
+        const point = new (window as any).BMap.Point(cluster.lng, cluster.lat)
+        const label = createClusterLabel(cluster, point)
+        map.value.addOverlay(label)
+      }
+    })
+  }
+}
+
+const createDeviceMarker = (device: IotDeviceVO, point: any) => {
+  // 根据设备是否在线选择不同的图标
+  const iconUrl = device.ifInline === 3
+    ? 'https://iot.deepoil.cc/images/dinggreen.svg'
+    : 'https://iot.deepoil.cc/images/dingout.svg';
+
+  const marker = new (window as any).BMap.Marker(point, {
+    icon: new (window as any).BMap.Icon(
+      iconUrl,
+      new (window as any).BMap.Size(40, 40),
+      {
+        anchor: new (window as any).BMap.Size(25, 40)
+        // imageOffset: new (window as any).BMap.Size(0, -5)
+      }
+    )
+  });
+
+  // 添加点击事件
+  marker.addEventListener('click', () => {
+    showDeviceInfoWindow(device, point);
+  });
+
+  return marker;
+};
+
+const createClusterLabel = (cluster: Cluster, point: any) => {
+  const label = new (window as any).BMap.Label(cluster.count.toString(), {
+    position: point,
+    offset: new (window as any).BMap.Size(-10, -10)
+  })
+
+  // 创建一个 style 标签并添加到 head 中,定义呼吸动画
+  const style = document.createElement('style')
+  style.textContent = `
+    @keyframes breathing {
+      0% {
+        border-color: rgba(255, 255, 255, 0.3);
+      }
+      50% {
+        border-color: rgba(255, 255, 255, 0.8);
+      }
+      100% {
+        border-color: rgba(255, 255, 255, 0.3);
+      }
+    }
+  `
+  document.head.appendChild(style)
+
+  // 初始样式
+  label.setStyle({
+    color: '#fff',
+    backgroundColor: 'blue',
+    borderRadius: '50%',
+    width: '50px',
+    height: '50px',
+    textAlign: 'center',
+    lineHeight: '40px',
+    cursor: 'pointer',
+    fontSize: '18px',
+    fontWeight: 'bold',
+    boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
+    transition: 'all 0.3s ease',
+    border: '6px solid rgba(255, 255, 255, 0.5)', // 添加带有不透明度的边框
+    animation: 'breathing 1.5s infinite' // 添加呼吸动画
+  })
+
+  // 鼠标悬停样式
+  const hoverStyle = {
+    backgroundColor: '#2196df',
+    transform: 'scale(1.1)'
+  }
+
+  // 鼠标移出样式
+  const normalStyle = {
+    backgroundColor: '#c38f65',
+    transform: 'scale(1)'
+  }
+
+  // 添加点击事件
+  label.addEventListener('click', () => {
+    if (map.value) {
+      const currentZoom = map.value.getZoom();
+      const MAX_ZOOM = 19; // 手动设定最大缩放级别
+      const newZoom = Math.min(currentZoom + 3, MAX_ZOOM);
+      map.value.setZoom(newZoom);
+      map.value.panTo(point);
+      initDeviceMarkers(); // 重新计算并显示标记和聚合标签
+    }
+  })
+
+  return label
+}
+
+const clusterDevices = (devices: IotDeviceVO[], map: any): Cluster[] => {
+  const clusters: Cluster[] = []
+  const gridSize = getGridSize(map.getZoom())
+  debugger
+
+  const gridMap = new Map<string, Cluster>()
+
+  devices.forEach((device) => {
+    const gridKey = `${Math.floor(device.lng / gridSize)}_${Math.floor(device.lat / gridSize)}`
+
+    if (!gridMap.has(gridKey)) {
+      gridMap.set(gridKey, {
+        lng: Math.floor(device.lng / gridSize) * gridSize + gridSize / 2,
+        lat: Math.floor(device.lat / gridSize) * gridSize + gridSize / 2,
+        count: 0,
+        devices: []
+      })
+    }
+
+    const cluster = gridMap.get(gridKey)!
+    cluster.count++
+    if (!cluster.devices) cluster.devices = []
+    cluster.devices.push(device)
+  })
+
+  return Array.from(gridMap.values())
+}
+
+const getGridSize = (zoom: number): number => {
+  if (zoom <= 5) return 2
+  if (zoom <= 8) return 1
+  if (zoom < 10) return 0.03
+  return 0.1
+}
+
+const showDeviceInfoWindow = (device: IotDeviceVO, point: any) => {
+  DeptApi.getDept(device.deptId).then(res=>{
+    dept.value = res.name
+    const content = `
+<div style="display: flex;flex-direction: column;justify-content: center;border: 1px solid #ccc;">
+        <div style="margin-top: 1px;padding: 8px">
+          <p><strong>设备编码:</strong> ${device.deviceCode}</p>
+          <p><strong>设备名称:</strong> ${device.deviceName}</p>
+          <p><strong>所在部门:</strong> ${res.name}</p>
+          <p><strong>位置:</strong> ${device.location}</p>
+          <p><strong>状态:</strong> ${getDictLabel(DICT_TYPE.PMS_DEVICE_STATUS, device.deviceStatus)}</p>
+          <p><strong>是否在线:</strong> ${getDictLabel(DICT_TYPE.IOT_DEVICE_STATUS, device.ifInline)}</p>
+          <p><strong>最后在线时间:</strong> ${device.lastInlineTime}</p>
+        </div>
+        <div style="margin-bottom: 5px;padding: 8px">
+            <button id="device-detail-btn" style=" background-color: #2196f3;
+              color: white;
+              border: none;
+              padding: 8px 16px;
+              border-radius: 4px;
+
+              cursor: pointer;">查看</button>
+        </div>
+        </div>
+      `
+    const infoWindow = new (window as any).BMap.InfoWindow(content, {
+      width: 350,
+      height: 270
+    })
+    map.value.openInfoWindow(infoWindow, point)
+
+    // 事件绑定(需延迟确保DOM加载)
+    setTimeout(function () {
+      document.getElementById('device-detail-btn').addEventListener('click', function () {
+        drawerVisible.value = true
+        deviceId.value = device.id;
+        deviceCode.value = device.deviceCode
+        deviceName.value = device.deviceName
+        ifLine.value = device.ifInline
+        lastLineTime.value = device.lastInlineTime
+        showDrawer.value.openDrawer()
+
+      })
+    }, 200)
+  })
+
+}
+
+const zoomIn = () => {
+  if (map.value) {
+    map.value.zoomIn()
+  }
+}
+
+const zoomOut = () => {
+  if (map.value) {
+    map.value.zoomOut()
+  }
+}
+
+const toggleMapType = () => {
+  if (!map.value) return
+
+  mapType.value = mapType.value === 'BMAP_NORMAL_MAP' ? 'BMAP_SATELLITE_MAP' : 'BMAP_NORMAL_MAP'
+  map.value.setMapType((window as any)[mapType.value])
+}
+
+const getData = async () => {
+  await IotDeviceApi.getMapDevice().then((res) => {
+    devices.value = res
+    initMap()
+  })
+}
+onMounted(async () => {
+  await getData()
+})
+
+onBeforeUnmount(() => {
+  if (map.value) {
+    // 清除地图上的覆盖物
+    map.value.clearOverlays();
+    // 移除地图容器中的内容
+    if (mapContainer.value) {
+      mapContainer.value.innerHTML = '';
+    }
+    // 解除地图的事件绑定
+    map.value.removeEventListener('zoomend', initDeviceMarkers);
+  }
+})
+
+//     return {
+//       mapContainer,
+//       selectedDevice,
+//       hoverDevice,
+//       // deviceStatusMap,
+//       zoomIn,
+//       zoomOut,
+//       toggleMapType
+//     }
+// })
+</script>
+
+<style scoped>
+.map-container {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+}
+
+#baidu-map {
+  width: 100%;
+  height: 100%;
+}
+
+.map-controls {
+  position: absolute;
+  top: 20px;
+  right: 20px; /* 修改为right定位 */
+  z-index: 1000;
+  display: flex;
+  flex-direction: column; /* 垂直排列按钮 */
+  gap: 10px;
+}
+
+.map-controls button {
+  padding: 5px 10px;
+  background: #fff;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  cursor: pointer;
+}
+</style>

+ 20 - 6
src/views/pms/material/MaterialForm.vue

@@ -16,13 +16,15 @@
         <el-col :span="12">
           <el-form-item label="所属物料组" prop="materialGroupId">
             <el-tree-select
+              filterable
               v-model="formData.materialGroupId"
-              :data="materialGroupList"
+              :data="materialGroupTree"
               :props="defaultProps"
+              :default-expanded-keys="firstLevelKeys"
               check-strictly
-              default-expand-all
-              node-key="materialGroupId"
-              value-key="materialGroupId"
+              clearable
+              node-key="id"
+              value-key="id"
               placeholder="请选择所属物料组"
             />
           </el-form-item>
@@ -92,6 +94,7 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const storedMaterialGroupId = ref(null);  // 通过store存储的物料组id 由父组件传递过来
+const materialGroupTree = ref<Tree[]>([])   // 物料组分类树
 
 const formData = ref({
   name: '',
@@ -111,7 +114,7 @@ const formRules = reactive<FormRules>({
 
 })
 const formRef = ref() // 表单 Ref
-const materialGroupList = ref<Tree[]>([]) // 树形结构
+const firstLevelKeys = ref<Tree[]>([])
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -131,10 +134,21 @@ const open = async (type: string, id?: number) => {
     }
   }
   // 加载 物料组 树
-  materialGroupList.value = handleTree(await MaterialGroupApi.getSimpleMaterialGroupList())
+  // materialGroupList.value = handleTree(await MaterialGroupApi.getSimpleMaterialGroupList())
+  await getMaterialGroupTree()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
+/** 获得 物料组分类 树 **/
+const getMaterialGroupTree = async () => {
+  materialGroupTree.value = []
+  const res = await MaterialGroupApi.getSimpleMaterialGroupList()
+  let categoryTree: Tree = { id: 0, name: '物料组分类', children: [] }
+  categoryTree.children = handleTree(res)
+  materialGroupTree.value.push(categoryTree)
+  firstLevelKeys.value = materialGroupTree.value.map(node => node.id);
+}
+
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {

+ 2 - 2
src/views/pms/material/MaterialGroupTree.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="head-container">
-    <el-input v-model="materialGroupName" class="mb-20px" clearable placeholder="请输入物料组名称">
+    <el-input v-model="materialGroupName" class="mb-20px" clearable placeholder="请输入分类名称">
       <template #prefix>
         <Icon icon="ep:search" />
       </template>
@@ -79,7 +79,7 @@ const handleMenuClick = (action) => {
 const getTree = async () => {
   const res = await MaterialGroupApi.getSimpleMaterialGroupList()
   materialGroupList.value = []
-  let device: Tree = { id: 0, name: '顶级物料组分类', children: [] }
+  let device: Tree = { id: 0, name: '物料组分类', children: [] }
   device.children = handleTree(res)
   materialGroupList.value.push(device)
   // materialGroupList.value.push(...handleTree(res))

+ 39 - 6
src/views/pms/materialgroup/MaterialGroupForm.vue

@@ -9,13 +9,17 @@
     >
       <el-form-item label="上级物料组" prop="parentId">
         <el-tree-select
+          ref="treeSelectRef"
+          filterable
           v-model="formData.parentId"
           :data="materialGroupTree"
           :props="defaultProps"
+          :default-expanded-keys="firstLevelKeys"
           check-strictly
-          default-expand-all
+          clearable
           placeholder="请选择上级物料组"
           value-key="id"
+          @filter="handleFilter"
         />
       </el-form-item>
       <el-form-item label="物料组名称" prop="name">
@@ -72,14 +76,18 @@ const formRules = reactive<FormRules>({
   status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const materialGroupTree = ref() // 树形结构
+const materialGroupTree = ref<Tree[]>([]) // 树形结构
+const firstLevelKeys = ref<Tree[]>([])
+
+const treeSelectRef = ref()
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: number, parentId: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+  formData.value.parentId = parentId
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -120,6 +128,21 @@ const submitForm = async () => {
   }
 }
 
+const handleFilter = (val: string) => {
+  // 当筛选值清空时重置展开状态
+  if (val === '') {
+    nextTick(() => {
+      const treeComponent = treeSelectRef.value?.treeRef
+      if (treeComponent) {
+        // 重置到初始展开状态
+        treeComponent.setExpandedKeys(firstLevelKeys.value)
+        // 滚动到顶部确保可视区域正确
+        treeComponent.scrollTo({ top: 0 })
+      }
+    })
+  }
+}
+
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
@@ -137,8 +160,18 @@ const resetForm = () => {
 const getTree = async () => {
   materialGroupTree.value = []
   const data = await MaterialGroupApi.getSimpleMaterialGroupList()
-  let materialGroup: Tree = { id: 0, name: '顶级物料组', children: [] }
-  materialGroup.children = handleTree(data)
-  materialGroupTree.value.push(materialGroup)
+  // let materialGroup: Tree = { id: 0, name: '顶级物料组', children: [] }
+  // materialGroup.children = handleTree(data)
+  // materialGroupTree.value.push(materialGroup)
+  // firstLevelKeys.value = materialGroupTree.value.map(node => node.id);
+
+  // 构建稳定的顶级节点
+  const rootNode = {
+    id: 0,
+    name: '物料组分类',
+    children: handleTree(data)
+  }
+  materialGroupTree.value = [rootNode]
+  firstLevelKeys.value = [rootNode.id]
 }
 </script>

+ 8 - 2
src/views/pms/materialgroup/index.vue

@@ -38,7 +38,7 @@
         <el-button
           type="primary"
           plain
-          @click="openForm('create')"
+          @click="openForm('create', undefined)"
           v-hasPermi="['rq:iot-material-group:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
@@ -58,6 +58,7 @@
       row-key="id"
       :default-expand-all="isExpandAll"
       v-if="refreshTable"
+      @row-click="handleClick"
     >
       <el-table-column prop="name" label="物料组名称" />
       <el-table-column prop="sort" label="排序" />
@@ -123,6 +124,11 @@ const queryFormRef = ref() // 搜索的表单
 const isExpandAll = ref(false) // 是否展开,默认全部展开
 const refreshTable = ref(true) // 重新渲染表格状态
 
+const parentId = ref('')
+const handleClick = (node: {}) => {
+  parentId.value = node.id
+}
+
 /** 查询物料组列表 */
 const getList = async () => {
   loading.value = true
@@ -158,7 +164,7 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+  formRef.value.open(type, id, parentId.value)
 }
 
 /** 删除按钮操作 */

+ 3 - 3
src/views/system/dept/DeptForm.vue

@@ -43,7 +43,7 @@
       </el-form-item>
       -->
       <el-form-item label="SAP工厂">
-        <el-select v-model="formData.factoryIds" multiple placeholder="请选择" @change="selectedFactoryChange">
+        <el-select v-model="formData.factoryIds" multiple placeholder="请选择" @change="selectedFactoryChange" filterable clearable>
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -53,7 +53,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="SAP成本中心">
-        <el-select v-model="formData.costCenterIds" multiple placeholder="请选择">
+        <el-select v-model="formData.costCenterIds" multiple placeholder="请选择" filterable clearable>
           <el-option
             v-for="item in costCenterList"
             :key="item.id"
@@ -63,7 +63,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="SAP库存地点">
-        <el-select v-model="formData.stockLocationIds" multiple placeholder="请选择">
+        <el-select v-model="formData.stockLocationIds" multiple placeholder="请选择" filterable clearable>
           <el-option
             v-for="item in stockLocationList"
             :key="item.id"

+ 1 - 1
src/views/system/user/UserAssignRoleForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="分配角色">
+  <Dialog v-model="dialogVisible" title="分配角色" :close-on-click-modal="false">
     <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
       <el-form-item label="用户名称">
         <el-input v-model="formData.username" :disabled="true" />

+ 1 - 1
src/views/system/user/UserForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" :close-on-click-modal="false">
     <el-form
       ref="formRef"
       v-loading="formLoading"