Explorar el Código

Merge remote-tracking branch 'origin/master'

lipenghui hace 1 mes
padre
commit
a1a10c22aa

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

@@ -86,4 +86,7 @@ export const IotStatApi = {
   getSafeCount: async (params: any) => {
     return await request.get({ url: `/rq/stat/home/safe` })
   },
+  getDeptStatistics: async (params: any) => {
+      return await request.get({ url: `/rq/iot-opeation-fill/getCount`, params })
+  },
 }

+ 4 - 1
src/locales/en.ts

@@ -963,7 +963,10 @@ export default {
     MaintItems:'MaintItems',
     nextMaintTime:'NextMaintTime(H)',
     nextMaintKil:'NextMaintKil(KM)',
-    nextMaintDate:'NextMaintDate'
+    nextMaintDate:'NextMaintDate',
+    completed: 'Completed',
+    delayed: 'Delayed',
+    maintaining: 'Maintaining'
   },
   inspect:{
     InspectionItems:'InspectionItems',

+ 4 - 1
src/locales/ru.ts

@@ -954,7 +954,10 @@ export default {
     MaintItems:'保养项',
     nextMaintTime:'距离下次保养运行时长(H)',
     nextMaintKil:'距离下次保养公里数(KM)',
-    nextMaintDate:'下次保养自然日期'
+    nextMaintDate:'下次保养自然日期',
+    completed: '完成',
+    delayed: '延时',
+    maintaining: '保养中'
   },
   inspect:{
     InspectionItems:'巡检项',

+ 5 - 2
src/locales/zh-CN.ts

@@ -497,7 +497,7 @@ export default {
   },
   operationFillForm:{
     team:'所属队伍',
-    sumTime:'累计运行时间(h)',
+    sumTime:'累计运行时间(H)',
     sumKil:'累计运行公里数(Km)',
     confirm:'确定',
     cancel:'取消',
@@ -957,7 +957,10 @@ export default {
     MaintItems:'保养项',
     nextMaintTime:'距离下次保养运行时长(H)',
     nextMaintKil:'距离下次保养公里数(KM)',
-    nextMaintDate:'下次保养自然日期'
+    nextMaintDate:'下次保养自然日期',
+    completed: '完成',
+    delayed: '延时',
+    maintaining: '保养中'
   },
   inspect:{
     InspectionItems:'巡检项',

+ 10 - 4
src/views/pms/iotmainworkorder/DeviceAlarmBomList.vue

@@ -216,9 +216,12 @@ const calculateTimePeriod = (item: IotMaintenanceBomVO) => {
     const next = Number(item.nextRunningTime) || 0;
     const totalRun = Number(item.totalRunTime) || 0;
     const lastRun = Number(item.lastRunningTime) || 0;
-    return next - (totalRun - lastRun);
+    const result = next - (totalRun - lastRun);
+    return Number(result.toFixed(2));
   }
-  return item.timePeriod; // 保持原始值
+  return typeof item.timePeriod === 'number'
+    ? Number(item.timePeriod.toFixed(2))
+    : item.timePeriod;
 };
 
 // 计算 距离下次保养公里数 KM
@@ -227,9 +230,12 @@ const calculateKiloPeriod = (item: IotMaintenanceBomVO) => {
     const next = Number(item.nextRunningKilometers) || 0;
     const totalRun = Number(item.totalMileage) || 0;
     const lastRun = Number(item.lastRunningKilometers) || 0;
-    return next - (totalRun - lastRun);
+    const result = next - (totalRun - lastRun);
+    return Number(result.toFixed(2));
   }
-  return item.kilometerCycle; // 保持原始值
+  return typeof item.kilometerCycle === 'number'
+    ? Number(item.kilometerCycle.toFixed(2))
+    : item.kilometerCycle;
 };
 
 // 计算下次保养日期

+ 76 - 32
src/views/pms/iotmainworkorder/IotMainWorkOrder.vue

@@ -40,22 +40,6 @@
                 disabled
               />
             </el-form-item>
-            <!--
-            <el-form-item label="责任人" prop="devicePersons">
-              <el-input type="text" v-model="formData.devicePersons"  disabled/>
-            </el-form-item>
-
-            <el-form-item label="责任人" prop="responsiblePerson">
-              <el-select v-model="formData.responsiblePerson" filterable clearable style="width: 100%">
-                <el-option
-                  v-for="item in deptUsers"
-                  :key="item.id"
-                  :label="item.nickname"
-                  :value="item.id"
-                />
-              </el-select>
-            </el-form-item>
-            -->
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('fault.start')" prop="actualStartTime">
@@ -533,10 +517,76 @@ const formData = ref({
   status: undefined,
   devicePersons: '',
 })
+
 const formRules = reactive({
   name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
-  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+  actualStartTime: [{
+    required: true,
+    message: t('fault.start') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const start = value ? dayjs(Number(value)) : null;
+
+      // 验证开始时间 <= 当前日期
+      if (start && start.isAfter(now)) {
+        callback(new Error(t('fault.start') + '不能超过当前日期'));
+        return;
+      }
+
+      const end = formData.value.actualEndTime
+        ? dayjs(Number(formData.value.actualEndTime))
+        : null;
+
+      // 只有当结束时间有效时,才比较开始和结束时间
+      if (end && end.isValid() && !end.isAfter(now)) {
+        if (start && start.isAfter(end)) {
+          callback(new Error(t('fault.start') + '不能超过' + t('fault.end')));
+          return;
+        }
+      }
+
+      // 触发结束时间的重新校验
+      if (formRef.value) {
+        formRef.value.validateField('actualEndTime', () => {});
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }],
+  actualEndTime: [{
+    required: true,
+    message: t('fault.end') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const end = value ? dayjs(Number(value)) : null;
+
+      // 验证结束时间 <= 当前日期
+      if (end && end.isAfter(now)) {
+        callback(new Error(t('fault.end') + '不能超过当前日期'));
+        return;
+      }
+
+      const start = formData.value.actualStartTime
+        ? dayjs(Number(formData.value.actualStartTime))
+        : null;
+
+      // 验证结束时间 >= 开始时间(仅当开始时间存在时)
+      if (start && end && end.isBefore(start)) {
+        callback(new Error(t('fault.end') + '必须大于等于' + t('fault.start')));
+        return;
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }]
 })
+
 const formRef = ref() // 表单 Ref
 
 interface MaterialFormExpose {
@@ -990,8 +1040,6 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
-
-
 onMounted(async () => {
   materialList.value = []
   const deptId = useUserStore().getUser.deptId
@@ -999,28 +1047,24 @@ onMounted(async () => {
   dept.value = await DeptApi.getDept(deptId)
   deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
   formData.value.deptId = deptId
-
   try{
     formType.value = 'update'
     // 查询保养工单 主表数据
     const workOrder = await IotMainWorkOrderApi.getIotMainWorkOrder(id);
     formData.value = workOrder
-    // 查询保养责任人
-    // const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0;
-    /* UserApi.getUser(personId).then((res) => {
-      formData.value.responsiblePerson = res.nickname;
-    }) */
     // 查询保养工单 明细数据
     const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams);
     list.value = []
     if (Array.isArray(data)) {
-      list.value = data.map(item => ({
-        ...item,
-        // 这里可以添加必要的字段转换(如果有日期等需要格式化的字段)
-        lastNaturalDate: item.lastNaturalDate ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD') : null
-      }))
-      // 同时查询所有设备的责任人
-      // await getDevicePersons();
+      // 添加过滤逻辑:只保留 status === 0 的记录
+      list.value = data
+        .filter(item => item.status === 0) // 关键过滤代码
+        .map(item => ({
+          ...item,
+          lastNaturalDate: item.lastNaturalDate
+            ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
+            : null
+        }))
     }
   } catch (error) {
     console.error('数据加载失败:', error)

+ 66 - 17
src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue

@@ -42,22 +42,6 @@
                 disabled
               />
             </el-form-item>
-            <!--
-            <el-form-item label="责任人" prop="devicePersons">
-              <el-input type="text" v-model="formData.devicePersons"  disabled/>
-            </el-form-item>
-
-            <el-form-item label="责任人" prop="responsiblePerson">
-              <el-select v-model="formData.responsiblePerson" filterable clearable style="width: 100%" disabled>
-                <el-option
-                  v-for="item in deptUsers"
-                  :key="item.id"
-                  :label="item.nickname"
-                  :value="item.id"
-                />
-              </el-select>
-            </el-form-item>
-            -->
           </el-col>
           <el-col :span="8">
             <el-form-item :label="t('fault.start')" prop="actualStartTime">
@@ -498,8 +482,73 @@ const getCurrentDeviceIds = (): number[] => {
 
 const formRules = reactive({
   name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
-  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+  actualStartTime: [{
+    required: true,
+    message: t('fault.start') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const start = value ? dayjs(Number(value)) : null;
+
+      // 验证开始时间 <= 当前日期
+      if (start && start.isAfter(now)) {
+        callback(new Error(t('fault.start') + '不能超过当前日期'));
+        return;
+      }
+
+      const end = formData.value.actualEndTime
+        ? dayjs(Number(formData.value.actualEndTime))
+        : null;
+
+      // 只有当结束时间有效时,才比较开始和结束时间
+      if (end && end.isValid() && !end.isAfter(now)) {
+        if (start && start.isAfter(end)) {
+          callback(new Error(t('fault.start') + '不能超过' + t('fault.end')));
+          return;
+        }
+      }
+
+      // 触发结束时间的重新校验
+      if (formRef.value) {
+        formRef.value.validateField('actualEndTime', () => {});
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }],
+  actualEndTime: [{
+    required: true,
+    message: t('fault.end') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const end = value ? dayjs(Number(value)) : null;
+
+      // 验证结束时间 <= 当前日期
+      if (end && end.isAfter(now)) {
+        callback(new Error(t('fault.end') + '不能超过当前日期'));
+        return;
+      }
+
+      const start = formData.value.actualStartTime
+        ? dayjs(Number(formData.value.actualStartTime))
+        : null;
+
+      // 验证结束时间 >= 开始时间(仅当开始时间存在时)
+      if (start && end && end.isBefore(start)) {
+        callback(new Error(t('fault.end') + '必须大于等于' + t('fault.start')));
+        return;
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }]
 })
+
 const formRef = ref() // 表单 Ref
 
 interface MaterialFormExpose {

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

@@ -176,9 +176,9 @@
             />
           </template>
         </el-table-column>
-        <el-table-column :label="t('iotMaintain.numberOfMaterials')" align="center" width="100">
+        <el-table-column :label="t('common.status')" align="center" width="100">
           <template #default="scope">
-            {{ hasMaterial(scope.row.bomNodeId) ? '是' : '否' }}
+            {{ getStatusText(scope.row) }}
           </template>
         </el-table-column>
         <el-table-column :label="t('iotMaintain.operation')" align="center" min-width="120px">
@@ -194,17 +194,6 @@
                   {{ t('form.set') }}
                 </el-button>
               </div>
-              <!--
-              <div style="margin-left: 12px">
-                <el-button
-                  link
-                  type="primary"
-                  @click="openMaterialForm(scope.row)"
-                >
-                  选择物料
-                </el-button>
-              </div>
-              -->
               <div style="margin-left: 12px">
                 <el-button
                   link
@@ -480,29 +469,23 @@
     @update:model-value="val => drawerVisible = val"
     :node-id="currentBomNodeId"
     :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
+    :hide-extra-columns="true"
   />
 </template>
 <script setup lang="ts">
-import { IotMaintainApi, IotMaintainVO } from '@/api/pms/maintain'
-import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
 import { ref } from 'vue'
-import type { ComponentPublicInstance } from 'vue'
 import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
 import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
 import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
-import { IotMaintenancePlanApi, IotMaintenancePlanVO } from '@/api/pms/maintenance'
 import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
-import MainPlanDeviceList from "@/views/pms/maintenance/MainPlanDeviceList.vue";
 import * as DeptApi from "@/api/system/dept";
 import {erpPriceTableColumnFormatter} from "@/utils";
 import dayjs from 'dayjs'
 import MaterialListDrawer from "@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue";
 import WorkOrderMaterial from "@/views/pms/iotmainworkorder/WorkOrderMaterial.vue";
-import {IotMaintainMaterialsApi} from "@/api/pms/maintain/materials";
 import {DICT_TYPE, getIntDictOptions, getStrDictOptions} from "@/utils/dict";
 
 /** 保养计划 表单 */
@@ -618,6 +601,24 @@ const openMaterialForm = (row: any) => {
   materialFormRef.value.open(formData.value.deptId, bomNodeId.value)
 }
 
+const getStatusText = (row: any) => {
+  // 状态为1直接返回"完成"
+  if (row.status === 1) return t('mainPlan.completed');
+
+  // 状态为0时判断延迟字段
+  const delayDuration = Number(row.delayDuration) || 0;
+  const delayKilometers = Number(row.delayKilometers) || 0;
+  const delayNaturalDate = Number(row.delayNaturalDate) || 0;
+
+  // 任意延迟字段大于0 -> 延时
+  if (delayDuration > 0 || delayKilometers > 0 || delayNaturalDate > 0) {
+    return t('mainPlan.delayed');
+  }
+
+  // 否则显示保养中
+  return t('mainPlan.maintaining');
+};
+
 const selectChoose = (selectedMaterial) => {
   selectedMaterial.bomNodeId = bomNodeId.value
   // 关联 bomNodeId
@@ -749,15 +750,6 @@ const submitForm = async () => {
     message.success(t('common.createSuccess'))
     close()
 
-    /* if (formType.value === 'create') {
-      await IotMaintenancePlanApi.createIotMaintenancePlan(data)
-      message.success(t('common.createSuccess'))
-      close()
-    } else {
-      await IotMaintainApi.updateIotMaintain(data)
-      message.success(t('common.updateSuccess'))
-      close()
-    } */
     // 发送操作成功的事件
     emit('success')
   } finally {

+ 8 - 5
src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue

@@ -13,9 +13,9 @@
       <div v-loading="loading" style="height: 100%">
         <el-table :data="filteredMaterials" style="width: 100%">
           <el-table-column prop="bomNodeId" :label="t('bomList.bomNode')" width="180" v-if="false"/>
-          <el-table-column prop="factory" :label="t('workOrderMaterial.factory')" width="180" />
-          <el-table-column prop="costCenter" :label="t('workOrderMaterial.costCenter')" width="180" />
-          <el-table-column prop="projectDepartment" :label="t('workOrderMaterial.storageLocation')"  width="180" />
+          <el-table-column prop="factory" :label="t('workOrderMaterial.factory')" width="180" v-if="!hideExtraColumns"/>
+          <el-table-column prop="costCenter" :label="t('workOrderMaterial.costCenter')" width="180" v-if="!hideExtraColumns"/>
+          <el-table-column prop="projectDepartment" :label="t('workOrderMaterial.storageLocation')"  width="180" v-if="!hideExtraColumns"/>
           <el-table-column prop="materialName" :label="t('workOrderMaterial.materialName')" width="180" />
           <el-table-column prop="materialCode" :label="t('workOrderMaterial.materialCode')" width="180" />
           <el-table-column prop="unit" :label="t('workOrderMaterial.unit')" width="180" />
@@ -50,7 +50,6 @@
 import { ref, watch, defineOptions, defineEmits } from 'vue'
 import { ElMessage } from 'element-plus'
 import * as PmsMaterialApi from '@/api/pms/material'
-import {dateFormatter} from "@/utils/formatTime";
 const drawerVisible = ref<boolean>(false)
 const emit = defineEmits(['update:modelValue', 'add', 'delete'])
 const { t } = useI18n() // 国际化
@@ -80,7 +79,11 @@ const total = ref(0) // 列表的总页数
 const props = defineProps({
   modelValue: Boolean,
   nodeId: Number,
-  materials: Array // 接收父组件传递的完整物料列表
+  materials: Array, // 接收父组件传递的完整物料列表
+  hideExtraColumns: {
+    type: Boolean,
+    default: false
+  }
 })
 
 // 监听bom树节点ID变化

+ 569 - 0
src/views/pms/iotopeationfill/statistics .vue

@@ -0,0 +1,569 @@
+<template>
+  <!-- 第一行:统计卡片行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="所属部门" prop="project_name">
+            <el-tree-select
+              v-model="queryParams.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+              clearable
+              style="width: 180px"
+            />
+          </el-form-item>
+          <el-form-item label="创建时间" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-220px"
+            />
+          </el-form-item>
+<!--          <el-form-item label="填写状态" prop="project_name">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="填写状态"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>-->
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </el-col>
+  </el-row>
+  <!-- 第二行:图表行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center">
+            <span class="text-base font-medium text-gray-600">运行记录填写统计</span>
+          </div>
+        </template>
+        <el-row class="h-[220px]">
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="reportingChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">总数</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">已填写</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">未填写</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="writeChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">填写中</span>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-col>
+  </el-row>
+
+  <!-- 第三行:消息统计行 -->
+  <el-row>
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center justify-between">
+            <span class="text-base font-medium text-gray-600">部门数量统计</span>
+          </div>
+        </template>
+        <div ref="chartContainer" class="h-[300px]"></div>
+      </el-card>
+    </el-col>
+  </el-row>
+
+</template>
+
+<script setup lang="ts" name="Index">
+import * as echarts from 'echarts/core'
+import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
+
+import {
+  GridComponent,
+  LegendComponent,
+  TitleComponent,
+  ToolboxComponent,
+  TooltipComponent
+} from 'echarts/components'
+import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import {
+  IotStatisticsDeviceMessageSummaryRespVO,
+  IotStatisticsSummaryRespVO
+} from '@/api/iot/statistics'
+import { formatDate } from '@/utils/formatTime'
+import { IotStatApi } from '@/api/pms/stat'
+import { ref, reactive, onMounted, onUnmounted } from "vue";
+import { defaultProps, handleTree } from "@/utils/tree";
+import * as DeptApi from "@/api/system/dept";
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import {useUserStore} from "@/store/modules/user";
+// 导入部门数据类型
+import { DeptTreeItem } from '@/api/system/dept'
+
+// 初始化echarts
+echarts.use([
+  TooltipComponent,
+  LegendComponent,
+  PieChart,
+  CanvasRenderer,
+  LabelLayout,
+  TitleComponent,
+  ToolboxComponent,
+  GridComponent,
+  LineChart,
+  UniversalTransition,
+  GaugeChart,
+  BarChart
+])
+
+const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
+const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
+
+// 定义部门数据类型
+interface DeptDataItem {
+  deptId: number;
+  name: string;
+  totalCount: number;
+  filledCount: number;
+  unfilledCount: number;
+  fillingCount: number;
+}
+
+/** IoT 首页 */
+defineOptions({ name: 'iotOpeationSta' })
+
+const timeRange = ref('7d') // 修改默认选择为近一周
+const dateRange = ref<[Date, Date] | null>(null)
+
+const queryParams = reactive({
+  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
+  endTime: Date.now(), // 设置默认结束时间为当前时间
+  deptId: null, // 选中的部门ID
+  status: null // 填写状态
+})
+
+const handleQuery = () => {
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+const resetQuery = () => {
+  // 重置查询参数
+  queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
+  queryParams.endTime = Date.now()
+  queryParams.deptId = null
+  queryParams.status = null
+
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+const reportingChartRef = ref() // 在线设备统计的图表
+const dealFinishedChartRef = ref() // 离线设备统计的图表
+const transOrderChartRef = ref() // 待激活设备统计的图表
+const orderFinishChartRef = ref()
+const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
+const writeChartRef = ref() // 待填写图表
+const finishedChartRef = ref() // 已完成图表
+
+// 基础统计数据
+const statsData = ref<IotStatisticsSummaryRespVO>({
+  productCategoryCount: 0,
+  productCount: 0,
+  deviceCount: 0,
+  deviceMessageCount: 0,
+  productCategoryTodayCount: 0,
+  productTodayCount: 0,
+  deviceTodayCount: 0,
+  deviceMessageTodayCount: 0,
+  deviceOnlineCount: 0,
+  deviceOfflineCount: 0,
+  deviceInactiveCount: 0,
+  productCategoryDeviceCounts: {}
+})
+
+const day = ref({
+  failureDay: undefined,
+  maintainDay: undefined
+})
+const week = ref({
+  failureWeek: undefined,
+  maintainWeek: undefined
+})
+const month = ref({
+  failureMonth: undefined,
+  maintainMonth: undefined
+})
+const total = ref({
+  failureTotal: undefined,
+  maintainTotal: undefined
+})
+
+const status = ref<IotStatusItem[]>([])
+
+// 定义状态项接口
+interface IotStatusItem {
+  totalCount: number
+  filledCount: number
+  unfilledCount: number
+  fillingCount: number
+}
+
+// 消息统计数据
+const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
+  upstreamCounts: {},
+  downstreamCounts: {}
+})
+
+/** 处理快捷时间范围选择 */
+const handleTimeRangeChange = (timeRange: string) => {
+  const now = Date.now()
+  let startTime: number
+
+  switch (timeRange) {
+    case '1h':
+      startTime = now - 60 * 60 * 1000
+      break
+    case '24h':
+      startTime = now - 24 * 60 * 60 * 1000
+      break
+    case '7d':
+      startTime = now - 7 * 24 * 60 * 60 * 1000
+      break
+    default:
+      return
+  }
+
+  // 清空日期选择器
+  dateRange.value = null
+
+  // 更新查询参数
+  queryParams.startTime = startTime
+  queryParams.endTime = now
+
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+/** 处理自定义日期范围选择 */
+const handleDateRangeChange = (value: [Date, Date] | null) => {
+  if (value) {
+    // 清空快捷选项
+    timeRange.value = ''
+
+    // 更新查询参数
+    queryParams.startTime = value[0].getTime()
+    queryParams.endTime = value[1].getTime()
+
+    // 重新获取数据
+    getStats()
+    initChart()
+  }
+}
+
+/** 获取统计数据 */
+const getStats = async () => {
+  // 获取基础统计数据
+  // 获取部门统计数据
+  IotStatApi.getDeptStatistics(queryParams).then((res) => {
+    deptDataList.value = res.deptCountList || [];
+    status.value = res.totalList;
+    initChart()
+    initCharts()
+  })
+}
+
+/** 初始化图表 */
+const initCharts = () => {
+  // 使用数组的第一个元素(如果存在)
+  const firstStatus = status.value[0] || {}
+  // 上报中
+  initGaugeChart(
+    reportingChartRef.value,
+    firstStatus.totalCount === undefined ? 0 : firstStatus.totalCount,
+    '#0d9'
+  )
+  // 处理完成
+  initGaugeChart(
+    dealFinishedChartRef.value,
+    firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
+    '#f50'
+  )
+  // 转工单
+  initGaugeChart(
+    transOrderChartRef.value,
+    firstStatus.unfilledCount === undefined ? 0 : firstStatus.unfilledCount,
+    '#05b'
+  )
+  // 待填写
+  initGaugeChart(
+    writeChartRef.value,
+    firstStatus.fillingCount === undefined ? 0 : firstStatus.fillingCount,
+    '#05b'
+  )
+}
+
+/** 初始化仪表盘图表 */
+const initGaugeChart = (el: any, value: number, color: string) => {
+  echarts.init(el).setOption({
+    series: [
+      {
+        type: 'gauge',
+        startAngle: 360,
+        endAngle: 0,
+        min: 0,
+        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
+        progress: {
+          show: true,
+          width: 12,
+          itemStyle: {
+            color: color
+          }
+        },
+        axisLine: {
+          lineStyle: {
+            width: 12,
+            color: [[1, '#E5E7EB']]
+          }
+        },
+        axisTick: { show: false },
+        splitLine: { show: false },
+        axisLabel: { show: false },
+        pointer: { show: false },
+        anchor: { show: false },
+        title: { show: false },
+        detail: {
+          valueAnimation: true,
+          fontSize: 24,
+          fontWeight: 'bold',
+          fontFamily: 'Inter, sans-serif',
+          color: color,
+          offsetCenter: [0, '0'],
+          formatter: (value: number) => {
+            return `${value} `
+          }
+        },
+        data: [{ value: value }]
+      }
+    ]
+  })
+}
+
+const chartContainer = ref(null)
+let chartInstance = null
+
+// 初始化部门统计图表
+const initChart = () => {
+  if (!chartContainer.value) return
+
+  // 准备数据
+  const deptNames = deptDataList.value.map(item => item.name)
+  const totalCounts = deptDataList.value.map(item => item.totalCount)
+  const filledCounts = deptDataList.value.map(item => item.filledCount)
+  const unfilledCounts = deptDataList.value.map(item => item.unfilledCount)
+  const fillingCounts = deptDataList.value.map(item => item.fillingCount)
+
+  // ECharts配置
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      formatter: (params) => {
+        let result = `<div class="font-bold">${params[0].axisValue}</div>`
+        params.forEach(param => {
+          result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
+        })
+        return result
+      }
+    },
+    legend: {
+      data: ['总数', '已填写', '未填写', '填写中'],
+      top: 25
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: deptNames,
+      axisLabel: {
+        rotate: 45,
+        margin: 15,
+        fontSize: 10
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: (value) => Math.floor(value).toString()
+      }
+    },
+    series: [
+      {
+        name: '总数',
+        type: 'bar',
+        barGap: 0,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#188df0' },
+            { offset: 1, color: '#188df0' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: totalCounts
+      },
+      {
+        name: '已填写',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#d3a137' },
+            { offset: 1, color: '#d3a137' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: filledCounts
+      },
+      {
+        name: '未填写',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'green' },
+            { offset: 1, color: 'green' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: unfilledCounts
+      },
+      {
+        name: '填写中',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'red' },
+            { offset: 1, color: 'red' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: fillingCounts
+      }
+    ]
+  }
+
+  // 初始化图表
+  chartInstance = echarts.init(chartContainer.value)
+  chartInstance.setOption(option)
+
+  // 窗口缩放监听
+  window.addEventListener('resize', handleResize)
+  handleResize()
+}
+
+// 自适应调整
+const handleResize = () => {
+  chartInstance?.resize()
+}
+
+/** 初始化 */
+onMounted(async () => {
+  queryParams.deptId = useUserStore().getUser.deptId;
+  getStats()
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+
+onUnmounted(() => {
+  chartInstance?.dispose()
+  window.removeEventListener('resize', handleResize)
+})
+</script>
+
+<style lang="scss" scoped>
+.chart-card {
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+  padding: 16px;
+}
+
+// 新增样式,隐藏滚动条但保留功能
+::-webkit-scrollbar {
+  display: none;
+}
+
+:host {
+  overflow: hidden;
+}
+
+// 确保页面铺满屏幕并不出现滚动条
+html, body {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+// 适配图表容器高度
+.el-row {
+  max-height: calc(100vh - 32px); // 减去页面padding
+  overflow: auto;
+  -ms-overflow-style: none; // 隐藏IE滚动条
+  scrollbar-width: none; // 隐藏Firefox滚动条
+}
+</style>

+ 2 - 18
src/views/pms/maintenance/IotMaintenancePlan.vue

@@ -327,7 +327,6 @@
 
 </template>
 <script setup lang="ts">
-import { IotMaintainApi, IotMaintainVO } from '@/api/pms/maintain'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
@@ -609,8 +608,6 @@ const deviceChoose = async(selectedDevices) => {
       list.value.push(item)
     }
   })
-  // 新增完设备后 查询现有设备-bom明细中 所有设备配置的负责人姓名 逗号分隔
-  // await getDevicePersons();
 }
 
 const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
@@ -812,11 +809,7 @@ onMounted(async () => {
     const plan = await IotMaintenancePlanApi.getIotMaintenancePlan(id);
     deviceLabel.value = plan.deviceName
     formData.value = plan
-    // 查询保养责任人
-    /* const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0;
-    UserApi.getUser(personId).then((res) => {
-      formData.value.responsiblePerson = res.nickname;
-    }) */
+
     // 查询保养计划明细
     const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams);
     list.value = []
@@ -826,8 +819,7 @@ onMounted(async () => {
         // 这里可以添加必要的字段转换(如果有日期等需要格式化的字段)
         lastNaturalDate: item.lastNaturalDate
       }))
-      // 同时查询所有设备的责任人
-      // await getDevicePersons();
+
     }
   } else {
     formType.value = 'create';
@@ -837,14 +829,6 @@ onMounted(async () => {
   }
 })
 const handleDelete = async (str: string) => {
-  /* try {
-    const index = list.value.findIndex((item) => (item.id+'-'+item.bomNodeId) === str)
-    if (index !== -1) {
-      // 通过 splice 删除元素
-      list.value.splice(index, 1)
-      deviceIds.value = []
-    }
-  } catch {} */
   try {
     const [deviceIdStr, bomNodeId] = str.split('-')
     const deviceId = parseInt(deviceIdStr)

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

@@ -10,13 +10,22 @@
         label-width="68px"
         @submit.prevent
       >
-        <el-form-item  :title="t('deviceList.deviceName')" prop="deviceName">
+        <el-form-item  :label="t('deviceList.deviceName')" prop="deviceName">
           <el-input
             @keyup.enter="handleQuery"
             v-model="queryParams.deviceName"
             :placeholder="t('deviceList.nameHolder')"
             clearable
-            class="!w-240px"
+            class="!w-200px"
+          />
+        </el-form-item>
+        <el-form-item  :label="t('deviceList.deviceCode')" prop="deviceCode">
+          <el-input
+            @keyup.enter="handleQuery"
+            v-model="queryParams.deviceCode"
+            :placeholder="t('deviceList.codeHolder')"
+            clearable
+            class="!w-200px"
           />
         </el-form-item>
         <el-form-item>