Bläddra i källkod

Merge remote-tracking branch 'origin/master'

lipenghui 3 månader sedan
förälder
incheckning
62cdad9157

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 199 - 162
pnpm-lock.yaml


+ 6 - 1
src/api/pms/iotmainworkorder/index.ts

@@ -46,11 +46,16 @@ export const IotMainWorkOrderApi = {
     return await request.put({ url: `/pms/iot-main-work-order/update`, data })
   },
 
-  // 填保养工单
+  // 填保养工单
   fillWorkOrder: async (data: any) => {
     return await request.put({ url: `/pms/iot-main-work-order/fillWorkOrder`, data })
   },
 
+  // 手工新增保养工单
+  addWorkOrder: async (data: any) => {
+    return await request.put({ url: `/pms/iot-main-work-order/addWorkOrder`, data })
+  },
+
   // 删除保养工单
   deleteIotMainWorkOrder: async (id: number) => {
     return await request.delete({ url: `/pms/iot-main-work-order/delete?id=` + id })

+ 40 - 1
src/router/modules/remaining.ts

@@ -295,6 +295,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '修改保养计划',
           activeMenu: '/maintenanceplan/edit'
         }
+      },
+      {
+        path: 'maintenanceplan/detail/:id(\\d+)',
+        component: () => import('@/views/pms/maintenance/IotMaintenancePlanDetail.vue'),
+        name: 'IotMaintenancePlanDetail',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:edit',
+          title: '保养计划详情',
+          activeMenu: '/maintenanceplan/detail'
+        }
       }
     ]
   },
@@ -329,10 +342,36 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           icon: 'ep:add',
-          title: '填保养工单',
+          title: '填保养工单',
           activeMenu: '/mainworkorder/bom'
         }
       },
+      {
+        path: 'mainworkorder/add',
+        component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue'),
+        name: 'IotMainWorkOrderAdd',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: '新增保养工单',
+          activeMenu: '/mainworkorder/add'
+        }
+      },
+      {
+        path: 'mainworkorder/detail/:id(\\d+)',
+        component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrderDetail.vue'),
+        name: 'IotMainWorkOrderDetail',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: '保养工单详情',
+          activeMenu: '/mainworkorder/detail'
+        }
+      }
     ]
   },
 

+ 1 - 0
src/utils/dict.ts

@@ -267,5 +267,6 @@ export enum DICT_TYPE {
   // ========== PMS模块  ==========
   PMS_BOM_NODE_EXT_ATTR = 'BOM_NODE_EXT_ATTR', // BOM节点扩展属性 维护 or 保养
   PMS_MAIN_WORK_ORDER_TYPE = 'pms_main_work_order_type', // 保养工单类型
+  PMS_MAIN_WORK_ORDER_RESULT = 'pms_main_work_order_result', // 保养工单状态
   RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection',//是否数采
 }

+ 2 - 1
src/views/pms/bom/MaterialListDrawer.vue

