浏览代码

运行记录优化

yuanchao 1 月之前
父节点
当前提交
f40c57263a

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

@@ -54,6 +54,10 @@ export const IotDeviceApi = {
     return await request.get({ url: `/rq/iot-device/page`, params })
   },
 
+  getPlanDevicePage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-opeation-fill/planDevPage`, params })
+  },
+
   // 获得设备关联责任人 分页
   responsiblePage: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/responsiblePage`, params })

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

@@ -88,6 +88,11 @@ export const IotOpeationFillApi = {
     return await request.put({ url: `/rq/iot-opeation-fill/update`, data })
   },
 
+  // 修改运行记录填报
+  updateIotOpeationFill1: async (data: IotOpeationFillVO) => {
+    return await request.put({ url: `/rq/iot-opeation-fill/update1`, data })
+  },
+
   // 删除运行记录填报
   deleteIotOpeationFill: async (id: number) => {
     return await request.delete({ url: `/rq/iot-opeation-fill/delete?id=` + id })

+ 57 - 0
src/api/pms/iotopeationfill/plan/index.ts

@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+// 巡检计划 VO
+export interface IotOperationPlanVO {
+  id: number // 主键id
+  planTitle: string // 巡检计划标题
+  planCode: string // 巡检计划编号
+  planCycle: number // 周期
+  planUnit: string // 单位
+  charge: string // 负责人
+  charges: [],
+  chargeName: string,
+  deviceIds: string // 设备
+  remark: string // 备注
+  deptId: number // 部门id
+  planDevList:[]
+}
+
+// 巡检计划 API
+export const IotOperationPlanApi = {
+  // 查询巡检计划分页
+  getIotOperationPlanPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-operation-plan/page`, params })
+  },
+
+  // 查询巡检计划详情
+  getIotOperationPlan: async (id: number) => {
+    return await request.get({ url: `/rq/iot-operation-plan/get?id=` + id })
+  },
+
+  // 新增巡检计划
+  createIotOperationPlan: async (data: IotOperationPlanVO) => {
+    return await request.post({ url: `/rq/iot-operation-plan/create`, data })
+  },
+
+  // 修改巡检计划
+  updateIotOperationPlan: async (data: IotOperationPlanVO) => {
+    return await request.put({ url: `/rq/iot-operation-plan/update`, data })
+  },
+
+  // 删除巡检计划
+  deleteIotOperationPlan: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-operation-plan/delete?id=` + id })
+  },
+
+  // 导出巡检计划 Excel
+  exportIotOperationPlan: async (params) => {
+    return await request.download({ url: `/rq/iot-operation-plan/export-excel`, params })
+  },
+  updateIotOperationStatus : (id: number, status: number) => {
+    const data = {
+      id,
+      status
+    }
+    return request.put({ url: '/rq/iot-operation-plan/update-status', data: data })
+  }
+}

+ 10 - 0
src/locales/en.ts