@@ -92,6 +92,7 @@ watch(() => props.nodeId, async (newVal) => {
 // 加载指定bom节点下的物料数据
 const loadMaterials = async (nodeId) => {
   queryParams.bomId = nodeId
+  queryParams.pageNo = 1
   try {
     loading.value = true
     // API调用
@@ -121,7 +122,7 @@ const handleClose = () => {
   materials.value = []
 }
 
-defineExpose({ openDrawer, closeDrawer }) // 暴露方法给父组件
+defineExpose({ openDrawer, closeDrawer, loadMaterials }) // 暴露方法给父组件
 
 </script>
 

+ 27 - 4
src/views/pms/bom/index.vue

@@ -69,10 +69,11 @@
           row-key="id"
           :default-expand-all="isExpandAll"
           v-if="refreshTable"
+          style="width: 100%"
         >
           <el-table-column prop="name" label="BOM节点名称" />
           <el-table-column prop="sort" label="排序" />
-          <el-table-column prop="status" label="状态">
+          <el-table-column prop="status" label="状态" >
             <template #default="scope">
               <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
             </template>
@@ -81,10 +82,10 @@
             label="创建时间"
             align="center"
             prop="createTime"
-            width="180"
             :formatter="dateFormatter"
           />
-          <el-table-column label="操作" align="center">
+          <el-table-column prop="materials" label="物料数量" />
+          <el-table-column label="操作" align="center" >
             <template #default="scope">
               <el-button
                 link
@@ -134,6 +135,7 @@
     :model-value="drawerVisible"
     @update:model-value="val => drawerVisible = val"
     :node-id="currentBomNodeId"
+    ref="showDrawer"
   />
 </template>
 <script lang="ts" setup>
@@ -202,10 +204,12 @@ const openSelectMaterialForm = (id?: number) => {
 }
 
 /** 查看物料详情 */
-const handleView = (nodeId) => {
+const handleView = async (nodeId) => {
   currentBomNodeId.value = nodeId
   drawerVisible.value = true
   showDrawer.value.openDrawer()
+  // 强制刷新物料数据
+  await showDrawer.value.loadMaterials(nodeId)
 }
 
 const chooseMaterial = async(row) => {
@@ -214,9 +218,13 @@ const chooseMaterial = async(row) => {
     CommonBomMaterialData.value.deviceCategoryId = selectedId.value
     CommonBomMaterialData.value.bomNodeId = currentBomNodeId.value
     CommonBomMaterialData.value.materialId = row.id
+    CommonBomMaterialData.value.name = row.name
+    CommonBomMaterialData.value.code = row.code
     const data = CommonBomMaterialData.value as unknown as CommonBomMaterialVO
     await CommonBomMaterialApi.createCommonBomMaterial(data);
     message.success(t('common.createSuccess'))
+    // 保存成功后立即刷新抽屉数据
+    showDrawer.value.loadMaterials(currentBomNodeId.value)
   } finally {
     // formLoading.value = false
   }
@@ -275,3 +283,18 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<style scoped>
+/* 确保表格容器正确继承宽度 */
+:deep(.el-table) {
+  width: 100% !important;
+}
+
+/* 操作按钮换行优化 */
+.flex-wrap {
+  flex-wrap: wrap;
+}
+.gap-4px {
+  gap: 4px;
+}
+</style>

+ 23 - 1
src/views/pms/iotmainworkorder/IotMainWorkOrder.vue

@@ -63,12 +63,19 @@
     <!-- 列表 -->
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <!-- 添加序号列 -->
+        <el-table-column
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
         <el-table-column label="bom节点" align="center" prop="bomNodeId" v-if="false"/>
         <el-table-column label="设备编码" align="center" prop="deviceCode" />
         <el-table-column label="设备名称" align="center" prop="deviceName" />
         <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
         <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
-        <el-table-column label="BOM节点" align="center" prop="name" />
+        <el-table-column label="保养项" align="center" prop="name" />
         <el-table-column label="运行里程" key="mileageRule" width="80">
           <template #default="scope">
             <el-switch
@@ -99,6 +106,11 @@
             />
           </template>
         </el-table-column>
+        <el-table-column label="已选物料" align="center" width="100">
+          <template #default="scope">
+            {{ hasMaterial(scope.row.bomNodeId) ? '是' : '否' }}
+          </template>
+        </el-table-column>
         <el-table-column label="操作" align="center" min-width="120px">
           <template #default="scope">
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
@@ -499,6 +511,10 @@ const handleView = (nodeId) => {
   console.log('当前bom节点:', currentBomNodeId.value)
 }
 
+const hasMaterial = (bomNodeId: number) => {
+  return materialList.value.some(item => item.bomNodeId === bomNodeId)
+}
+
 // 保存配置
 const saveConfig = () => {
   (configFormRef.value as any).validate((valid: boolean) => {
@@ -738,12 +754,18 @@ onMounted(async () => {
   // formData.value.name = dept.value.name + ' - 保养计划'
   deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
   formData.value.deptId = deptId
+
   // if (id){
   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 = []

+ 876 - 0
src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue

@@ -0,0 +1,876 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      v-loading="formLoading"
+      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
+      label-width="130px"
+    >
+      <div class="base-expandable-content">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item label="工单名称" prop="name">
+              <el-input type="text" v-model="formData.name" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="工单编号" prop="orderNumber">
+              <el-input type="text" v-model="formData.orderNumber" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <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="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+  </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="warning">
+          <Icon icon="ep:plus" 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
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
+        <el-table-column label="bom节点" align="center" prop="bomNodeId" v-if="false"/>
+        <el-table-column label="设备编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="保养项" align="center" prop="name" />
+        <!--
+        <el-table-column label="运行里程" key="mileageRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.mileageRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="运行时间" key="runningTimeRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.runningTimeRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="自然日期" key="naturalDateRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.naturalDateRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        -->
+        <el-table-column label="已选物料" align="center" width="100">
+          <template #default="scope">
+            {{ hasMaterial(scope.row.bomNodeId) ? '是' : '否' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" min-width="120px">
+          <template #default="scope">
+            <div style="display: flex; justify-content: center; align-items: center; width: 100%">
+              <div>
+                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
+                <el-button
+                  style="vertical-align: middle"
+                  link
+                  type="danger"
+                  @click="handleDelete(scope.row.id+'-'+scope.row.bomNodeId)"
+                >
+                  移除
+                </el-button>
+              </div>
+              <!-- 新增配置按钮
+              <div style="margin-left: 12px">
+                <el-button
+                  link
+                  type="primary"
+                  @click="openConfigDialog(scope.row)"
+                >
+                  配置
+                </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
+                  type="primary"
+                  @click="handleView(scope.row.bomNodeId)"
+                >
+                  物料详情
+                </el-button>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </ContentWrap>
+
+    <!-- 选择的物料列表 -->
+    <ContentWrap>
+      <el-table v-loading="false" :data="materialList" :stripe="true" :show-overflow-tooltip="true" v-if="false">
+        <el-table-column label="bom节点" align="center" prop="bomNodeId" />
+        <el-table-column label="物料编码" align="center" prop="materialCode" />
+        <el-table-column label="物料名称" align="center" prop="materialName" />
+        <el-table-column label="单位" align="center" prop="unit" />
+        <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="消耗数量" align="center" prop="quantity" />
+        <el-table-column label="总库存数量" align="center" prop="totalInventoryQuantity" />
+      </el-table>
+    </ContentWrap>
+
+  </ContentWrap>
+  <ContentWrap>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">保 存</el-button>
+        <el-button @click="close">取 消</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <MainPlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
+  <!-- 新增配置对话框 -->
+  <el-dialog
+    v-model="configDialog.visible"
+    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
+    width="600px"
+  >
+    <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
+      <!-- 里程配置 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="上次保养里程数(KM)"
+        prop="lastRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟公里数 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="推迟公里数(KM)"
+        prop="delayKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 运行时间配置 -->
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="上次保养运行时间(H)"
+        prop="lastRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟时长 -->
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="推迟时长(H)"
+        prop="delayDuration"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayDuration"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 自然日期配置 -->
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="上次保养自然日期(D)"
+        prop="lastNaturalDate"
+      >
+        <el-date-picker
+          v-model="configDialog.form.lastNaturalDate"
+          type="date"
+          placeholder="选择日期"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟自然日期 -->
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="推迟自然日期(D)"
+        prop="delayNaturalDate"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayNaturalDate"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 保养规则周期值 + 提前量 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期(KM)"
+        prop="nextRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期-提前量(KM)"
+        prop="kiloCycleLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.kiloCycleLead"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期(H)"
+        prop="nextRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期-提前量(H)"
+        prop="timePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.timePeriodLead"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期(D)"
+        prop="nextNaturalDate"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextNaturalDate"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期-提前量(D)"
+        prop="naturalDatePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.naturalDatePeriodLead"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="configDialog.visible = false">取消</el-button>
+      <el-button type="primary" @click="saveConfig">保存</el-button>
+    </template>
+  </el-dialog>
+  <!-- 表单弹窗:添加/修改 -->
+  <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
+  <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
+  <MaterialListDrawer
+    :model-value="drawerVisible"
+    @update:model-value="val => drawerVisible = val"
+    :node-id="currentBomNodeId"
+    :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
+  />
+</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";
+
+
+/** 保养计划 表单 */
+defineOptions({ name: 'IotMainWorkOrderAdd' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
+const dept = ref() // 当前登录人所属部门对象
+const configFormRef = ref() // 配置弹出框对象
+const bomNodeId = ref() // 最新的bomNodeId
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const drawerVisible = ref<boolean>(false)
+const currentBomNodeId = ref() // 当前选中的bom节点
+const showDrawer = ref()
+const list = ref<IotMainWorkOrderBomVO[]>([]) // 保养工单bom关联列表的数据
+const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
+const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const formData = ref({
+  id: undefined,
+  deptId: undefined,
+  name: '',
+  orderNumber: undefined,
+  responsiblePerson: undefined,
+  remark: undefined,
+  status: undefined,
+})
+const formRules = reactive({
+  name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+interface MaterialFormExpose {
+  open: (deptId: number, bomNodeId: number) => void
+}
+
+const materialFormRef = ref<MaterialFormExpose>();
+
+// 新增配置相关状态
+const configDialog = reactive({
+  visible: false,
+  current: null as IotMaintenanceBomVO | null,
+  form: {
+    lastRunningKilometers: 0,
+    delayKilometers: 0,
+    lastRunningTime: 0,
+    delayDuration: 0,
+    lastNaturalDate: '',
+    delayNaturalDate: 0,
+    // 保养规则 周期
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    // 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0
+  }
+})
+
+// 打开配置对话框
+const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
+  configDialog.current = row
+
+  // 处理日期初始化(核心修改)
+  let initialDate = ''
+  if (row.lastNaturalDate) {
+    // 如果已有值:时间戳 -> 日期字符串
+    initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
+  } else {
+    // 如果无值:设置默认值避免1970问题
+    initialDate = ''
+  }
+
+  configDialog.form = {
+    lastRunningKilometers: row.lastRunningKilometers || 0,
+    delayKilometers: row.delayKilometers || 0,
+    lastRunningTime: row.lastRunningTime || 0,
+    delayDuration: row.delayDuration || 0,
+    lastNaturalDate: initialDate,
+    delayNaturalDate: row.delayNaturalDate || 0,
+    // 保养规则 周期值
+    nextRunningKilometers: row.nextRunningKilometers || 0,
+    nextRunningTime: row.nextRunningTime || 0,
+    nextNaturalDate: row.nextNaturalDate || 0,
+    // 提前量
+    kiloCycleLead: row.kiloCycleLead || 0,
+    timePeriodLead: row.timePeriodLead || 0,
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0
+  }
+  configDialog.visible = true
+}
+
+// const materialFormRef = ref()
+const openMaterialForm = (row: any) => {
+  bomNodeId.value = row.bomNodeId;
+  console.log('这是一个对象:', row.bomNodeId)
+  materialFormRef.value.open(formData.value.deptId, bomNodeId.value)
+}
+
+const selectChoose = (selectedMaterial) => {
+  selectedMaterial.bomNodeId = bomNodeId.value
+  // 关联 bomNodeId
+  const processedMaterials = selectedMaterial.map(material => ({
+    ...material,
+    bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
+  }));
+
+  // 避免重复添加
+  processedMaterials.forEach(newMaterial => {
+    // 检查是否已存在相同 bomNodeId + materialCode 的条目
+    const isExist = materialList.value.some(item =>
+      item.bomNodeId === bomNodeId.value &&
+      item.materialCode === newMaterial.materialCode
+    );
+
+    if (!isExist) {
+      materialList.value.push(newMaterial);
+    }
+  });
+  console.log('选择完成的数据:', JSON.stringify(selectedMaterial))
+  console.log('添加到本地列表的数据:', materialList.value)
+}
+
+
+const deviceChoose = async(selectedDevices) => {
+  const newIds = selectedDevices.map(device => device.id)
+  deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
+  const params = {
+    deviceIds: deviceIds.value.join(',') // 明确传递数组参数
+  }
+  queryParams.deviceIds = JSON.parse(JSON.stringify(params.deviceIds))
+  // 根据选择的设备筛选出设备关系的分类BOM中与保养相关的节点项
+  const res = await IotDeviceApi.deviceAssociateBomList(queryParams)
+  const rawData = res || []
+  if(rawData.length === 0){
+    message.error('选择的设备不存在待保养BOM项')
+  }
+  if (!Array.isArray(rawData)) {
+    console.error('接口返回数据结构异常:', rawData)
+    return
+  }
+  // 转换数据结构(根据你的接口定义调整)
+  const newItems = rawData.map(device => ({
+    assetClass: device.assetClass,
+    deviceCode: device.deviceCode,
+    deviceName: device.deviceName,
+    deviceStatus: device.deviceStatus,
+    deptName: device.deptName,
+    name: device.name,
+    code: device.code,
+    assetProperty: device.assetProperty,
+    remark: null,    // 初始化备注
+    deviceId: device.id, // 移除操作需要
+    bomNodeId: device.bomNodeId,
+    totalRunTime: device.totalRunTime,
+    totalMileage: device.totalMileage,
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    lastNaturalDate: null, // 初始化为null而不是0
+    // 保养规则 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0
+  }))
+  // 获取选择的设备相关的id数组
+  newItems.forEach(item => {
+    deviceIds.value.push(item.deviceId)
+  })
+  // 合并到现有列表(去重)
+  newItems.forEach(item => {
+    const exists = list.value.some(
+      existing => (existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId)
+    )
+    if (!exists) {
+      list.value.push(item)
+    }
+  })
+}
+
+/** 查看已经选择的物料 并编辑 */
+const handleView = (nodeId) => {
+  currentBomNodeId.value = nodeId
+  drawerVisible.value = true
+  // showDrawer.value.openDrawer()
+  console.log('当前bom节点:', currentBomNodeId.value)
+}
+
+const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
+const openForm = () => {
+  deviceFormRef.value?.open();
+}
+
+const hasMaterial = (bomNodeId: number) => {
+  return materialList.value.some(item => item.bomNodeId === bomNodeId)
+}
+
+// 保存配置
+const saveConfig = () => {
+  (configFormRef.value as any).validate((valid: boolean) => {
+    if (!valid) return
+    if (!configDialog.current) return
+
+    // 动态校验逻辑
+    const requiredFields = []
+    if (configDialog.current.mileageRule === 0) {
+      requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
+    }
+    if (configDialog.current.runningTimeRule === 0) {
+      requiredFields.push('nextRunningTime', 'timePeriodLead')
+    }
+    if (configDialog.current.naturalDateRule === 0) {
+      requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
+    }
+
+    const missingFields = requiredFields.filter(field =>
+      !configDialog.form[field as keyof typeof configDialog.form]
+    )
+
+    if (missingFields.length > 0) {
+      message.error('请填写所有必填项')
+      return
+    }
+
+    // 强制校验逻辑
+    if (configDialog.current.naturalDateRule === 0) {
+      if (!configDialog.form.lastNaturalDate) {
+        message.error('必须选择自然日期')
+        return
+      }
+
+      // 验证日期有效性
+      const dateValue = dayjs(configDialog.form.lastNaturalDate)
+      if (!dateValue.isValid()) {
+        message.error('日期格式不正确')
+        return
+      }
+    }
+
+    // 转换逻辑(关键修改)
+    const finalDate = configDialog.form.lastNaturalDate
+      ? dayjs(configDialog.form.lastNaturalDate).valueOf()
+      : null // 改为null而不是0
+
+    // 更新当前行的数据
+    Object.assign(configDialog.current, {
+      ...configDialog.form,
+      lastNaturalDate: finalDate
+    })
+    configDialog.visible = false
+  })
+}
+
+const queryParams = reactive({
+  workOrderId: id
+})
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotMainWorkOrder', params:{}})
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验表格数据
+  const isValid = validateTableData()
+  if (!isValid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      mainWorkOrder: formData.value,
+      mainWorkOrderBom: list.value,
+      mainWorkOrderMaterials: materialList.value
+    }
+    await IotMainWorkOrderApi.addWorkOrder(data)
+    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 {
+    formLoading.value = false
+  }
+}
+
+// 新增表单校验规则
+const configFormRules = reactive({
+  nextRunningKilometers: [{
+    required: true,
+    message: '里程周期必须填写',
+    trigger: 'blur'
+  }],
+  kiloCycleLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextRunningTime: [{
+    required: true,
+    message: '时间周期必须填写',
+    trigger: 'blur'
+  }],
+  timePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextNaturalDate: [{
+    required: true,
+    message: '自然日周期必须填写',
+    trigger: 'blur'
+  }],
+  naturalDatePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }]
+})
+
+/** 校验表格数据 */
+const validateTableData = (): boolean => {
+  let isValid = true
+  const errorMessages: string[] = []
+  const noRulesErrorMessages: string[] = []  // 未设置任何保养项规则 的错误提示信息
+  const noRules: string[] = []  // 行记录中设置了保养规则的记录数量
+  const configErrors: string[] = []   // 保养规则配置弹出框
+  let shouldBreak = false;
+
+  if (list.value.length === 0) {
+    errorMessages.push('请至少添加一条设备保养明细')
+    isValid = false
+    // 直接返回无需后续校验
+    message.error('请至少添加一条设备保养明细')
+    return isValid
+  }
+
+  /* list.value.forEach((row, index) => {
+    if (shouldBreak) return;
+    const rowNumber = index + 1 // 用户可见的行号从1开始
+    const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
+    // 校验逻辑
+    const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
+      if (ruleValue === 0) { // 规则开启
+        if (!row[configField] || row[configField] <= 0) {
+          configErrors.push(`第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`)
+          isValid = false
+        }
+      }
+    }
+    // 里程校验逻辑
+    if (row.mileageRule === 0) { // 假设 0 表示开启状态
+      if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
+        isValid = false
+      }
+      // 再校验配置值
+      checkConfig('里程', row.mileageRule, 'lastRunningKilometers')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置里程规则`)
+    }
+    // 运行时间校验逻辑
+    if (row.runningTimeRule === 0) {
+      if (!row.nextRunningTime || row.nextRunningTime <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启运行时间规则必须填写有效的时间周期`)
+        isValid = false
+      }
+      checkConfig('运行时间', row.runningTimeRule, 'lastRunningTime')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置运行时间规则`)
+    }
+    // 自然日期校验逻辑
+    if (row.naturalDateRule === 0) {
+      if (!row.nextNaturalDate) {
+        errorMessages.push(`第 ${rowNumber} 行:开启自然日期规则必须填写有效的自然日期周期`)
+        isValid = false
+      }
+      checkConfig('自然日期', row.naturalDateRule, 'lastNaturalDate')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置自然日期规则`)
+    }
+    // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
+    if (noRules.length === 3) {
+      isValid = false
+      shouldBreak = true; // 设置标志变量为true,退出循环
+      noRulesErrorMessages.push('保养项至少设置1个保养规则')
+    }
+    noRules.length = 0;
+  })
+  if (errorMessages.length > 0) {
+    message.error('设置保养规则后,请维护对应的周期值')
+  } else if (noRulesErrorMessages.length > 0) {
+    message.error(noRulesErrorMessages.pop())
+  } else if (configErrors.length > 0) {
+    message.error(configErrors.pop())
+  } */
+  return isValid
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceId: undefined,
+    status: undefined,
+    description: undefined,
+    pic: undefined,
+    remark: undefined,
+    deviceName: undefined,
+    processInstanceId: undefined,
+    auditStatus: undefined,
+    deptId: undefined
+  }
+  formRef.value?.resetFields()
+}
+onMounted(async () => {
+  materialList.value = []
+  const deptId = useUserStore().getUser.deptId
+  // 查询当前登录人所属部门名称
+  dept.value = await DeptApi.getDept(deptId)
+  formData.value.name = dept.value.name + ' - 保养工单'
+  deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+  formData.value.deptId = deptId
+
+  // if (id){
+  try{
+    formType.value = 'create'
+    const { wsCache } = useCache()
+    const userInfo = wsCache.get(CACHE_KEY.USER)
+    formData.value.responsiblePerson = userInfo.user.id;
+    // 查询保养工单 主表数据
+    // 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
+      }))
+    } */
+  } catch (error) {
+    console.error('数据加载失败:', error)
+    message.error('数据加载失败,请重试')
+  }
+})
+
+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 {}
+}
+
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+</style>

+ 830 - 0
src/views/pms/iotmainworkorder/IotMainWorkOrderDetail.vue

@@ -0,0 +1,830 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      v-loading="formLoading"
+      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
+      label-width="130px"
+    >
+      <div class="base-expandable-content">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item label="工单名称" prop="name">
+              <el-input type="text" v-model="formData.name" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="工单编号" prop="orderNumber">
+              <el-input type="text" v-model="formData.orderNumber" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <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="工单类型" prop="type">
+              <el-select disabled v-model="formData.type" clearable>
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="保养结果" prop="result">
+              <el-select disabled v-model="formData.result" clearable>
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" disabled/>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+  </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="warning">
+            <Icon icon="ep:plus" 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
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
+        <el-table-column label="bom节点" align="center" prop="bomNodeId" v-if="false"/>
+        <el-table-column label="设备编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="保养项" align="center" prop="name" />
+        <el-table-column label="运行里程" key="mileageRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.mileageRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="运行时间" key="runningTimeRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.runningTimeRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="自然日期" key="naturalDateRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.naturalDateRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="true"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="已选物料" align="center" width="100">
+          <template #default="scope">
+            {{ hasMaterial(scope.row.bomNodeId) ? '是' : '否' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" min-width="120px">
+          <template #default="scope">
+            <div style="display: flex; justify-content: center; align-items: center; width: 100%">
+              <!-- 新增配置按钮 -->
+              <div style="margin-left: 12px">
+                <el-button
+                  link
+                  type="primary"
+                  @click="openConfigDialog(scope.row)"
+                >
+                  配置
+                </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
+                  type="primary"
+                  @click="handleView(scope.row.id, scope.row.bomNodeId)"
+                >
+                  物料详情
+                </el-button>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </ContentWrap>
+
+    <!-- 选择的物料列表 -->
+    <ContentWrap>
+      <el-table v-loading="false" :data="materialList" :stripe="true" :show-overflow-tooltip="true" v-if="false">
+        <el-table-column label="bom节点" align="center" prop="bomNodeId" />
+        <el-table-column label="物料编码" align="center" prop="materialCode" />
+        <el-table-column label="物料名称" align="center" prop="materialName" />
+        <el-table-column label="单位" align="center" prop="unit" />
+        <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="消耗数量" align="center" prop="quantity" />
+        <el-table-column label="总库存数量" align="center" prop="totalInventoryQuantity" />
+      </el-table>
+    </ContentWrap>
+
+  </ContentWrap>
+  <ContentWrap>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button @click="close">取 消</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <!-- 新增配置对话框 -->
+  <el-dialog
+    v-model="configDialog.visible"
+    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
+    width="600px"
+  >
+    <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
+      <!-- 里程配置 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="上次保养里程数(KM)"
+        prop="lastRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟公里数 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="推迟公里数(KM)"
+        prop="delayKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 运行时间配置 -->
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="上次保养运行时间(H)"
+        prop="lastRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟时长 -->
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="推迟时长(H)"
+        prop="delayDuration"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayDuration"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 自然日期配置 -->
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="上次保养自然日期(D)"
+        prop="lastNaturalDate"
+      >
+        <el-date-picker
+          v-model="configDialog.form.lastNaturalDate"
+          type="date"
+          placeholder="选择日期"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 推迟自然日期 -->
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="推迟自然日期(D)"
+        prop="delayNaturalDate"
+      >
+        <el-input-number
+          v-model="configDialog.form.delayNaturalDate"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <!-- 保养规则周期值 + 提前量 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期(KM)"
+        prop="nextRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期-提前量(KM)"
+        prop="kiloCycleLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.kiloCycleLead"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期(H)"
+        prop="nextRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期-提前量(H)"
+        prop="timePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.timePeriodLead"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期(D)"
+        prop="nextNaturalDate"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextNaturalDate"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期-提前量(D)"
+        prop="naturalDatePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.naturalDatePeriodLead"
+          :min="0"
+          controls-position="right"
+          :disabled="true"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="configDialog.visible = false">取消</el-button>
+    </template>
+  </el-dialog>
+  <!-- 表单弹窗:添加/修改 -->
+  <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
+  <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
+  <MaterialListDrawer
+    :model-value="drawerVisible"
+    @update:model-value="val => drawerVisible = val"
+    :node-id="currentBomNodeId"
+    :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
+  />
+</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";
+
+/** 保养计划 表单 */
+defineOptions({ name: 'IotMainWorkOrderDetail' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
+const dept = ref() // 当前登录人所属部门对象
+const configFormRef = ref() // 配置弹出框对象
+const bomNodeId = ref() // 最新的bomNodeId
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const drawerVisible = ref<boolean>(false)
+const currentBomNodeId = ref() // 当前选中的bom节点
+const showDrawer = ref()
+const list = ref<IotMainWorkOrderBomVO[]>([]) // 保养工单bom关联列表的数据
+const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
+const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const formData = ref({
+  id: undefined,
+  deptId: undefined,
+  name: '',
+  orderNumber: undefined,
+  responsiblePerson: undefined,
+  type: undefined,
+  result: undefined,
+  remark: undefined,
+  status: undefined,
+})
+const formRules = reactive({
+  name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+interface MaterialFormExpose {
+  open: (deptId: number, bomNodeId: number) => void
+}
+
+const materialFormRef = ref<MaterialFormExpose>();
+
+// 新增配置相关状态
+const configDialog = reactive({
+  visible: false,
+  current: null as IotMaintenanceBomVO | null,
+  form: {
+    lastRunningKilometers: 0,
+    delayKilometers: 0,
+    lastRunningTime: 0,
+    delayDuration: 0,
+    lastNaturalDate: '',
+    delayNaturalDate: 0,
+    // 保养规则 周期
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    // 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0
+  }
+})
+
+// 打开配置对话框
+const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
+  configDialog.current = row
+
+  // 处理日期初始化(核心修改)
+  let initialDate = ''
+  if (row.lastNaturalDate) {
+    // 如果已有值:时间戳 -> 日期字符串
+    initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
+  } else {
+    // 如果无值:设置默认值避免1970问题
+    initialDate = ''
+  }
+
+  configDialog.form = {
+    lastRunningKilometers: row.lastRunningKilometers || 0,
+    delayKilometers: row.delayKilometers || 0,
+    lastRunningTime: row.lastRunningTime || 0,
+    delayDuration: row.delayDuration || 0,
+    lastNaturalDate: initialDate,
+    delayNaturalDate: row.delayNaturalDate || 0,
+    // 保养规则 周期值
+    nextRunningKilometers: row.nextRunningKilometers || 0,
+    nextRunningTime: row.nextRunningTime || 0,
+    nextNaturalDate: row.nextNaturalDate || 0,
+    // 提前量
+    kiloCycleLead: row.kiloCycleLead || 0,
+    timePeriodLead: row.timePeriodLead || 0,
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0
+  }
+  configDialog.visible = true
+}
+
+// const materialFormRef = ref()
+const openMaterialForm = (row: any) => {
+  bomNodeId.value = row.bomNodeId;
+  console.log('这是一个对象:', row.bomNodeId)
+  materialFormRef.value.open(formData.value.deptId, bomNodeId.value)
+}
+
+const selectChoose = (selectedMaterial) => {
+  selectedMaterial.bomNodeId = bomNodeId.value
+  // 关联 bomNodeId
+  const processedMaterials = selectedMaterial.map(material => ({
+    ...material,
+    bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
+  }));
+
+  // 避免重复添加
+  processedMaterials.forEach(newMaterial => {
+    // 检查是否已存在相同 bomNodeId + materialCode 的条目
+    const isExist = materialList.value.some(item =>
+      item.bomNodeId === bomNodeId.value &&
+      item.materialCode === newMaterial.materialCode
+    );
+
+    if (!isExist) {
+      materialList.value.push(newMaterial);
+    }
+  });
+  console.log('选择完成的数据:', JSON.stringify(selectedMaterial))
+  console.log('添加到本地列表的数据:', materialList.value)
+}
+
+/** 查看已经选择的物料 */
+const handleView = (nodeId, bomId) => {
+  currentBomNodeId.value = nodeId
+  drawerVisible.value = true
+  // showDrawer.value.openDrawer()
+  const queryParams = {
+    pageNo: 1,
+    pageSize: 100,
+    workOrderId: formData.value.id,
+    bomId: nodeId
+  }
+  IotMainWorkOrderBomMaterialApi.getIotMainWorkOrderBomMaterialPage(queryParams).then((res) => {
+    currentBomNodeId.value = bomId
+    materialList.value = res.list
+  })
+  console.log('当前bom节点:', currentBomNodeId.value)
+}
+
+const hasMaterial = (bomNodeId: number) => {
+  return materialList.value.some(item => item.bomNodeId === bomNodeId)
+}
+
+// 保存配置
+const saveConfig = () => {
+  (configFormRef.value as any).validate((valid: boolean) => {
+    if (!valid) return
+    if (!configDialog.current) return
+
+    // 动态校验逻辑
+    const requiredFields = []
+    if (configDialog.current.mileageRule === 0) {
+      requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
+    }
+    if (configDialog.current.runningTimeRule === 0) {
+      requiredFields.push('nextRunningTime', 'timePeriodLead')
+    }
+    if (configDialog.current.naturalDateRule === 0) {
+      requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
+    }
+
+    const missingFields = requiredFields.filter(field =>
+      !configDialog.form[field as keyof typeof configDialog.form]
+    )
+
+    if (missingFields.length > 0) {
+      message.error('请填写所有必填项')
+      return
+    }
+
+    // 强制校验逻辑
+    if (configDialog.current.naturalDateRule === 0) {
+      if (!configDialog.form.lastNaturalDate) {
+        message.error('必须选择自然日期')
+        return
+      }
+
+      // 验证日期有效性
+      const dateValue = dayjs(configDialog.form.lastNaturalDate)
+      if (!dateValue.isValid()) {
+        message.error('日期格式不正确')
+        return
+      }
+    }
+
+    // 转换逻辑(关键修改)
+    const finalDate = configDialog.form.lastNaturalDate
+      ? dayjs(configDialog.form.lastNaturalDate).valueOf()
+      : null // 改为null而不是0
+
+    // 更新当前行的数据
+    Object.assign(configDialog.current, {
+      ...configDialog.form,
+      lastNaturalDate: finalDate
+    })
+    configDialog.visible = false
+  })
+}
+
+const queryParams = reactive({
+  workOrderId: id
+})
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotMainWorkOrder', params:{}})
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验表格数据
+  // const isValid = validateTableData()
+  // if (!isValid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      mainWorkOrder: formData.value,
+      mainWorkOrderBom: list.value,
+      mainWorkOrderMaterials: materialList.value
+    }
+    debugger
+    await IotMainWorkOrderApi.fillWorkOrder(data)
+    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 {
+    formLoading.value = false
+  }
+}
+
+// 新增表单校验规则
+const configFormRules = reactive({
+  nextRunningKilometers: [{
+    required: true,
+    message: '里程周期必须填写',
+    trigger: 'blur'
+  }],
+  kiloCycleLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextRunningTime: [{
+    required: true,
+    message: '时间周期必须填写',
+    trigger: 'blur'
+  }],
+  timePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextNaturalDate: [{
+    required: true,
+    message: '自然日周期必须填写',
+    trigger: 'blur'
+  }],
+  naturalDatePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }]
+})
+
+/** 校验表格数据 */
+const validateTableData = (): boolean => {
+  let isValid = true
+  const errorMessages: string[] = []
+  const noRulesErrorMessages: string[] = []  // 未设置任何保养项规则 的错误提示信息
+  const noRules: string[] = []  // 行记录中设置了保养规则的记录数量
+  const configErrors: string[] = []   // 保养规则配置弹出框
+  let shouldBreak = false;
+
+  if (list.value.length === 0) {
+    errorMessages.push('请至少添加一条设备保养明细')
+    isValid = false
+    // 直接返回无需后续校验
+    message.error('请至少添加一条设备保养明细')
+    return isValid
+  }
+
+  list.value.forEach((row, index) => {
+    if (shouldBreak) return;
+    const rowNumber = index + 1 // 用户可见的行号从1开始
+    const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
+    // 校验逻辑
+    const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
+      if (ruleValue === 0) { // 规则开启
+        if (!row[configField] || row[configField] <= 0) {
+          configErrors.push(`第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`)
+          isValid = false
+        }
+      }
+    }
+    // 里程校验逻辑
+    if (row.mileageRule === 0) { // 假设 0 表示开启状态
+      if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
+        isValid = false
+      }
+      // 再校验配置值
+      checkConfig('里程', row.mileageRule, 'lastRunningKilometers')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置里程规则`)
+    }
+    // 运行时间校验逻辑
+    if (row.runningTimeRule === 0) {
+      if (!row.nextRunningTime || row.nextRunningTime <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启运行时间规则必须填写有效的时间周期`)
+        isValid = false
+      }
+      checkConfig('运行时间', row.runningTimeRule, 'lastRunningTime')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置运行时间规则`)
+    }
+    // 自然日期校验逻辑
+    if (row.naturalDateRule === 0) {
+      if (!row.nextNaturalDate) {
+        errorMessages.push(`第 ${rowNumber} 行:开启自然日期规则必须填写有效的自然日期周期`)
+        isValid = false
+      }
+      checkConfig('自然日期', row.naturalDateRule, 'lastNaturalDate')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置自然日期规则`)
+    }
+    // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
+    if (noRules.length === 3) {
+      isValid = false
+      shouldBreak = true; // 设置标志变量为true,退出循环
+      noRulesErrorMessages.push('保养项至少设置1个保养规则')
+    }
+    noRules.length = 0;
+  })
+  if (errorMessages.length > 0) {
+    message.error('设置保养规则后,请维护对应的周期值')
+  } else if (noRulesErrorMessages.length > 0) {
+    message.error(noRulesErrorMessages.pop())
+  } else if (configErrors.length > 0) {
+    message.error(configErrors.pop())
+  }
+  return isValid
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceId: undefined,
+    status: undefined,
+    description: undefined,
+    pic: undefined,
+    remark: undefined,
+    deviceName: undefined,
+    processInstanceId: undefined,
+    auditStatus: undefined,
+    deptId: undefined
+  }
+  formRef.value?.resetFields()
+}
+onMounted(async () => {
+  materialList.value = []
+  const deptId = useUserStore().getUser.deptId
+  // 查询当前登录人所属部门名称
+  dept.value = await DeptApi.getDept(deptId)
+  // formData.value.name = dept.value.name + ' - 保养计划'
+  deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+  formData.value.deptId = deptId
+  // if (id){
+  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
+      }))
+    }
+  } catch (error) {
+    console.error('数据加载失败:', error)
+    message.error('数据加载失败,请重试')
+  }
+})
+
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+</style>

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