@@ -538,9 +538,15 @@ export default {
     duty:'RP',
     view:'View',
     fill:'Fill',
+    ignore:'Ignore',
     all:'All',
     clear:'Clear',
     orderDevice:'OrderDevice',
+    deviceCount:'DeviceCount',
+    fillCount:'FillCount',
+    unFillCount:'UnFillCount',
+    selectAll:'SelectAll',
+    deselectAll:'DeselectAll'
   },
   operationFillForm:{
     team:'Team',
@@ -1076,7 +1082,9 @@ export default {
     cycle:'Cycle',
     beginCreateTime:'firstExecuteTime',
     SelectInspectionDevice:'SelectInspectionDevice',
+    SelectOperationDevice:'SelectOperationDevice',
     InspectionRoute:'InspectionRoute',
+    OperationDevice:'OperationDevice',
     planTitle:'PlanTitle',
     planNumber:'PlanNumber',
     orderType:'OrderType',
@@ -1154,6 +1162,8 @@ export default {
     InspectOrderAdd:'InspectOrderAdd',
     InspectOrder:'InspectOrder',
     InspectPlanAdd:'InspectPlanAdd',
+    OperationPlanAdd:'OperationPlanAdd',
+    OperationPlanEdit:'OperationPlanEdit',
     InspectPlanEdit:'InspectPlanEdit',
     InspectOrderDetail:'InspectOrderDetail',
     FailureDetail:'FailureDetail',

+ 10 - 0
src/locales/zh-CN.ts

@@ -534,9 +534,15 @@ export default {
     duty:'责任人',
     view:'查看',
     fill:'填写',
+    ignore:'忽略',
     all:'全部',
     clear:'清空',
     orderDevice:'工单设备',
+    deviceCount:'应填设备数',
+    fillCount:'已填设备数',
+    unFillCount:'未填设备数',
+    selectAll:'全部选中',
+    deselectAll:'取消全选'
   },
   operationFillForm:{
     team:'所属队伍',
@@ -1071,7 +1077,9 @@ export default {
     cycle:'周期',
     beginCreateTime:'首次执行时间',
     SelectInspectionDevice:'选择巡检设备',
+    SelectOperationDevice:'选择运行设备',
     InspectionRoute:'巡检路线',
+    OperationDevice:'运行设备',
     planTitle:'计划标题',
     planNumber:'计划编号',
     orderType:'工单类型',
@@ -1148,6 +1156,8 @@ export default {
     InspectionRouteEdit:"巡检路线编辑",
     InspectOrderAdd:'巡检工单填写',
     InspectPlanAdd:'巡检计划添加',
+    OperationPlanAdd:'运行计划添加',
+    OperationPlanEdit:'运行计划编辑',
     InspectPlanEdit:'巡检计划编辑',
     InspectOrderDetail:'巡检工单详情',
     InspectOrder:'巡检工单',

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

@@ -353,6 +353,55 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
     ]
   },
+  {
+    path: '/iotOperationPlan',
+    component: Layout,
+    name: 'iotOperationPlan',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'template/detail',
+        component: () => import('@/views/pms/iotopeationfill/plan/index.vue'),
+        name: 'iotOperationPlan',
+        meta: {
+          title: t('rem.FillInInformation'),
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          keepAlive: true,
+          activeMenu: '/template/info'
+        }
+      },
+      {
+        path: 'iotopeationfill/plan/add',
+        component: () => import('@/views/pms/iotopeationfill/plan/IotOperationPlan.vue'),
+        name: 'OperationPlanAdd',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: t('rem.OperationPlanAdd'),
+          activeMenu: '/iotopeationfill/plan/add'
+        }
+      },
+      {
+        path: 'iotopeationfill/plan/add/:id(\\\\d+)',
+        component: () => import('@/views/pms/iotopeationfill/plan/IotOperationPlan.vue'),
+        name: 'OperationPlanEdit',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: t('rem.OperationPlanEdit'),
+          activeMenu: '/iotopeationfill/plan/add'
+        }
+      },
+    ]
+  },
   {
     path: '/FillOrderInfoS',
     component: Layout,

+ 141 - 12
src/views/pms/iotopeationfill/index.vue

@@ -77,36 +77,52 @@
     <!-- 列表 -->
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true"  :show-overflow-tooltip="true">
-        <el-table-column label="序号" width="60" align="center">
+        <el-table-column :label="t('common.index')" min-width="60" align="center">
           <template #default="scope">
             {{ scope.$index + 1 }}
           </template>
         </el-table-column>
-        <el-table-column :label="t('bomList.name')" align="center" prop="orderName" width="320"/>
-        <el-table-column :label="t('operationFill.duty')" align="center" prop="userName" />
-        <el-table-column :label="t('operationFill.orderDevice')" align="center" prop="fillList" />
-        <el-table-column :label="t('operationFill.status')" align="center" prop="orderStatus" >
+        <el-table-column :label="t('bomList.name')" align="center" prop="orderName" min-width="220"/>
+        <el-table-column :label="t('operationFill.duty')" align="center" prop="userName" min-width="100"/>
+        <el-table-column :label="t('operationFill.orderDevice')" align="center" prop="fillList" min-width="150"/>
+        <el-table-column :label="t('operationFill.status')" align="center" prop="orderStatus" min-width="120">
           <template #default="scope">
             <dict-tag :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS" :value="scope.row.orderStatus" />
           </template>
         </el-table-column>
-<!--        <el-table-column label="备注" align="center" prop="remark" />-->
+        <el-table-column :label="t('operationFill.deviceCount')" align="center" prop="allDev" min-width="120">
+          <template #default="scope">
+            <el-tag  type="info"> {{scope.row.allDev}}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('operationFill.fillCount')" align="center" prop="fillDev" min-width="120">
+          <template #default="scope">
+            <el-tag  type="success"> {{scope.row.fillDev}}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('operationFill.unFillCount')" align="center" prop="unFillDev" min-width="120">
+          <template #default="scope">
+            <el-tag  type="danger"> {{scope.row.unFillDev}}</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column
           :label="t('dict.createTime')"
           align="center"
           prop="createTime"
           :formatter="dateFormatter"
+          min-width="170"
         />
         <el-table-column
-          label="填写时间"
+          :label="t('dict.fillTime')"
           align="center"
           prop="updateTime"
           :formatter="dateFormatter"
+          min-width="170"
         />
-        <el-table-column :label="t('operationFill.operation')" align="center" min-width="120px">
+        <el-table-column :label="t('operationFill.operation')" align="center" min-width="120px" fixed="right">
           <template #default="scope">
 
-              <div v-if="scope.row.orderStatus !== 1">
+              <div v-if="scope.row.orderStatus == 0||scope.row.orderStatus == 2">
                 <el-button
                   link
                   type="primary"
@@ -116,6 +132,24 @@
                 >
                   {{t('operationFill.fill')}}
                 </el-button>
+                <el-button
+                  link
+                  type="warning"
+                  v-hasPermi="['rq:iot-opeation-fill:update']"
+                  v-if="scope.row.orderStatus !== 1"
+                  @click="openDialog(scope.row.id)"
+                >
+                  {{t('operationFill.ignore')}}
+                </el-button>
+              </div>
+              <div v-else-if="scope.row.orderStatus === 3">
+                <el-button
+                  link
+                  type="success"
+                  @click="openWrite(scope.row.deptId+','+scope.row.userId+','+scope.row.createTime+','+scope.row.id+','+scope.row.orderStatus)"
+                >
+                  {{t('operationFill.view')}}
+                </el-button>
               </div>
               <div v-else>
                 <el-button
@@ -143,6 +177,36 @@
           </template>
         </el-table-column>
       </el-table>
+      <el-dialog
+        v-model="dialogVisible"
+        title="忽略理由"
+        :width="600"
+        :before-close="handleClose"
+        append-to-body
+        :close-on-click-modal="false"
+      >
+        <el-form
+          ref="reasonFormRef"
+          :model="form"
+          :rules="rules"
+          label-width="60px"
+        >
+          <el-form-item label="理由" prop="reason">
+            <el-input
+              type="textarea"
+              v-model="form.reason"
+              placeholder="请输入忽略理由"
+              :rows="4"
+              resize="none"
+            />
+          </el-form-item>
+        </el-form>
+
+        <template #footer>
+          <el-button @click="handleCancel">取消</el-button>
+          <el-button type="primary" @click="handleConfirm">确定</el-button>
+        </template>
+      </el-dialog>
       <!-- 分页 -->
       <Pagination
         :total="total"
@@ -174,7 +238,7 @@ const orderStatus = query.orderStatus;
 const createTime = query.createTime;
 /** 巡检工单 列表 */
 defineOptions({ name: 'IotOpeationFill1' })
-
+const dialogVisible = ref(false);
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -194,6 +258,54 @@ const queryParams = reactive({
   orderStatus:undefined
   //userId:useUserStore().getUser.id
 })
+
+
+const form = reactive({
+  id: undefined,
+  reason: '',
+});
+
+// 表单验证规则
+const rules = {
+  reason: [
+    { required: true, message: '请输入忽略理由', trigger: 'blur' },
+    { min: 2, message: '理由长度不能少于2个字符', trigger: 'blur' }
+  ]
+};
+// 打开对话框
+const openDialog = (id:number) => {
+  dialogVisible.value = true;
+  form.id = id;
+  form.reason = '';
+};
+// 取消按钮处理
+const handleCancel = () => {
+  dialogVisible.value = false;
+  resetForm();
+};
+
+// 确定按钮处理
+const handleConfirm = async () => {
+  // 表单验证
+  try {
+    await reasonFormRef.value.validate();
+    // 验证通过,调用接口
+    await IotOpeationFillApi.updateIotOpeationFill1(form)
+    ElMessage.success('操作成功');
+    dialogVisible.value = false;
+    resetForm();
+  } catch (error) {
+    return;
+  }
+};
+// 重置表单
+const resetForm = () => {
+  reasonFormRef.value?.resetFields();
+};
+const reasonFormRef = ref(null);
+
+
+
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
@@ -307,15 +419,32 @@ onMounted(async () => {
   if(deptId != null){
     queryParams.deptId = deptId;
   }
-  if(orderStatus === 3){
+  if(orderStatus === 4){
     queryParams.orderStatus = null
   }
-  if(orderStatus != null && orderStatus != 3){
+  if(orderStatus != null && orderStatus != 4){
     queryParams.orderStatus = orderStatus;
   }
   if(createTime){
     queryParams.createTime = createTime;
   }
+  // 计算近一周时间
+  const end = new Date();
+  const start = new Date();
+  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
+
+  // 格式化日期为后端需要的格式
+  const formatDate = (date) => {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    const seconds = String(date.getSeconds()).padStart(2, '0');
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+  };
+
+  queryParams.createTime = [formatDate(start), formatDate(end)];
   getList()
 })
 </script>

+ 627 - 0
src/views/pms/iotopeationfill/plan/IotOperationPlan.vue

@@ -0,0 +1,627 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <ContentWrap>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
+        label-width="130px"
+      >
+        <div class="base-expandable-content">
+          <el-row>
+            <el-col :span="12">
+              <el-form-item :label="t('main.planName')" prop="planTitle">
+                <el-input v-model="formData.planTitle" :placeholder="t('main.nameHolder')" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item :label="t('workOrderMaterial.unit')" prop="planUnit">
+                <el-select
+                  v-model="formData.planUnit"
+                  :placeholder="t('route.unitHolder')"
+                  :disabled="formType === 'update'"
+                  clearable
+                >
+                  <el-option
+                    v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
+                    :key="dict.label"
+                    :label="dict.label"
+                    :value="dict.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item :label="t('route.cycle')" prop="planCycle">
+                <el-input-number
+                  style="width: 100%"
+                  v-model="formData.planCycle"
+                  :precision="0"
+                  :min="1"
+                  :max="99999"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item :label="t('route.beginCreateTime')" prop="beginCreateTime">
+                <el-date-picker
+                  style="width: 150%"
+                  v-model="formData.beginCreateTime"
+                  type="datetime"
+                  value-format="x"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item :label="t('form.remark')" prop="remark">
+                <el-input type="textarea" v-model="formData.remark" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </el-form>
+    </ContentWrap>
+
+    <ContentWrap>
+      <!-- 列表 -->
+      <!--      <ContentWrap>-->
+      <ContentWrap>
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item>
+            <el-button @click="openForm" type="primary"
+            ><Icon icon="ep:plus" class="mr-5px" />
+              {{ t('route.SelectOperationDevice') }}</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+      <draggable
+        v-model="list"
+        item-key="id"
+        tag="div"
+        class="sortable-container"
+        handle=".sortable-item"
+        :animation="150"
+        @start="dragStart"
+        @end="dragEnd"
+      >
+        <template #item="{ element, index }">
+          <div class="sortable-item">
+            <!-- 序号显示 -->
+            <div class="order-number">{{ index + 1 }}</div>
+
+            <!-- 拖动手柄 -->
+            <span class="drag-handle">≡</span>
+
+            <!-- 组件内容 -->
+            <div class="component-content">
+                <span style="font-weight: bold">{{ t('iotDevice.code') }}:</span
+                ><span style="font-size: 14px">{{ element.deviceCode }}</span
+            >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+              <span style="font-weight: bold">{{ t('iotDevice.name') }}:</span
+              ><span style="font-size: 14px">{{ element.deviceName }}</span>
+            </div>
+            <div>
+              <!--              <el-button type="warning" @click="deleteDraggable(index)">{{ t('form.delete') }}</el-button>-->
+              <el-button
+                style="vertical-align: middle"
+                link
+                type="danger"
+                @click="handleDelete(element.id,element.deviceId)"
+              >
+                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
+                {{ t('form.delete') }}
+              </el-button>
+            </div>
+          </div>
+        </template>
+      </draggable>
+    </ContentWrap>
+
+    <ContentWrap>
+      <el-form>
+        <el-form-item style="float: right">
+          <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('iotMaintain.save') }}</el-button>
+          <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <InspectItemList
+      ref="inspectItemFormRef"
+      :classify="formData.deviceClassify"
+      :deviceId="formData.deviceId"
+      @choose="inspectItemChoose"
+    />
+    <PlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
+    <RouteInspectItemDrawer
+      ref="showDrawer"
+      :model-value="drawerVisible"
+      @update:model-value="(val) => (drawerVisible = val)"
+    />
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { ref } from 'vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import InspectItemList from '@/views/pms/inspect/route/InspectItemList.vue'
+import PlanDeviceList from '@/views/pms/iotopeationfill/plan/PlanDeviceList.vue'
+import { IotInspectRouteVO } from '@/api/pms/inspect/route'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
+import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
+import draggable from 'vuedraggable'
+import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
+import {defaultProps,handleTree} from "@/utils/tree";
+import * as ProductClassifyApi from "@/api/pms/productclassify";
+import {IotInspectItemVO} from "@/api/pms/inspect/item";
+import {IotOperationPlanApi, IotOperationPlanVO} from "@/api/pms/iotopeationfill/plan";
+
+/** 维修工单 表单 */
+defineOptions({ name: 'OperationPlanAdd' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const drawerVisible = ref<boolean>(false)
+const showDrawer = ref()
+const list = ref<IotInspectRouteVO[]>([]) // 列表的数据
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const productClassifyList = ref<Tree[]>([]) // 树形结构
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const formData = ref({
+  id: undefined,
+  planTitle: undefined,
+  planCode: undefined,
+  planCycle: undefined,
+  beginCreateTime: undefined,
+  planUnit: undefined,
+  charge: undefined,
+  charges: [],
+  deviceIds: undefined,
+  remark: undefined,
+  deptId: undefined,
+  planDevList:[]
+})
+const formRules = reactive({
+  planTitle: [{ required: true, message: '运行计划标题不能为空', trigger: 'blur' }],
+  planCode: [{ required: true, message: '运行计划编号不能为空', trigger: 'blur' }],
+  planCycle: [{ required: true, message: '周期不能为空', trigger: 'blur' }],
+  beginCreateTime: [{ required: true, message: '首次执行时间不能为空', trigger: 'blur' }],
+  planUnit: [{ required: true, message: '单位不能为空', trigger: 'blur' }]
+  // charges: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
+})
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceCode: undefined,
+  deviceName: undefined,
+  brand: undefined,
+  brandName: undefined,
+  model: undefined,
+  deptId: undefined,
+  deviceStatus: undefined,
+  assetProperty: undefined,
+  picUrl: undefined,
+  remark: undefined,
+  manufacturerId: undefined,
+  supplierId: undefined,
+  manDate: [],
+  nameplate: undefined,
+  expires: undefined,
+  plPrice: undefined,
+  plDate: [],
+  plYear: undefined,
+  plStartDate: [],
+  plMonthed: undefined,
+  plAmounted: undefined,
+  remainAmount: undefined,
+  infoId: undefined,
+  infoType: undefined,
+  infoName: undefined,
+  infoRemark: undefined,
+  infoUrl: undefined,
+  templateJson: undefined,
+  creator: undefined,
+  sortingFields: [],
+  assetClass: undefined,
+})
+const queryFormRef = ref() // 搜索的表单
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+// 添加全选相关状态
+const isAllSelected = ref(false) // 是否全选所有数据
+const totalSelectedCount = ref(0) // 总选中数量
+const deselectedRows = ref([]) // 全选状态下取消选择的行
+const allDataCache = ref([])
+
+// 修改表格复选框绑定
+
+const selectedRows = ref<IotDeviceVO[]>([]) // 多选数据(存储所有选中行的数组)
+// 修改全选方法,同步更新selectedRows
+const selectAll = async () => {
+  try {
+    // 显示加载状态
+    loading.value = true;
+    isAllSelected.value = true;
+    deselectedRows.value = [];
+    selectedRows.value = []; // 清空现有选中项
+    allDataCache.value = []; // 清空缓存
+
+    const paramsCopy = { ...queryParams };
+    const originalPageNo = paramsCopy.pageNo;
+    const originalPageSize = paramsCopy.pageSize;
+
+    paramsCopy.pageNo = 1;
+    paramsCopy.pageSize = 100; // 按后台限制的最大页数获取
+
+    // 获取第一页数据
+    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy);
+    totalSelectedCount.value = firstPageData.total;
+    allDataCache.value.push(...firstPageData.list);
+
+    // 分批获取剩余数据
+    if (firstPageData.total > 100) {
+      const totalPages = Math.ceil(firstPageData.total / 100);
+      const fetchPromises = [];
+
+      for (let page = 2; page <= totalPages; page++) {
+        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }));
+      }
+
+      const remainingPages = await Promise.all(fetchPromises);
+      remainingPages.forEach(pageData => {
+        allDataCache.value.push(...pageData.list);
+      });
+    }
+
+    // 全选时,将所有数据放入selectedRows
+    selectedRows.value = [...allDataCache.value];
+
+    // 恢复原始分页参数
+    queryParams.pageNo = originalPageNo;
+    queryParams.pageSize = originalPageSize;
+
+    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }));
+  } catch (error) {
+    console.error('全选失败', error);
+    message.error(t('operationFill.selectAllFailed'));
+    isAllSelected.value = false;
+    selectedRows.value = [];
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 修改取消全选方法,同步清空selectedRows
+const deselectAll = () => {
+  isAllSelected.value = false;
+  deselectedRows.value = [];
+  selectedRows.value = []; // 清空所有选中项
+  allDataCache.value = []; // 清空缓存
+  totalSelectedCount.value = 0;
+  message.info(t('operationFill.allDeselected'));
+};
+
+// 修改单选逻辑,确保selectedRows正确更新
+const selectRow = (row) => {
+  if (isAllSelected.value) {
+    // 全选状态下的单选操作
+    const deselectedIndex = deselectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    const selectedIndex = selectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    if (deselectedIndex > -1) {
+      // 从取消列表中移除,添加到选中列表
+      deselectedRows.value.splice(deselectedIndex, 1);
+      if (selectedIndex === -1) {
+        selectedRows.value.push({ ...row });
+      }
+    } else {
+      // 从选中列表中移除,添加到取消列表
+      if (selectedIndex > -1) {
+        selectedRows.value.splice(selectedIndex, 1);
+      }
+      deselectedRows.value.push({ ...row });
+    }
+  } else {
+    // 非全选状态下的单选操作
+    const index = selectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    if (index > -1) {
+      // 取消选择
+      selectedRows.value.splice(index, 1);
+    } else {
+      // 选中
+      selectedRows.value.push({ ...row });
+    }
+  }
+};
+
+
+
+// 生成行唯一标识
+const rowKey = (row: any) => {
+  return `${row.id}`; // 确保行更新时重新渲染
+};
+
+// 修改移除标签方法,确保selectedRows同步更新
+const removeTag = (route) => {
+  // 从选中列表中移除
+  const selectedIndex = selectedRows.value.findIndex(
+    item => item.id === route.id && item.deviceCode === route.deviceCode
+  );
+
+  if (selectedIndex > -1) {
+    selectedRows.value.splice(selectedIndex, 1);
+  }
+
+  // 全选状态下还要添加到取消列表
+  if (isAllSelected.value) {
+    const deselectedIndex = deselectedRows.value.findIndex(
+      item => item.id === route.id && item.deviceCode === route.deviceCode
+    );
+
+    if (deselectedIndex === -1) {
+      deselectedRows.value.push({ ...route });
+    }
+  }
+
+  // 刷新表格
+  list.value = [...list.value];
+};
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotDeviceApi.getPlanDevicePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+// 拖动状态管理
+const items = ref([])
+const deviceChoose = (rows) => {
+  debugger
+  rows.forEach((it) => {
+    const ifExist = list.value.find((item) => item.id === it.id&&item.deviceId===it.deviceId)
+    if (!ifExist) {
+      list.value.push(it)
+      console.log(JSON.stringify(it))
+    }
+  })
+  // list.value = rows
+}
+const dragStart = () => {
+  document.body.style.cursor = 'grabbing'
+}
+const dragEnd = (event) => {
+  document.body.style.cursor = ''
+  console.log(
+    '新顺序:',
+    items.value.map((c) => c.id)
+  )
+  console.log('拖拽后的新位置:', event.newIndex + 1)
+}
+const viewRoute = (itemJson) => {
+  drawerVisible.value = true
+  showDrawer.value.openDrawer(JSON.parse(itemJson))
+}
+const formRef = ref() // 表单 Ref
+const inspectItemChoose = (rows) => {
+  items.value = []
+  items.value = rows
+}
+const deviceFormRef = ref()
+const openForm = () => {
+  deviceFormRef.value.open()
+}
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'iotOperationPlan', params: {} })
+}
+const itemsWithIndex = computed(() => {
+  return list.value.map((item, index) => ({
+    ...item,
+    index: index + 1 // 序号从1开始
+  }))
+})
+const { wsCache } = useCache()
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    formData.value.planDevList = list;
+    const user = wsCache.get(CACHE_KEY.USER)
+    const data = formData.value as unknown as IotOperationPlanVO
+    if (formType.value === 'create') {
+      //校验是否是已有计划
+      const newitems = itemsWithIndex
+      formData.value.deviceIds = JSON.stringify(newitems.value)
+      if (!formData.value.deptId) {formData.value.deptId = user.user.deptId}
+      await IotOperationPlanApi.createIotOperationPlan(data).then((res) => {
+      })
+      message.success(t('common.createSuccess'))
+      close()
+    } else {
+      const newitems = itemsWithIndex
+      formData.value.deviceIds = JSON.stringify(newitems.value)
+      await IotOperationPlanApi.updateIotOperationPlan(data)
+      message.success(t('common.updateSuccess'))
+      close()
+    }
+    //创建定时任务
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+onMounted(async () => {
+
+  const route = useRoute()
+  const deptId = useUserStore().getUser.deptId
+  formData.value.deptId = Number(route.query.deptId)
+  if (route.query.deptName) {
+    formData.value.planTitle = `${route.query.deptName} - 运行计划`
+  }
+  deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+  if (id) {
+    formType.value = 'update'
+    const iotInspectPlan = await IotOperationPlanApi.getIotOperationPlan(id)
+    formData.value = iotInspectPlan
+    list.value = JSON.parse(iotInspectPlan.deviceIds)
+  } else {
+    formType.value = 'create'
+  }
+  productClassifyList.value = handleTree(
+    await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
+  )
+})
+const handleDelete = async (id: number, deviceId: number) => {
+  try {
+    debugger
+    const index = list.value.findIndex((item) => item.id === id&&item.deviceId===deviceId)
+    if (index !== -1) {
+      // 通过 splice 删除元素
+      list.value.splice(index, 1)
+    }
+  } catch {}
+}
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+/* 横向布局容器 */
+
+
+/* 拖拽手柄样式 */
+.drag-handle {
+  opacity: 0.5;
+  cursor: move;
+  transition: opacity 0.3s;
+}
+.drag-handle:hover {
+  opacity: 1;
+}
+
+/* 拖拽时的悬停效果 */
+.horizontal-item:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+/* 滚动条样式 */
+.horizontal-list::-webkit-scrollbar {
+  height: 8px;
+}
+.horizontal-list::-webkit-scrollbar-thumb {
+  background: #888;
+  border-radius: 4px;
+}
+
+.sortable-container {
+  cursor: move;
+  display: flex;
+  flex-direction: column;
+  gap: 9px;
+  //max-height: 80vh;
+  overflow-y: auto;
+  padding: 7px;
+}
+
+.sortable-item {
+  display: flex;
+  align-items: center;
+  padding: 8px;
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  transition:
+    transform 0.3s,
+    box-shadow 0.3s;
+  user-select: none;
+  height: 50%;
+}
+
+.sortable-item:hover {
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.order-number {
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #409eff;
+  color: white;
+  border-radius: 50%;
+  margin-right: 7px;
+  font-weight: bold;
+}
+
+.drag-handle {
+  padding: 0 12px;
+  opacity: 0.4;
+  transition: opacity 0.3s;
+}
+
+.drag-handle:hover {
+  opacity: 1;
+}
+
+.component-content {
+  flex: 1;
+  min-width: 0;
+}
+
+/* 优化滚动条 */
+.sortable-container::-webkit-scrollbar {
+  width: 8px;
+}
+.sortable-container::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 4px;
+}
+</style>