@@ -44,6 +44,7 @@
         ref="tableRef"
         :show-overflow-tooltip="true"
         @row-click="handleRowClick"
+        :row-class-name="rowClassName"
       >
         <el-table-column width="60" label="选择">
           <template #default="{ row }">
@@ -140,7 +141,6 @@ const handleChildSubmit = (formData) => {
   addMateriall.value = modified;
   list.value.unshift(modified)
   total.value = total.value+1
-  debugger
 }
 const removeOnesFromKeys = (obj: Record<string, any>) => {
   return Object.keys(obj).reduce(
@@ -187,7 +187,6 @@ const handleConfirm = () => {
     return
   }
   const filters = selectedRows.value.filter((item) => item.quantity === null||item.quantity === undefined)
-  debugger
   if (filters.length > 0) {
     message.error('消耗数量必填')
     return
@@ -216,6 +215,20 @@ const handleClose = () => {
   emit('close')
 }
 
+const rowClassName = ({ row }: { row: any }) => {
+  let className = '';
+  if(row.bomMaterialFlag === 'Y'){
+    className = 'row-green-bg'
+  }
+  if (row.materialSource === '本地库存' && row.bomMaterialFlag != 'Y') {
+    className = ''
+  }
+  if (row.materialSource === 'sap库存') {
+    className = 'row-red-bg'
+  }
+  return className
+}
+
 // 多选 切换行选中状态
 const toggleRow = (row) => {
   const index = selectedRows.value.findIndex((item) => item.materialCode === row.materialCode)
@@ -268,4 +281,12 @@ const resetQuery = () => {
   background-color: #c2dca8;
   border-color: #c2dca8;
 }
+
+:deep(.el-table .row-green-bg) {
+  background-color: #f0f9eb !important; /* Element Plus 绿色背景 */
+}
+
+:deep(.el-table .bg-blue-5) {
+  background-color: #3fa5de !important; /* Element Plus 绿色背景 */
+}
 </style>

+ 23 - 4
src/views/pms/iotmainworkorder/index.vue

@@ -72,6 +72,11 @@
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="工单号" align="center" prop="orderNumber" />
       <el-table-column label="工单名称" align="center" prop="name" />
+      <el-table-column label="保养状态" align="center" prop="result" >
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT" :value="scope.row.result" />
+        </template>
+      </el-table-column>
       <el-table-column label="工单类型" align="center" prop="type" >
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE" :value="scope.row.type" />
@@ -106,9 +111,19 @@
             type="primary"
             @click="openForm('update', scope.row.id)"
             v-hasPermi="['pms:iot-main-work-order:update']"
+            v-if="scope.row.result === 1"
           >
             填报
           </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="detail(scope.row.id)"
+            v-hasPermi="['pms:iot-main-work-order:query']"
+          >
+            查看
+          </el-button>
+          <!--
           <el-button
             link
             type="danger"
@@ -117,6 +132,7 @@
           >
             删除
           </el-button>
+          -->
         </template>
       </el-table-column>
     </el-table>
@@ -204,14 +220,13 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  // formRef.value.open(type, id)
-  //修改
+  // 修改
   if (typeof id === 'number') {
     push({ name: 'IotMainWorkOrderBom', params: {id } })
     return
+  } else {
+    push({ name: 'IotMainWorkOrderAdd', params:{} })
   }
-  // 新增
-  // push({ name: 'IotAddMainPlan', params:{} })
 }
 
 /** 删除按钮操作 */
@@ -227,6 +242,10 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+const detail = (id?: number) => {
+  push({ name: 'IotMainWorkOrderDetail', params:{id} })
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 0 - 263
src/views/pms/iotopeationmodel/IotOpeationModelForm.vue

@@ -1,263 +0,0 @@
-<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="部门id" prop="deptId">
-        <el-input v-model="formData.deptId" placeholder="请输入部门id" />
-      </el-form-item>
-      <el-form-item label="所属组织" prop="orgName">
-        <el-input v-model="formData.orgName" placeholder="请输入所属组织" />
-      </el-form-item>-->
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="设备类别" prop="deviceType">
-            <el-tree-select
-              v-model="formData.deviceType"
-              :data="productClassifyList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              placeholder="请选择设备类别"
-              @change="assetclasschange"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="设备部件" prop="deviceComponent">
-            <el-input v-model="formData.deviceComponent" placeholder="请输入设备部件" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="12">
-          <div v-for="(dynamicItem, index) in dynamicItems" :key="index">
-            <el-form-item label="填写信息" prop="fillInfo" :for="`dynamic-${index}`" >
-              <el-input v-model="dynamicItems[index]" :id="`dynamic-${index}`" placeholder="请输入填写信息"/>
-            </el-form-item>
-
-          </div>
-        </el-col>
-        <el-col :span="12">
-          <div style="margin-left:30px">
-            <el-button type="success" @click="addDynamicItem">添加</el-button>
-            <el-button type="danger" @click="removeDynamicItem(index)">移除</el-button>
-          </div>
-        </el-col>
-      </el-row>
-
-
-    </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 { IotOpeationModelApi, IotOpeationModelVO } from '@/api/pms/iotopeationmodel'
-import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
-import {defaultProps,handleTree} from "@/utils/tree";
-import * as DeptApi from "@/api/system/dept";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import {IotDeviceApi} from "@/api/pms/device";
-import {object} from "vue-types";
-import {userInfo} from "os";
-import {UserInfo} from "@/layout/components/UserInfo";
-
-
-
-/** 运行记录模板主 表单 */
-defineOptions({ name: 'IotOpeationModelForm' })
-const deptInfo = defineProps(['dept'])
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const list = ref([])
-
-const deptList = ref<Tree[]>([]) // 树形结构
-const productClassifyList = ref<Tree[]>([]) // 树形结构
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const { params, name } = useRoute() // 查询参数
-const id = params.id
-const brandLabel = ref('') // 表单的类型:create - 新增;update - 修改
-const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
-const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
-
-
-import { ref } from 'vue';
-
-
-
-
-
-
-
-
-const fixedItems = ref([
-  { label: '设备类别', value: '' },
-  { label: '设备部件', value: '' }
-]);
-
-const comp = ref([''])//初始化部件数组
-
-function addComp() {
-  comp.value.push(''); // 添加一个空字符串作为新的动态项输入框的初始值
-}
-
-function removeComp(index) {
-  comp.value.splice(index, 1); // 移除指定索引的动态项
-}
-
-const dynamicItems = ref(['']); // 初始动态项数组
-
-function addDynamicItem() {
-  dynamicItems.value.push(''); // 添加一个空字符串作为新的动态项输入框的初始值
-}
-
-function removeDynamicItem(index) {
-    dynamicItems.value.splice(index, 1); // 移除指定索引的动态项
-}
-
-
-const formData = ref({
-  id: undefined,
-  deptId: undefined,
-  orgName: undefined,
-  deviceType: undefined,
-  deviceComponent: undefined,
-  fillInfo: undefined,
-  creName: undefined,
-  creDate: undefined,
-})
-const formRules = reactive({
-  orgName: [{ required: true, message: '所属组织不能为空', trigger: 'blur' }],
-  deviceType: [{ required: true, message: '设备类别不能为空', trigger: 'change' }],
-  deviceComponent: [{ required: true, message: '设备部件不能为空', trigger: 'blur' }],
-  fillInfo: [{ required: true, message: '填写信息不能为空', trigger: 'blur' }],
-  creName: [{ required: true, message: '创建人不能为空', trigger: 'blur' }],
-  creDate: [{ required: true, message: '创建日期不能为空', trigger: 'blur' }],
-})
-const formRef = ref() // 表单 Ref
-
-onMounted(async () => {
-  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-  productClassifyList.value = handleTree(
-    await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
-  )
-  formData.value.assetProperty = 'zy'
-  // 修改时,设置数据
-  if (id) {
-    formType.value = 'update';
-    formLoading.value = true
-    try {
-      const iotDevice = await IotDeviceApi.getIotDevice(id);
-      formData.value = iotDevice
-      brandLabel.value = iotDevice.brandName;
-      zzLabel.value = iotDevice.zzName;
-      supplierLabel.value = iotDevice.supplierName;
-      list.value = JSON.parse(iotDevice.templateJson);
-      list.value.forEach((item) => {
-        formData.value[item.identifier] = item.value;
-      })
-    } finally {
-      formLoading.value = false
-    }
-  } else {
-    formType.value = 'create';
-  }
-})
-
-const assetclasschange = () => {
-  const deviceType = formData.value.deviceType
-  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(deviceType).then(res => {
-    if (res){
-      res.forEach((item) => {
-        if (item.requiredFlag) {
-          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
-          item.rules = []
-          item.rules.push(rule)
-        }
-      })
-      list.value = res
-      debugger
-    } else {
-      list.value = []
-    }
-  })
-}
-/** 打开弹窗 */
-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 IotOpeationModelApi.getIotOpeationModel(id)
-      dynamicItems.value = formData.value.fillInfo.split(",")
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-
-const arry = []
-
-const submitForm = async () => {
-  // 方法1:使用JavaScript的Date对象
-  formData.value.creName = "test";
-  if(formData.value.deptId==undefined){
-    formData.value.deptId = deptInfo.dept.deptId;
-    formData.value.orgName = deptInfo.dept.orgName;
-  }
-  formData.value.fillInfo = dynamicItems.value.join(",");
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as IotOpeationModelVO
-    if (formType.value === 'create') {
-      alert(JSON.stringify(data));
-      await IotOpeationModelApi.createIotOpeationModel(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await IotOpeationModelApi.updateIotOpeationModel(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    deptId: undefined,
-    orgName: undefined,
-    deviceType: undefined,
-    deviceComponent: undefined,
-    fillInfo: undefined,
-    creName: undefined,
-    creDate: undefined,
-  }
-  formRef.value?.resetFields()
-  dynamicItems.value = [''];
-}
-</script>

+ 0 - 306
src/views/pms/iotopeationmodel/index.vue

@@ -1,306 +0,0 @@
-<template>
-
-  <el-row :gutter="20">
-    <el-col :span="4" :xs="24">
-      <ContentWrap class="h-1/1">
-        <DeptTree @node-click="handleDeptNodeClick" />
-      </ContentWrap>
-    </el-col>
-    <el-col :span="20" :xs="24">
-      <ContentWrap>
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item label="设备类别" prop="deviceType">
-            <el-tree-select
-              v-model="formData.deviceType"
-              :data="productClassifyList"
-              :props="defaultProps"
-              check-strictly
-              node-key="id"
-              placeholder="请选择设备类别"
-              @change="assetclasschange"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item label="设备部件" prop="deviceComponent">
-            <el-input
-              v-model="queryParams.deviceComponent"
-              placeholder="请输入设备部件"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item label="创建人" prop="creName">
-            <el-input
-              v-model="queryParams.creName"
-              placeholder="请输入创建人"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item label="创建日期" prop="creDate">
-            <el-date-picker
-              v-model="queryParams.creDate"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="date"
-              start-placeholder="请选择"
-              :default-time="[new Date('1 00:00:00')]"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-operation-model:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> 新增
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-operation-model: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="所属组织" align="center" prop="orgName" />
-          <el-table-column label="设备类别" align="center" prop="deviceType" />
-          <el-table-column label="设备部件" align="center" prop="deviceComponent" />
-          <el-table-column label="填写信息" align="center" prop="fillInfo" />
-          <el-table-column label="创建人" align="center" prop="creName" />
-          <el-table-column
-            label="创建日期"
-            align="center"
-            prop="creDate"
-            :formatter="dateFormatter2"
-            width="180px"
-          />
-          <el-table-column label="操作" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-operation-model:update']"
-              >
-                编辑
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-operation-model:delete']"
-              >
-                删除
-              </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>
-
-
-    <!-- 表单弹窗:添加/修改 -->
-    <IotOpeationModelForm ref="formRef" @success="getList" :dept="deptInfo"/>
-  </el-row>
-</template>
-
-<script setup lang="ts">
-import { dateFormatter2 } from '@/utils/formatTime'
-import DeptTree from "@/views/system/user/DeptTree.vue";
-import download from '@/utils/download'
-import {defaultProps,handleTree} from "@/utils/tree";
-import { IotOpeationModelApi, IotOpeationModelVO } from '@/api/pms/iotopeationmodel'
-import IotOpeationModelForm from './IotOpeationModelForm.vue'
-import * as DeptApi from "@/api/system/dept";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import {IotDeviceApi} from "@/api/pms/device";
-import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
-
-/** 运行记录模板主 列表 */
-defineOptions({ name: 'IotOpeationModel' })
-
-const message = useMessage() // 消息弹窗
-
-const { t } = useI18n() // 国际化
-
-const loading = ref(true) // 列表的加载中
-const list = ref<IotOpeationModelVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const { push } = useRouter() // 路由跳转
-const deptList = ref<Tree[]>([]) // 树形结构
-const productClassifyList = ref<Tree[]>([]) // 树形结构
-const { params, name } = useRoute() // 查询参数
-const id = params.id
-const brandLabel = ref('') // 表单的类型:create - 新增;update - 修改
-const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
-const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
-
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  deptId: undefined,
-  orgName: undefined,
-  deviceType: undefined,
-  deviceComponent: undefined,
-  fillInfo: undefined,
-  creName: undefined,
-  creDate: [],
-})
-let deptInfo = {deptId:'',orgName:''};
-const formData = ref({
-  deviceType: undefined
-})
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await IotOpeationModelApi.getIotOpeationModelPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  //修改
-
-  formRef.value.open(type, id)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotOpeationModelApi.deleteIotOpeationModel(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await IotOpeationModelApi.exportIotOpeationModel(queryParams)
-    download.excel(data, '运行记录模板主.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
-const assetclasschange = () => {
-  const assetClass = formData.value.deviceType
-  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then(res => {
-    if (res){
-      res.forEach((item) => {
-        if (item.requiredFlag) {
-          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
-          item.rules = []
-          item.rules.push(rule)
-        }
-      })
-      list.value = res
-      debugger
-    } else {
-      list.value = []
-    }
-  })
-}
-
-onMounted(async () => {
-  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-  productClassifyList.value = handleTree(
-    await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
-  )
-  formData.value.assetProperty = 'zy'
-  // 修改时,设置数据
-  if (id) {
-    formType.value = 'update';
-    formLoading.value = true
-    try {
-      const iotDevice = await IotDeviceApi.getIotDevice(id);
-      formData.value = iotDevice
-      brandLabel.value = iotDevice.brandName;
-      zzLabel.value = iotDevice.zzName;
-      supplierLabel.value = iotDevice.supplierName;
-      list.value = JSON.parse(iotDevice.templateJson);
-      list.value.forEach((item) => {
-        formData.value[item.identifier] = item.value;
-      })
-    } finally {
-      formLoading.value = false
-    }
-  } else {
-    formType.value = 'create';
-  }
-})
-/** 处理部门被点击 */
-const handleDeptNodeClick = async (row) => {
-  queryParams.deptId = row.id
-  queryParams.orgName = row.name;
-  deptInfo.orgName = queryParams.orgName;
-  deptInfo.deptId = queryParams.deptId;
-  await getList()
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>

+ 8 - 1
src/views/pms/maintenance/IotMaintenancePlan.vue

@@ -61,6 +61,13 @@
     <!-- 列表 -->
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <!-- 添加序号列 -->
+        <el-table-column
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
         <el-table-column label="设备编码" align="center" prop="deviceCode" />
         <el-table-column label="设备名称" align="center" prop="deviceName" />
         <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
@@ -117,7 +124,7 @@
         <el-table-column label="操作" align="center" min-width="120px">
           <template #default="scope">
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
-                <div>
+              <div>
                 <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
                 <el-button
                   style="vertical-align: middle"

+ 726 - 0
src/views/pms/maintenance/IotMaintenancePlanDetail.vue

@@ -0,0 +1,726 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      v-loading="formLoading"
+      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
+      label-width="130px"
+    >
+      <div class="base-expandable-content">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item label="计划名称" prop="name">
+              <el-input type="text" v-model="formData.name" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="计划编号" prop="serialNumber">
+              <el-input type="text" v-model="formData.serialNumber" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <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="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" disabled/>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+  </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="warning">
+            <Icon icon="ep:plus" 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
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
+        <el-table-column label="设备编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column label="BOM节点" align="center" prop="name" />
+        <el-table-column label="运行里程" key="mileageRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.mileageRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        <!--
+        <el-table-column label="里程周期(H)" align="center" prop="nextRunningKilometers" :formatter="erpPriceTableColumnFormatter">
+          <template #default="scope">
+            <el-input v-model="scope.row.nextRunningKilometers" />
+          </template>
+        </el-table-column>
+        -->
+        <el-table-column label="运行时间" key="runningTimeRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.runningTimeRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        <!--
+        <el-table-column label="时间周期(H)" align="center" prop="nextRunningTime" :formatter="erpPriceTableColumnFormatter">
+          <template #default="scope">
+            <el-input v-model="scope.row.nextRunningTime" />
+          </template>
+        </el-table-column>
+        -->
+        <el-table-column label="自然日期" key="naturalDateRule" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.naturalDateRule"
+              :active-value="0"
+              :inactive-value="1"
+            />
+          </template>
+        </el-table-column>
+        <!--
+        <el-table-column label="自然日期(D)" align="center" prop="nextNaturalDate" :formatter="erpPriceTableColumnFormatter">
+          <template #default="scope">
+            <el-input v-model="scope.row.nextNaturalDate" />
+          </template>
+        </el-table-column>
+        -->
+        <el-table-column label="操作" align="center" min-width="120px">
+          <template #default="scope">
+            <div style="display: flex; justify-content: center; align-items: center; width: 100%">
+              <div>
+                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
+                <el-button
+                  style="vertical-align: middle"
+                  link
+                  type="danger"
+                  @click="handleDelete(scope.row.id+'-'+scope.row.bomNodeId)"
+                >
+                  移除
+                </el-button>
+              </div>
+              <!-- 新增配置按钮 -->
+              <div style="margin-left: 12px">
+                <el-button
+                  link
+                  type="primary"
+                  @click="openConfigDialog(scope.row)"
+                >
+                  配置
+                </el-button>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </ContentWrap>
+  </ContentWrap>
+  <ContentWrap>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">保 存</el-button>
+        <el-button @click="close">取 消</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <MainPlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
+  <!-- 新增配置对话框 -->
+  <el-dialog
+    v-model="configDialog.visible"
+    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
+    width="600px"
+  >
+    <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
+      <!-- 里程配置 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="上次保养里程数(KM)"
+        prop="lastRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 运行时间配置 -->
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="上次保养运行时间(H)"
+        prop="lastRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.lastRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <!-- 自然日期配置 -->
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="上次保养自然日期(D)"
+        prop="lastNaturalDate"
+      >
+        <el-date-picker
+          v-model="configDialog.form.lastNaturalDate"
+          type="date"
+          placeholder="选择日期"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+      <!-- 保养规则周期值 + 提前量 -->
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期(KM)"
+        prop="nextRunningKilometers"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningKilometers"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.mileageRule === 0"
+        label="运行里程周期-提前量(KM)"
+        prop="kiloCycleLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.kiloCycleLead"
+          :precision="2"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期(H)"
+        prop="nextRunningTime"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextRunningTime"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.runningTimeRule === 0"
+        label="运行时间周期-提前量(H)"
+        prop="timePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.timePeriodLead"
+          :precision="1"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期(D)"
+        prop="nextNaturalDate"
+      >
+        <el-input-number
+          v-model="configDialog.form.nextNaturalDate"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="configDialog.current?.naturalDateRule === 0"
+        label="自然日周期-提前量(D)"
+        prop="naturalDatePeriodLead"
+      >
+        <el-input-number
+          v-model="configDialog.form.naturalDatePeriodLead"
+          :min="0"
+          controls-position="right"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="configDialog.visible = false">取消</el-button>
+      <el-button type="primary" @click="saveConfig">保存</el-button>
+    </template>
+  </el-dialog>
+
+</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 { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
+import { IotMaintenancePlanApi, IotMaintenancePlanVO } from '@/api/pms/maintenance'
+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 {IotMainWorkOrderBomApi} from "@/api/pms/iotmainworkorderbom";
+
+/** 保养计划 表单 */
+defineOptions({ name: 'IotMaintenancePlanDetail' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
+const dept = ref() // 当前登录人所属部门对象
+const configFormRef = ref() // 配置弹出框对象
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const list = ref<IotMaintenanceBomVO[]>([]) // 设备bom关联列表的数据
+const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const formData = ref({
+  id: undefined,
+  deptId: undefined,
+  name: '',
+  serialNumber: undefined,
+  responsiblePerson: undefined,
+  remark: undefined,
+  failureName: undefined,
+  status: undefined,
+})
+const formRules = reactive({
+  name: [{ required: true, message: '计划名称不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+// 新增配置相关状态
+const configDialog = reactive({
+  visible: false,
+  current: null as IotMaintenanceBomVO | null,
+  form: {
+    lastRunningKilometers: 0,
+    lastRunningTime: 0,
+    lastNaturalDate: '',
+    // 保养规则 周期
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    // 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0
+  }
+})
+
+// 打开配置对话框
+const openConfigDialog = (row: IotMaintenanceBomVO) => {
+  configDialog.current = row
+
+  // 处理日期初始化(核心修改)
+  let initialDate = ''
+  if (row.lastNaturalDate) {
+    // 如果已有值:时间戳 -> 日期字符串
+    initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
+  } else {
+    // 如果无值:设置默认值避免1970问题
+    initialDate = ''
+  }
+
+  configDialog.form = {
+    lastRunningKilometers: row.lastRunningKilometers || 0,
+    lastRunningTime: row.lastRunningTime || 0,
+    lastNaturalDate: initialDate,
+    // 保养规则 周期值
+    nextRunningKilometers: row.nextRunningKilometers || 0,
+    nextRunningTime: row.nextRunningTime || 0,
+    nextNaturalDate: row.nextNaturalDate || 0,
+    // 提前量
+    kiloCycleLead: row.kiloCycleLead || 0,
+    timePeriodLead: row.timePeriodLead || 0,
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0
+  }
+  configDialog.visible = true
+}
+
+// 保存配置
+const saveConfig = () => {
+  (configFormRef.value as any).validate((valid: boolean) => {
+    if (!valid) return
+    if (!configDialog.current) return
+
+    // 动态校验逻辑
+    const requiredFields = []
+    if (configDialog.current.mileageRule === 0) {
+      requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
+    }
+    if (configDialog.current.runningTimeRule === 0) {
+      requiredFields.push('nextRunningTime', 'timePeriodLead')
+    }
+    if (configDialog.current.naturalDateRule === 0) {
+      requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
+    }
+
+    const missingFields = requiredFields.filter(field =>
+      !configDialog.form[field as keyof typeof configDialog.form]
+    )
+
+    if (missingFields.length > 0) {
+      message.error('请填写所有必填项')
+      return
+    }
+
+    // 强制校验逻辑
+    if (configDialog.current.naturalDateRule === 0) {
+      if (!configDialog.form.lastNaturalDate) {
+        message.error('必须选择自然日期')
+        return
+      }
+
+      // 验证日期有效性
+      const dateValue = dayjs(configDialog.form.lastNaturalDate)
+      if (!dateValue.isValid()) {
+        message.error('日期格式不正确')
+        return
+      }
+    }
+
+    // 转换逻辑(关键修改)
+    const finalDate = configDialog.form.lastNaturalDate
+      ? dayjs(configDialog.form.lastNaturalDate).valueOf()
+      : null // 改为null而不是0
+
+    // 更新当前行的数据
+    Object.assign(configDialog.current, {
+      ...configDialog.form,
+      lastNaturalDate: finalDate
+    })
+    configDialog.visible = false
+  })
+}
+
+const queryParams = reactive({
+  deviceIds: undefined
+})
+
+const deviceChoose = async(selectedDevices) => {
+  const newIds = selectedDevices.map(device => device.id)
+  deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
+  const params = {
+    deviceIds: deviceIds.value.join(',') // 明确传递数组参数
+  }
+  queryParams.deviceIds = JSON.parse(JSON.stringify(params.deviceIds))
+  // 根据选择的设备筛选出设备关系的分类BOM中与保养相关的节点项
+  const res = await IotDeviceApi.deviceAssociateBomList(queryParams)
+  const rawData = res || []
+  if(rawData.length === 0){
+    message.error('选择的设备不存在待保养BOM项')
+  }
+  if (!Array.isArray(rawData)) {
+    console.error('接口返回数据结构异常:', rawData)
+    return
+  }
+  // 转换数据结构(根据你的接口定义调整)
+  const newItems = rawData.map(device => ({
+    assetClass: device.assetClass,
+    deviceCode: device.deviceCode,
+    deviceName: device.deviceName,
+    deviceStatus: device.deviceStatus,
+    deptName: device.deptName,
+    name: device.name,
+    code: device.code,
+    assetProperty: device.assetProperty,
+    remark: null,    // 初始化备注
+    deviceId: device.id, // 移除操作需要
+    bomNodeId: device.bomNodeId,
+    totalRunTime: device.totalRunTime,
+    totalMileage: device.totalMileage,
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    lastNaturalDate: null, // 初始化为null而不是0
+    // 保养规则 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0
+  }))
+  // 获取选择的设备相关的id数组
+  newItems.forEach(item => {
+    deviceIds.value.push(item.deviceId)
+  })
+  // 合并到现有列表(去重)
+  newItems.forEach(item => {
+    const exists = list.value.some(
+      existing => (existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId)
+    )
+    if (!exists) {
+      list.value.push(item)
+    }
+  })
+}
+
+const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
+const openForm = () => {
+  deviceFormRef.value?.open();
+}
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotMaintenancePlan', params:{}})
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验表格数据
+  const isValid = validateTableData()
+  if (!isValid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      mainPlan: formData.value,
+      mainPlanBom: list.value
+    }
+    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 {
+    formLoading.value = false
+  }
+}
+
+// 新增表单校验规则
+const configFormRules = reactive({
+  nextRunningKilometers: [{
+    required: true,
+    message: '里程周期必须填写',
+    trigger: 'blur'
+  }],
+  kiloCycleLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextRunningTime: [{
+    required: true,
+    message: '时间周期必须填写',
+    trigger: 'blur'
+  }],
+  timePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }],
+  nextNaturalDate: [{
+    required: true,
+    message: '自然日周期必须填写',
+    trigger: 'blur'
+  }],
+  naturalDatePeriodLead: [{
+    required: true,
+    message: '提前量必须填写',
+    trigger: 'blur'
+  }]
+})
+
+/** 校验表格数据 */
+const validateTableData = (): boolean => {
+  let isValid = true
+  const errorMessages: string[] = []
+  const noRulesErrorMessages: string[] = []  // 未设置任何保养项规则 的错误提示信息
+  const noRules: string[] = []  // 行记录中设置了保养规则的记录数量
+  const configErrors: string[] = []   // 保养规则配置弹出框
+  let shouldBreak = false;
+
+  if (list.value.length === 0) {
+    errorMessages.push('请至少添加一条设备保养明细')
+    isValid = false
+    // 直接返回无需后续校验
+    message.error('请至少添加一条设备保养明细')
+    return isValid
+  }
+
+  list.value.forEach((row, index) => {
+    if (shouldBreak) return;
+    const rowNumber = index + 1 // 用户可见的行号从1开始
+    const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
+    // 校验逻辑
+    const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
+      if (ruleValue === 0) { // 规则开启
+        if (!row[configField] || row[configField] <= 0) {
+          configErrors.push(`第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`)
+          isValid = false
+        }
+      }
+    }
+    // 里程校验逻辑
+    if (row.mileageRule === 0) { // 假设 0 表示开启状态
+      if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
+        isValid = false
+      }
+      // 再校验配置值
+      checkConfig('里程', row.mileageRule, 'lastRunningKilometers')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置里程规则`)
+    }
+    // 运行时间校验逻辑
+    if (row.runningTimeRule === 0) {
+      if (!row.nextRunningTime || row.nextRunningTime <= 0) {
+        errorMessages.push(`第 ${rowNumber} 行:开启运行时间规则必须填写有效的时间周期`)
+        isValid = false
+      }
+      checkConfig('运行时间', row.runningTimeRule, 'lastRunningTime')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置运行时间规则`)
+    }
+    // 自然日期校验逻辑
+    if (row.naturalDateRule === 0) {
+      if (!row.nextNaturalDate) {
+        errorMessages.push(`第 ${rowNumber} 行:开启自然日期规则必须填写有效的自然日期周期`)
+        isValid = false
+      }
+      checkConfig('自然日期', row.naturalDateRule, 'lastNaturalDate')
+    } else {
+      noRules.push(`第 ${rowNumber} 行:未设置自然日期规则`)
+    }
+    // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
+    if (noRules.length === 3) {
+      isValid = false
+      shouldBreak = true; // 设置标志变量为true,退出循环
+      noRulesErrorMessages.push('保养项至少设置1个保养规则')
+    }
+    noRules.length = 0;
+  })
+  if (errorMessages.length > 0) {
+    message.error('设置保养规则后,请维护对应的周期值')
+  } else if (noRulesErrorMessages.length > 0) {
+    message.error(noRulesErrorMessages.pop())
+  } else if (configErrors.length > 0) {
+    message.error(configErrors.pop())
+  }
+  return isValid
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    failureName: undefined,
+    deviceId: undefined,
+    status: undefined,
+    ifStop: undefined,
+    failureTime: undefined,
+    failureInfluence: undefined,
+    failureSystem: undefined,
+    description: undefined,
+    pic: undefined,
+    solution: undefined,
+    maintainStartTime: undefined,
+    maintainEndTime: undefined,
+    remark: undefined,
+    deviceName: undefined,
+    processInstanceId: undefined,
+    auditStatus: undefined,
+    deptId: undefined
+  }
+  formRef.value?.resetFields()
+}
+onMounted(async () => {
+  const deptId = useUserStore().getUser.deptId
+  // 查询当前登录人所属部门名称
+  dept.value = await DeptApi.getDept(deptId)
+  // 根据当前登录人部门信息生成生成 保养计划 名称
+  formData.value.name = dept.value.name + ' - 保养计划'
+  deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+  formData.value.deptId = deptId
+  // if (id){
+    formType.value = 'update'
+    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.getWorkOrderBOMs(queryParams);
+  /* } else {
+    formType.value = 'create';
+    const { wsCache } = useCache()
+    const userInfo = wsCache.get(CACHE_KEY.USER)
+    formData.value.responsiblePerson = userInfo.user.id;
+  } */
+})
+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 {}
+}
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+</style>

+ 14 - 0
src/views/pms/maintenance/index.vue

@@ -107,6 +107,15 @@
               >
                 编辑
               </el-button>
+              <el-button
+                link
+                type="primary"
+                @click="detail(scope.row.id)"
+                v-hasPermi="['rq:iot-maintenance-plan:query']"
+              >
+                查看
+              </el-button>
+              <!--
               <el-button
                 link
                 type="danger"
@@ -115,6 +124,7 @@
               >
                 删除
               </el-button>
+              -->
             </template>
           </el-table-column>
         </el-table>
@@ -192,6 +202,10 @@ const resetQuery = () => {
   handleQuery()
 }
 
+const detail = (id?: number) => {
+  push({ name: 'IotMaintenancePlanDetail', params:{id} })
+}
+
 /** 添加/修改操作 */
 const openForm = (type: string, id?: number) => {
   //修改

+ 4 - 0
src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelForm.vue

@@ -131,7 +131,11 @@ const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number,name?:string) => {
+
   deviceCategoryName = name;
+  if(type=='update'){
+    await getAttrList();
+  }
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type

Vissa filer visades inte eftersom för många filer har ändrats