+ 132 - 0
src/views/pms/iotopeationfill/plan/IotOperationPlanForm.vue

@@ -0,0 +1,132 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="巡检计划标题" prop="planTitle">
+        <el-input v-model="formData.planTitle" placeholder="请输入巡检计划标题" />
+      </el-form-item>
+      <el-form-item label="巡检计划编号" prop="planCode">
+        <el-input v-model="formData.planCode" placeholder="请输入巡检计划编号" />
+      </el-form-item>
+      <el-form-item label="周期" prop="planCycle">
+        <el-input v-model="formData.planCycle" placeholder="请输入周期" />
+      </el-form-item>
+      <el-form-item label="单位" prop="planUnit">
+        <el-input v-model="formData.planUnit" placeholder="请输入单位" />
+      </el-form-item>
+      <el-form-item label="负责人" prop="charge">
+        <el-input v-model="formData.charge" placeholder="请输入负责人" />
+      </el-form-item>
+      <el-form-item label="设备" prop="deviceIds">
+        <el-input v-model="formData.deviceIds" placeholder="请输入设备" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="部门id" prop="deptId">
+        <el-input v-model="formData.deptId" placeholder="请输入部门id" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
+
+/** 巡检计划 表单 */
+defineOptions({ name: 'IotOperationPlanForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  planTitle: undefined,
+  planCode: undefined,
+  planCycle: undefined,
+  planUnit: undefined,
+  charge: undefined,
+  deviceIds: undefined,
+  remark: undefined,
+  deptId: undefined,
+})
+const formRules = reactive({
+  planTitle: [{ required: true, message: '巡检计划标题不能为空', trigger: 'blur' }],
+  planCode: [{ required: true, message: '巡检计划编号不能为空', trigger: 'blur' }],
+  planCycle: [{ required: true, message: '周期不能为空', trigger: 'blur' }],
+  planUnit: [{ required: true, message: '单位不能为空', trigger: 'blur' }],
+  charge: [{ required: true, message: '负责人不能为空', trigger: 'blur' }],
+  deviceIds: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotInspectPlanApi.getIotInspectPlan(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IotInspectPlanVO
+    if (formType.value === 'create') {
+      await IotInspectPlanApi.createIotInspectPlan(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotInspectPlanApi.updateIotInspectPlan(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    planTitle: undefined,
+    planCode: undefined,
+    planCycle: undefined,
+    planUnit: undefined,
+    charge: undefined,
+    deviceIds: undefined,
+    remark: undefined,
+    deptId: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 436 - 0
src/views/pms/iotopeationfill/plan/PlanDeviceList.vue

@@ -0,0 +1,436 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="t('route.OperationDevice')" style="width: 1500px; min-height: 600px">
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item :label="t('monitor.deviceName')" prop="deviceName" style="margin-left: 25px">
+          <el-input
+            v-model="queryParams.deviceName"
+            :placeholder="t('iotDevice.nameHolder')"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-220px"
+          />
+        </el-form-item>
+        <el-form-item  :label="t('iotDevice.code')" prop="deviceCode">
+          <el-input
+            v-model="queryParams.deviceCode"
+            :placeholder="t('iotDevice.codeHolder')"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-220px"
+          />
+        </el-form-item>
+        <el-form-item :label="t('iotDevice.dept')" prop="deptId">
+          <el-tree-select
+            v-model="queryParams.deptId"
+            :data="deptList"
+            :props="defaultProps"
+            check-strictly
+            node-key="id"
+            filterable
+            placeholder="请选择部门"
+            class="!w-220px"
+          />
+          <!--              <el-tree-select-->
+          <!--                v-model="formData.deptId"-->
+          <!--                :data="deptList"-->
+          <!--                :props="defaultProps"-->
+          <!--                check-strictly-->
+          <!--                node-key="id"-->
+          <!--                placeholder="请选择归属部门"-->
+          <!--              />-->
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('chooseMaintain.search') }}</el-button>
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('chooseMaintain.reset')  }}</el-button>
+          <el-button @click="handleConfirm" type="danger"><Icon icon="ep:check" class="mr-5px" />  {{ t('chooseMaintain.confirm')  }}</el-button>
+          <el-button
+            type="primary"
+            size="small"
+            @click="selectAll"
+            :disabled="loading || list.length === 0 || isAllSelected"
+          >
+            {{ t('operationFill.selectAll') }}
+          </el-button>
+          <el-button
+            type="default"
+            size="small"
+            @click="deselectAll"
+            :disabled="loading || (!isAllSelected && selectedRows.length === 0)"
+            style="margin-left: 8px;"
+          >
+            {{ t('operationFill.deselectAll') }}
+          </el-button>
+<!--          <el-button @click="toggleAll" type="primary"-->
+<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.selectAll') }}</el-button-->
+<!--          >-->
+<!--          <el-button @click="invertSelection" type="info"-->
+<!--            ><Icon icon="ep:refresh" class="mr-5px" />{{ t('chooseMaintain.cancelAllSelection') }}</el-button-->
+<!--          >-->
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <!-- 新增:已选物料标签区域 -->
+    <ContentWrap v-if="selectedRows.length > 0"
+                 style="margin: -10px 0px 10px; padding: 10px 15px; background: #f8fafc; border-radius: 4px; border: 1px solid #ebeef5;">
+      <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
+        <div style="font-weight: bold; color: #606266; margin-right: 10px;">已选路线设备:</div>
+        <el-tag
+          v-for="item in selectedRows"
+          :key="item.id"
+          @close="removeTag(item)"
+          style="margin-bottom: 5px; position: relative; padding-right: 25px;"
+        >
+          {{ item.deviceCode }}
+          <!-- 自定义关闭按钮 -->
+          <span class="close-icon" @click.stop="removeTag(item)">
+            <Icon icon="ep:close" style="font-size: 12px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);"/>
+          </span>
+        </el-tag>
+      </div>
+    </ContentWrap>
+    <ContentWrap>
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :row-key="rowKey"
+        :show-overflow-tooltip="true"
+        table-layout="auto"
+        @row-click="handleRowClick"
+        class="full-width-table" >
+        <el-table-column width="80" :label="t('workOrderMaterial.select')">
+          <template #default="{ row }">
+            <el-checkbox
+              :model-value="selectedRows.some(item => item.id === row.id&&item.deviceCode===row.deviceCode)"
+              @click.stop="selectRow(row)"
+              class="no-label-radio"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
+        <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" />
+        <el-table-column :label="t('iotDevice.status')" align="center" prop="deviceStatus" >
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" />
+        <el-table-column :label="t('operationFill.operation')" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="viewRoute(scope.row.itemJson)">
+              {{ t('operationFill.view') }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+      <RouteInspectItemDrawer
+        ref="showDrawer"
+        :model-value="drawerVisible"
+        @update:model-value="(val) => (drawerVisible = val)"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import {IotDeviceApi, IotDeviceVO} from '@/api/pms/device'
+import { dateFormatter } from '@/utils/formatTime'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { IotInspectRouteApi } from '@/api/pms/inspect/route'
+import { IotInspectItemVO } from '@/api/pms/inspect/item'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import RouteInspectItemDrawer from "@/views/pms/inspect/plan/RouteInspectItemDrawer.vue";
+import {ref} from "vue";
+import {defaultProps, handleTree} from "@/utils/tree";
+import * as DeptApi from "@/api/system/dept";
+const { t } = useI18n() // 国际化
+const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const queryFormRef = ref() // 搜索的表单
+const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const deptList = ref<Tree[]>([]) // 树形结构
+const drawerVisible = ref<boolean>(false)
+const showDrawer = ref()
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  label: '',
+  status: undefined,
+  deptId: undefined,
+  assetClass: undefined,
+  deviceName: undefined,
+  deviceCode: undefined,
+  routeName: undefined,
+})
+const message = useMessage() // 消息弹窗
+// 添加全选相关状态
+const isAllSelected = ref(false) // 是否全选所有数据
+const totalSelectedCount = ref(0) // 总选中数量
+const deselectedRows = ref([]) // 全选状态下取消选择的行
+const allDataCache = ref([])
+// 生成行唯一标识
+const rowKey = (row: any) => {
+  return `${row.id}`; // 确保行更新时重新渲染
+};
+const selectAll = async () => {
+  try {
+    // 显示加载状态
+    loading.value = true;
+    isAllSelected.value = true;
+    deselectedRows.value = [];
+    selectedRows.value = []; // 清空现有选中项
+    allDataCache.value = []; // 清空缓存
+
+    const paramsCopy = { ...queryParams };
+    const originalPageNo = paramsCopy.pageNo;
+    const originalPageSize = paramsCopy.pageSize;
+
+    paramsCopy.pageNo = 1;
+    paramsCopy.pageSize = 100; // 按后台限制的最大页数获取
+
+    // 获取第一页数据
+    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy);
+    totalSelectedCount.value = firstPageData.total;
+    allDataCache.value.push(...firstPageData.list);
+
+    // 分批获取剩余数据
+    if (firstPageData.total > 100) {
+      const totalPages = Math.ceil(firstPageData.total / 100);
+      const fetchPromises = [];
+
+      for (let page = 2; page <= totalPages; page++) {
+        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }));
+      }
+
+      const remainingPages = await Promise.all(fetchPromises);
+      remainingPages.forEach(pageData => {
+        allDataCache.value.push(...pageData.list);
+      });
+    }
+
+    // 全选时,将所有数据放入selectedRows
+    selectedRows.value = [...allDataCache.value];
+
+    // 恢复原始分页参数
+    queryParams.pageNo = originalPageNo;
+    queryParams.pageSize = originalPageSize;
+
+    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }));
+  } catch (error) {
+    console.error('全选失败', error);
+    message.error(t('operationFill.selectAllFailed'));
+    isAllSelected.value = false;
+    selectedRows.value = [];
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 修改取消全选方法,同步清空selectedRows
+const deselectAll = () => {
+  isAllSelected.value = false;
+  deselectedRows.value = [];
+  selectedRows.value = []; // 清空所有选中项
+  allDataCache.value = []; // 清空缓存
+  totalSelectedCount.value = 0;
+  message.info(t('operationFill.allDeselected'));
+};
+
+// 修改单选逻辑,确保selectedRows正确更新
+const selectRow = (row) => {
+  if (isAllSelected.value) {
+    // 全选状态下的单选操作
+    const deselectedIndex = deselectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    const selectedIndex = selectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    if (deselectedIndex > -1) {
+      // 从取消列表中移除,添加到选中列表
+      deselectedRows.value.splice(deselectedIndex, 1);
+      if (selectedIndex === -1) {
+        selectedRows.value.push({ ...row });
+      }
+    } else {
+      // 从选中列表中移除,添加到取消列表
+      if (selectedIndex > -1) {
+        selectedRows.value.splice(selectedIndex, 1);
+      }
+      deselectedRows.value.push({ ...row });
+    }
+  } else {
+    // 非全选状态下的单选操作
+    const index = selectedRows.value.findIndex(
+      item => item.id === row.id && item.deviceCode === row.deviceCode
+    );
+
+    if (index > -1) {
+      // 取消选择
+      selectedRows.value.splice(index, 1);
+    } else {
+      // 选中
+      selectedRows.value.push({ ...row });
+    }
+  }
+};
+
+
+
+
+
+// 修改移除标签方法,确保selectedRows同步更新
+const removeTag = (route) => {
+  // 从选中列表中移除
+  const selectedIndex = selectedRows.value.findIndex(
+    item => item.id === route.id && item.deviceCode === route.deviceCode
+  );
+
+  if (selectedIndex > -1) {
+    selectedRows.value.splice(selectedIndex, 1);
+  }
+
+  // 全选状态下还要添加到取消列表
+  if (isAllSelected.value) {
+    const deselectedIndex = deselectedRows.value.findIndex(
+      item => item.id === route.id && item.deviceCode === route.deviceCode
+    );
+
+    if (deselectedIndex === -1) {
+      deselectedRows.value.push({ ...route });
+    }
+  }
+
+  // 刷新表格
+  list.value = [...list.value];
+};
+const handleConfirm = () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请至少选择一个')
+    return
+  }
+
+  emit(
+    'choose',
+    selectedRows.value.map((row) => ({
+      ...row,
+      // 确保返回必要字段
+      code: row.item,
+      name: row.standard
+    }))
+  )
+  dialogVisible.value = false
+  handleClose()
+}
+
+// 关闭时清空选择
+const handleClose = () => {
+  tableRef.value?.clearSelection()
+  selectedRows.value = []
+  emit('close')
+}
+
+
+const viewRoute = (itemJson) => {
+  drawerVisible.value = true
+  showDrawer.value.openDrawer(JSON.parse(itemJson))
+}
+const toggleAll = () => {
+  list.value.forEach((row) => {
+    tableRef.value.toggleRowSelection(row, true)
+  })
+}
+
+const handleSelectionChange = (val) => {
+  selectedRows.value = val
+}
+const invertSelection = () => {
+  selectedRows.value.forEach((row) => {
+    tableRef.value.toggleRowSelection(row, false)
+    selectedRows.value.splice(row)
+  })
+}
+const selectedRows = ref<IotInspectItemVO[]>([]) // 多选数据(存储所有选中行的数组)
+const tableRef = ref()
+
+// 多选 切换行选中状态
+const toggleRow = (row) => {
+  const includes = selectedRows.value.includes(row)
+  if (includes) {
+    selectedRows.value.splice(row)
+  } else {
+    selectedRows.value.push(row) // 选中
+  }
+  tableRef.value.toggleRowSelection(row, !includes)
+}
+
+// 点击整行选中
+const handleRowClick = (row) => {
+  toggleRow(row)
+}
+
+const open = async (classify) => {
+  dialogVisible.value = true
+  queryParams.assetClass = classify
+  queryParams.deviceName = undefined
+  queryParams.deviceCode = undefined
+  await getList()
+  selectedRows.value = []
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+}
+defineExpose({ open })
+const { wsCache } = useCache()
+const getList = async () => {
+  loading.value = true
+  list.value = []
+  try {
+    const data = await IotDeviceApi.getPlanDevicePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const choose = (row: DictDataVO) => {
+  emit('choose', row)
+  dialogVisible.value = false
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+</script>
+<style lang="scss">
+.no-label-radio .el-radio__label {
+  display: none;
+}
+.no-label-radio .el-radio__inner {
+  margin-right: 0;
+}
+</style>

+ 341 - 0
src/views/pms/iotopeationfill/plan/index.vue

@@ -0,0 +1,341 @@
+<template>
+  <el-row :gutter="20">
+    <el-col :class="{'leftcontent': true, 'collapsed': isLeftContentCollapsed}" :span="isLeftContentCollapsed ? 0 : 4" :xs="24">
+      <ContentWrap class="h-1/1">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+
+    <el-col class="rightcontent" :span="isLeftContentCollapsed ? 24 : 20" :xs="24" style="position: relative;height: 100vh;">
+        <div
+            class="toggle-button"
+            :style="{ left: isLeftContentCollapsed ? '0px' : '-13px' }"
+            @click="toggleLeftContent"
+            @mouseover="handleMouseOver"
+            @mouseout="handleMouseOut"
+            :title="hoverText"
+          >
+            <span style="font-size: 5px;" :class="{'triangle': true, 'rotated': isLeftContentCollapsed}"></span>
+        </div>
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item :label="t('main.planName')" prop="planTitle" style="margin-left: 25px">
+            <el-input
+              v-model="queryParams.planTitle"
+              :placeholder="t('main.nameHolder')"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item :label="t('main.planCode')" prop="planCode">
+            <el-input
+              v-model="queryParams.planCode"
+              :placeholder="t('main.codeHolder')"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              :start-placeholder="t('info.start')"
+              :end-placeholder="t('info.end')"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-220px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />  {{ t('operationFill.reset') }}</el-button>
+            <el-button
+              type="primary"
+              plain
+              @click="openForm('create')"
+              v-hasPermi="['rq:iot-inspect-plan:create']"
+            >
+              <Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
+            </el-button>
+            <el-button
+              type="success"
+              plain
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['rq:iot-inspect-plan:export']"
+            >
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <!-- 列表 -->
+      <ContentWrap>
+        <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('route.planTitle')" align="center" prop="planTitle" min-width="240" />
+<!--          <el-table-column :label="t('route.planNumber')" align="center" prop="planCode" min-width="200" />-->
+          <el-table-column :label="t('route.cycle')" align="center" prop="planCycle"  min-width="80"/>
+          <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="planUnit" min-width="80">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="scope.row.planUnit" />
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('bomList.status')" key="status" min-width="80">
+            <template #default="scope">
+              <el-switch
+                v-model="scope.row.status"
+                :active-value="0"
+                :inactive-value="1"
+                @change="handleStatusChange(scope.row)"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('route.beginCreateTime')"
+            align="center"
+            prop="beginCreateTime"
+            :formatter="dateFormatter"
+            min-width="180px"
+          />
+          <el-table-column
+            :label="t('route.lastCreateTime')"
+            align="center"
+            prop="lastCreateTime"
+            :formatter="dateFormatter"
+            min-width="180px"
+          />
+          <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" min-width="130"/>
+          <el-table-column :label="t('inspect.createName')" align="center" prop="createName" min-width="130"/>
+          <el-table-column :label="t('operationFill.operation')" align="center" min-width="180" fixed="right">
+            <template #default="scope">
+              <el-button
+                link
+                type="primary"
+                @click="openForm('update', scope.row.id)"
+                v-hasPermi="['rq:iot-operation-plan:update']"
+              >
+                {{ t('fault.edit') }}
+              </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="handleDelete(scope.row.id)"
+                v-hasPermi="['rq:iot-operation-plan:delete']"
+              >
+                {{ t('fault.del') }}
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+  <!-- 表单弹窗:添加/修改 -->
+  <IotOperationPlanForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { dateFormatter } from '@/utils/formatTime';
+import download from '@/utils/download';
+import { IotOperationPlanApi, IotOperationPlanVO } from '@/api/pms/iotopeationfill/plan';
+import IotOperationPlanForm from './IotOperationPlanForm.vue';
+import { CommonStatusEnum } from '@/utils/constants';
+import { DICT_TYPE } from "@/utils/dict";
+import DeptTree from "@/views/system/user/DeptTree.vue";
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+defineOptions({ name: 'iotOperationPlan' })
+const message = useMessage() // 消息弹窗
+
+const { push } = useRouter();
+const { t } = useI18n();
+
+const loading = ref(true);
+const list = ref<IotOperationPlanVO[]>([]);
+const total = ref(0);
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  planTitle: undefined,
+  planCode: undefined,
+  planCycle: undefined,
+  planUnit: undefined,
+  charge: undefined,
+  deviceIds: undefined,
+  remark: undefined,
+  createTime: [],
+  deptId: undefined
+});
+const queryFormRef = ref();
+const exportLoading = ref(false);
+
+const isLeftContentCollapsed = ref(false);
+const hoverText = ref('');
+const selectedDept = ref<{ id: number; name: string }>()
+
+const handleDeptNodeClick = async (row) => {
+  selectedDept.value = { id: row.id, name: row.name }
+  queryParams.deptId = row.id;
+  await getList();
+};
+
+const handleStatusChange = async (row: IotOperationPlanVO) => {
+  try {
+    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用';
+    await message.confirm('确认要"' + text + '""' + row.planTitle + '"巡检计划吗?');
+    await IotOperationPlanApi.updateIotOperationStatus(row.id, row.status);
+    await getList();
+  } catch {
+    row.status = row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE;
+  }
+};
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const data = await IotOperationPlanApi.getIotOperationPlanPage(queryParams);
+    list.value = data.list;
+    total.value = data.total;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleQuery = () => {
+  queryParams.pageNo = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  queryFormRef.value.resetFields();
+  selectedDept.value = undefined
+  handleQuery();
+};
+
+const openForm = (type: string, id?: number) => {
+  if (typeof id === 'number') {
+    push({ name: 'OperationPlanEdit', params: { id } });
+    return;
+  }
+  push({
+    name: 'OperationPlanAdd',
+    query: {
+      deptId: selectedDept.value?.id,
+      deptName: selectedDept.value?.name
+    }
+  })
+};
+
+const handleDelete = async (id: number) => {
+  try {
+    await message.delConfirm();
+    await IotOperationPlanApi.deleteIotOperationPlan(id);
+    message.success(t('common.delSuccess'));
+    await getList();
+  } catch {}
+};
+
+const handleExport = async () => {
+  try {
+    await message.exportConfirm();
+    exportLoading.value = true;
+    const data = await IotOperationPlanApi.exportIotOperationPlan(queryParams);
+    download.excel(data, '巡检计划.xls');
+  } catch {
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
+const toggleLeftContent = () => {
+  isLeftContentCollapsed.value = !isLeftContentCollapsed.value;
+};
+
+const handleMouseOver = () => {
+  hoverText.value = isLeftContentCollapsed.value ? '展开' : '收起';
+};
+
+const handleMouseOut = () => {
+  hoverText.value = '';
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.leftcontent {
+  transition: width 0.3s ease;
+  position: relative;
+}
+
+.leftcontent.collapsed {
+  width: 0;
+  overflow: hidden;
+}
+
+.rightcontent {
+  position: relative;
+}
+
+.toggle-button {
+  position: absolute;
+  top: 44%;
+  transform: translate(-65%,-50%);
+  width: 12px;
+  height: 40px;
+  background-color: #f0f0f0;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1;
+  clip-path: polygon(0 0, 100% 18%, 100% 85%, 0 100%);
+  border-radius: 8px;
+}
+
+/* 添加鼠标悬停样式 */
+.toggle-button:hover {
+  background-color: #afafaf;
+}
+
+
+.triangle {
+  width: 0;
+  height: 0;
+  border-top: 4px solid transparent;
+  border-bottom: 4px solid transparent;
+  transition: transform 0.4s ease;
+  border-right: 5px solid gray; /* 修改为右边框显示颜色 */
+  border-left: none; /* 移除左边框 */
+}
+
+.triangle.rotated {
+  transform: rotate(180deg);
+}
+</style>

+ 50 - 6
src/views/pms/iotopeationfill/statistics.vue

@@ -67,30 +67,36 @@
           </div>
         </template>
         <el-row class="h-[220px]">
-          <el-col :span="6" class="flex flex-col items-center" @click="openFill(queryParams.deptId,3,queryParams.createTime)">
+          <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,4,queryParams.createTime)">
             <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" @click="openFill(queryParams.deptId,1,queryParams.createTime)">
+          <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,1,queryParams.createTime)">
             <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" @click="openFill(queryParams.deptId,0,queryParams.createTime)">
+          <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,0,queryParams.createTime)">
             <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" @click="openFill(queryParams.deptId,2,queryParams.createTime)">
+          <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,2,queryParams.createTime)">
             <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-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,3,queryParams.createTime)">
+            <div ref="ignoreChartRef" 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>
@@ -120,6 +126,12 @@
               <span class="text-sm text-gray-600">未填写</span>
             </div>
           </el-col>
+          <el-col :span="6" class="flex flex-col items-center" @click="openForm(queryParams.deptId,3,queryParams.createTime)">
+            <div ref="ignoreChartRef1" 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>
@@ -255,12 +267,12 @@ const reportingChartRef = ref() // 在线设备统计的图表
 const dealFinishedChartRef = ref() // 离线设备统计的图表
 const transOrderChartRef = ref() // 待激活设备统计的图表
 const writeChartRef = ref() // 待填写图表
-
+const ignoreChartRef = ref() // 上下行消息量统计的图表
 //运行记录设备统计
 const reportingChartRef1 = ref() // 在线设备统计的图表
 const dealFinishedChartRef1 = ref() // 离线设备统计的图表
 const transOrderChartRef1 = ref() // 待激活设备统计的图表
-
+const ignoreChartRef1 = ref() // 上下行消息量统计的图表
 
 // 基础统计数据
 const statsData = ref<IotStatisticsSummaryRespVO>({
@@ -401,6 +413,19 @@ const initCharts = () => {
     firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
     '#05b'
   )
+
+  initGaugeChart(
+    ignoreChartRef.value,
+    firstStatus.ignoreCount === undefined ? 0 : firstStatus.ignoreCount,
+    'purple'
+  )
+
+  initGaugeChart(
+    ignoreChartRef1.value,
+    firstStatus1.ignoreCount === undefined ? 0 : firstStatus1.ignoreCount,
+    'purple'
+  )
+
   initGaugeChart(
     dealFinishedChartRef1.value,
     firstStatus1.filledCount === undefined ? 0 : firstStatus1.filledCount,
@@ -617,6 +642,25 @@ const removeBeforeEach = router.beforeEach((to, from, next) => {
 /** 初始化 */
 onMounted(async () => {
   queryParams.deptId = useUserStore().getUser.deptId;
+
+  // 计算近一周时间
+  const end = new Date();
+  const start = new Date();
+  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
+
+  // 格式化日期为后端需要的格式
+  const formatDate = (date) => {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    const seconds = String(date.getSeconds()).padStart(2, '0');
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+  };
+
+  queryParams.createTime = [formatDate(start), formatDate(end)];
+
   getStats()
   getDevStats();
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())