Zimo 1 vecka sedan
förälder
incheckning
f1038e4886

+ 32 - 33
src/api/pms/iotmaintenancebom/index.ts

@@ -8,71 +8,71 @@ export interface IotMaintenanceBomVO {
   deviceCategoryId: number // 所属设备分类
   deviceId: number // 设备id
   rule: string // 保养规则(里程 运行时间 自然日期) 可多选
-  mileageRule: number       // 保养规则-里程(0启用 1停用)
-  naturalDateRule: number   // 保养规则-自然日期(0启用 1停用)
-  runningTimeRule: number   // 保养规则-运行时间(0启用 1停用)
+  mileageRule: number // 保养规则-里程(0启用 1停用)
+  naturalDateRule: number // 保养规则-自然日期(0启用 1停用)
+  runningTimeRule: number // 保养规则-运行时间(0启用 1停用)
   lastRunningTime: number // 上次保养运行时长(小时)
   nextRunningTime: number // 下次保养运行时长(小时) 运行时长周期
   timePeriod: number // 时间周期(小时) 距离下次保养运行时长
   kilometerCycle: number // 公里数周期(千米) 距离下次保养公里数
   naturalDatePeriod: number // 自然日周期(天) 下次保养自然日期
-  timePeriodLead: number  // 运行时长周期提前量 H
+  timePeriodLead: number // 运行时长周期提前量 H
   lastRunningKilometers: number // 上次保养运行公里数(千米)
   nextRunningKilometers: number // 下次保养运行公里数(千米)
   kiloCycleLead: number // 公里数周期-提前量 km
   lastNaturalDate: Date // 上次保养自然日期(天)
-  tempLastNaturalDate: Date // 上次保养自然日期(天) 临时变量
-  nextNaturalDate: number // 下次保养自然日期(天)
+  tempLastNaturalDate: string | null // 上次保养自然日期(天) 临时变量
+  nextNaturalDate: number | null // 下次保养自然日期(天)
   naturalDatePeriodLead: number // 自然日周期-提前量(天)
   bomNodeId: number // bom节点id
   name: string // BOM名称
-  code: string // BOM编码
+  code: string | null // BOM编码
   parentId: number // 父BOM id 顶级为0
   childIds: string // 子节点id 逗号分隔
   level: number // 层级
   leafFlag: number // 是否叶子节点 0是 1否
   sort: number // 显示顺序
-  type: string // 1维修 2保养 维修+保养
+  type: string | null // 1维修 2保养 维修+保养
   status: number // 状态 0启用  1停用
   remark: string // 备注
   version: number // 版本
   // 扩展字段
-  deviceName: string  // 设备名称
-  deviceCode: string  // 设备编码
-  deviceStatus: string  // 设备状态
+  deviceName: string // 设备名称
+  deviceCode: string // 设备编码
+  deviceStatus: string // 设备状态
   assetProperty: string //资产性质
-  totalMileage: number  // 累计运行公里数
-  totalRunTime: number  // 累计运行时间
-  tempTotalMileage: number  // 临时 累计运行公里数
-  tempTotalRunTime: number  // 临时 累计运行时间
-  isRuntimeFromTemp: false
-  isMileageFromTemp: false
+  totalMileage: number | null // 累计运行公里数
+  totalRunTime: number | null // 累计运行时间
+  tempTotalMileage: number | null // 临时 累计运行公里数
+  tempTotalRunTime: number | null // 临时 累计运行时间
+  isRuntimeFromTemp: boolean
+  isMileageFromTemp: boolean
   // 运行记录模板中 包含多个 累计时长 属性列表
   timeAccumulatedAttrs: Array<{
-    pointName: string;
-    totalRunTime: number;
-    totalMileage: number;
-  }>;
+    pointName: string
+    totalRunTime: number
+    totalMileage: number
+  }>
   // 运行记录模板中 包含多个 累计公里数 属性列表
   mileageAccumulatedAttrs: Array<{
-    pointName: string;
-    totalRunTime: number;
-    totalMileage: number;
-  }>;
+    pointName: string
+    totalRunTime: number
+    totalMileage: number
+  }>
   // 上次保养时间 不同于自然日保养规则下的 上次保养自然日期
   lastMaintenanceDate: Date
   // 下次保养公里数
-  nextMaintenanceKm: number
+  nextMaintenanceKm: number | null
   // 剩余保养公里数
-  remainKm: number
+  remainKm: number | null
   // 下次保养运行时长
-  nextMaintenanceH: number
+  nextMaintenanceH: number | null
   // 剩余保养运行时长
-  remainH: number
+  remainH: number | null
   // 下次保养日期
-  nextMaintenanceDate: Date
+  nextMaintenanceDate: string | null
   // 自然日期保养 剩余天数
-  remainDay: number
+  remainDay: number | null
 }
 
 // PMS 保养计划明细BOM API
@@ -110,6 +110,5 @@ export const IotMaintenanceBomApi = {
   // 获得PMS 保养工单明细BOM列表
   getMainPlanBOMs: async (params: any) => {
     return await request.get({ url: `/pms/iot-maintenance-bom/getMainPlanBOMs`, params })
-  },
-
+  }
 }

+ 4 - 7
src/router/modules/remaining.ts

@@ -803,8 +803,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     component: Layout,
     name: 'PmsMaintenanceCenter',
     meta: {
-      hidden: true,
-      keepAlive: true
+      hidden: true
     },
     children: [
       {
@@ -823,10 +822,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/add',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlan.vue'),
+        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
         name: 'IotAddMainPlan',
         meta: {
-          keepAlive: true,
           noCache: false,
           hidden: true,
           canTo: true,
@@ -837,10 +835,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/edit/:id(\\d+)',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlanEdit.vue'),
+        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
         name: 'IotMainPlanEdit',
         meta: {
-          keepAlive: true,
           noCache: true,
           hidden: true,
           canTo: true,
@@ -851,7 +848,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/detail/:id(\\d+)',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlanDetail.vue'),
+        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
         name: 'IotMaintenancePlanDetail',
         meta: {
           noCache: true,

+ 1588 - 0
src/views/pms/maintenance/IotMaintenancePlanManage.vue

@@ -0,0 +1,1588 @@
+<template>
+  <div class="maintenance-plan-page" v-loading="formLoading">
+    <section class="plan-section plan-form-section">
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="88px"
+        class="plan-form">
+        <el-row :gutter="48">
+          <el-col :span="13">
+            <el-form-item :label="t('main.planName')" prop="name">
+              <el-input v-model="formData.name" :disabled="isReadonly" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="11">
+            <el-form-item :label="t('main.planCode')" prop="serialNumber">
+              <el-input v-model="formData.serialNumber" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item :label="t('iotMaintain.remark')" prop="remark">
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                :rows="2"
+                :placeholder="t('iotMaintain.remarkHolder')"
+                :disabled="isReadonly" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </section>
+
+    <section class="plan-section plan-table-section">
+      <div v-if="!isReadonly" class="table-toolbar">
+        <el-button @click="openForm" type="warning">
+          <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}
+        </el-button>
+      </div>
+
+      <ZmTable
+        ref="tableRef"
+        class="maintenance-plan-table"
+        :data="list"
+        :loading="loading"
+        :show-border="true"
+        :cell-class-name="cellClassName"
+        :cell-style="cellStyle"
+        :max-height="tableMaxHeight"
+        :column-max-width="420"
+        :highlight-current-row="false"
+        @header-dragend="handleHeaderDragEnd">
+        <ZmTableColumn
+          type="index"
+          :label="t('iotDevice.serial')"
+          :width="62"
+          fixed="left"
+          hide-in-column-settings />
+        <ZmTableColumn
+          prop="deviceCode"
+          :label="t('iotMaintain.deviceCode')"
+          :min-width="100"
+          fixed="left" />
+        <ZmTableColumn
+          prop="deviceName"
+          :label="t('iotMaintain.deviceName')"
+          :min-width="110"
+          fixed="left" />
+        <ZmTableColumn
+          prop="name"
+          :label="t('bomList.bomNode')"
+          :min-width="190"
+          fixed="left"
+          :show-overflow-tooltip="false">
+          <template #default="{ row }">
+            <div class="full-content-cell">{{ row.name }}</div>
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn prop="runningTimeRule" :label="t('main.runTime')" :width="92">
+          <template #default="{ row }">
+            <el-switch
+              v-model="row.runningTimeRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="isReadonly"
+              @change="handleRuleChange(row, 'runningTime')" />
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn prop="mileageRule" :label="t('main.mileage')" :width="92">
+          <template #default="{ row }">
+            <el-switch
+              v-model="row.mileageRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="isReadonly"
+              @change="handleRuleChange(row, 'mileage')" />
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn prop="naturalDateRule" :label="t('main.date')" :width="92">
+          <template #default="{ row }">
+            <el-switch
+              v-model="row.naturalDateRule"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="isReadonly"
+              @change="handleRuleChange(row, 'date')" />
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn
+          prop="totalRunTime"
+          :label="t('operationFillForm.sumTime')"
+          :min-width="130"
+          :real-value="(row) => row.totalRunTime ?? row.tempTotalRunTime">
+          <template #default="{ row }">
+            {{ row.totalRunTime ?? row.tempTotalRunTime ?? '-' }}
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn
+          prop="totalMileage"
+          :label="t('operationFillForm.sumKil')"
+          :min-width="150"
+          :real-value="(row) => row.totalMileage ?? row.tempTotalMileage">
+          <template #default="{ row }">
+            {{ row.totalMileage ?? row.tempTotalMileage ?? '-' }}
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn
+          prop="lastMaintenanceDate"
+          :label="t('mainPlan.lastMaintenanceDate')"
+          :min-width="126"
+          :real-value="(row) => formatDate(row.lastMaintenanceDate)">
+          <template #default="{ row }">
+            {{ formatDate(row.lastMaintenanceDate) }}
+          </template>
+        </ZmTableColumn>
+
+        <ZmTableColumn v-if="hasTimeRule" column-key="time-group" label="保养时长" is-parent>
+          <ZmTableColumn
+            prop="lastRunningTime"
+            :label="t('mainPlan.lastMaintenanceOperationTime')"
+            :min-width="150" />
+          <ZmTableColumn
+            prop="nextMaintenanceH"
+            :label="t('mainPlan.nextMaintenanceH')"
+            :min-width="150">
+            <template #default="{ row }">{{ row.nextMaintenanceH ?? '-' }}</template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="remainH" :label="t('mainPlan.remainH')" :min-width="120">
+            <template #default="{ row }">{{ row.remainH ?? '-' }}</template>
+          </ZmTableColumn>
+        </ZmTableColumn>
+
+        <ZmTableColumn v-if="hasMileageRule" column-key="mileage-group" label="保养里程" is-parent>
+          <ZmTableColumn
+            prop="lastRunningKilometers"
+            :label="t('mainPlan.lastMaintenanceMileage')"
+            :min-width="150" />
+          <ZmTableColumn
+            prop="nextMaintenanceKm"
+            :label="t('mainPlan.nextMaintenanceKm')"
+            :min-width="150">
+            <template #default="{ row }">{{ row.nextMaintenanceKm ?? '-' }}</template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="remainKm" :label="t('mainPlan.remainKm')" :min-width="120">
+            <template #default="{ row }">{{ row.remainKm ?? '-' }}</template>
+          </ZmTableColumn>
+        </ZmTableColumn>
+
+        <ZmTableColumn v-if="hasDateRule" column-key="date-group" label="保养日期" is-parent>
+          <ZmTableColumn
+            prop="lastNaturalDate"
+            :label="t('mainPlan.lastMaintenanceNaturalDate')"
+            :min-width="150"
+            :real-value="(row) => formatDate(row.lastNaturalDate)">
+            <template #default="{ row }">{{ formatDate(row.lastNaturalDate) }}</template>
+          </ZmTableColumn>
+          <ZmTableColumn
+            prop="nextMaintenanceDate"
+            :label="t('mainPlan.nextMaintDate')"
+            :min-width="140">
+            <template #default="{ row }">{{ row.nextMaintenanceDate ?? '-' }}</template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="remainDay" :label="t('mainPlan.remainDay')" :min-width="120">
+            <template #default="{ row }">{{ row.remainDay ?? '-' }}</template>
+          </ZmTableColumn>
+        </ZmTableColumn>
+
+        <ZmTableColumn
+          column-key="operation"
+          :label="t('operationFill.operation')"
+          :width="150"
+          fixed="right"
+          action>
+          <template #default="{ row }">
+            <div class="table-actions">
+              <el-button v-if="!isReadonly" link type="danger" @click="handleDelete(row)">
+                <Icon icon="ep:zoom-out" class="mr-3px" />
+                {{ t('modelTemplate.delete') }}
+              </el-button>
+              <el-button link type="primary" @click="openConfigDialog(row)">
+                {{ isReadonly ? t('form.set') : t('modelTemplate.update') }}
+              </el-button>
+            </div>
+          </template>
+        </ZmTableColumn>
+      </ZmTable>
+    </section>
+
+    <section class="plan-footer">
+      <el-button v-if="!isReadonly" type="primary" @click="submitForm" :disabled="formLoading">
+        {{ t('iotMaintain.save') }}
+      </el-button>
+      <el-button @click="close">
+        {{ isReadonly ? t('operationFillForm.cancel') : t('iotMaintain.cancel') }}
+      </el-button>
+    </section>
+  </div>
+  <MainPlanDeviceList v-if="!isReadonly" ref="deviceFormRef" @choose="deviceChoose" />
+  <!-- 新增配置对话框 -->
+  <el-dialog
+    v-model="configDialog.visible"
+    :title="`设备 ${configDialog.current?.deviceCode + '-' + configDialog.current?.name} 保养配置`"
+    width="600px"
+    :close-on-click-modal="false">
+    <!-- 使用header插槽自定义标题 -->
+    <template #header>
+      <span
+        >设备
+        <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong>
+        保养项配置</span
+      >
+    </template>
+    <el-form
+      :model="configDialog.form"
+      label-width="200px"
+      :rules="configFormRules"
+      ref="configFormRef">
+      <div class="form-group">
+        <div class="group-title">{{ t('mainPlan.basicMaintenanceRecords') }}</div>
+        <!-- 里程配置 -->
+        <el-form-item
+          v-if="configDialog.current?.mileageRule === 0"
+          :label="t('mainPlan.lastMaintenanceMileage')"
+          prop="lastRunningKilometers">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.lastRunningKilometers"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+        <!-- 运行时间配置 -->
+        <el-form-item
+          v-if="configDialog.current?.runningTimeRule === 0"
+          :label="t('mainPlan.lastMaintenanceOperationTime')"
+          prop="lastRunningTime">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.lastRunningTime"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+        <!-- 自然日期配置 -->
+        <el-form-item
+          v-if="configDialog.current?.naturalDateRule === 0"
+          :label="t('mainPlan.lastMaintenanceNaturalDate')"
+          prop="lastNaturalDate">
+          <el-date-picker
+            :disabled="isReadonly"
+            v-model="configDialog.form.lastNaturalDate"
+            type="date"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="x"
+            style="width: 60%" />
+        </el-form-item>
+      </div>
+
+      <div class="form-group" v-if="configDialog.current?.mileageRule === 0">
+        <div class="group-title">{{ t('mainPlan.operatingMileageRuleConfiguration') }}</div>
+        <!-- 保养规则周期值 + 提前量 -->
+        <el-form-item
+          v-if="configDialog.current?.mileageRule === 0"
+          :label="t('mainPlan.operatingMileageCycle')"
+          prop="nextRunningKilometers">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.nextRunningKilometers"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.mileageRule === 0"
+          :label="t('mainPlan.OperatingMileageCycle_lead')"
+          prop="kiloCycleLead">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.kiloCycleLead"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+      </div>
+
+      <div class="form-group" v-if="configDialog.current?.runningTimeRule === 0">
+        <div class="group-title">{{ t('mainPlan.RunTimeRuleConfiguration') }}</div>
+        <el-form-item
+          v-if="configDialog.current?.runningTimeRule === 0"
+          :label="t('mainPlan.RunTimeCycle')"
+          prop="nextRunningTime">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.nextRunningTime"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.runningTimeRule === 0"
+          :label="t('mainPlan.RunTimeCycle_Lead')"
+          prop="timePeriodLead">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.timePeriodLead"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+      </div>
+
+      <div class="form-group" v-if="configDialog.current?.naturalDateRule === 0">
+        <div class="group-title">{{ t('mainPlan.NaturalDayRuleConfig') }}</div>
+        <el-form-item
+          v-if="configDialog.current?.naturalDateRule === 0"
+          :label="t('mainPlan.NaturalDailyCycle')"
+          prop="nextNaturalDate">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.nextNaturalDate"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.naturalDateRule === 0"
+          :label="t('mainPlan.NaturalDailyCycle_Lead')"
+          prop="naturalDatePeriodLead">
+          <el-input-number
+            :disabled="isReadonly"
+            v-model="configDialog.form.naturalDatePeriodLead"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%" />
+        </el-form-item>
+      </div>
+
+      <!-- 运行记录模板中 多个 累计运行时长 累计运行里程 属性匹配-->
+      <div
+        class="form-group"
+        v-if="
+          (configDialog.current?.runningTimeRule === 0 ||
+            configDialog.current?.mileageRule === 0) &&
+          (configDialog.current?.timeAccumulatedAttrs?.length ||
+            configDialog.current?.mileageAccumulatedAttrs?.length) &&
+          (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime)) &&
+          (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
+        ">
+        <div class="group-title">{{ t('mainPlan.accumulatedParams') }}</div>
+        <!-- 累计运行时长 -->
+        <el-form-item
+          v-if="
+            configDialog.current?.runningTimeRule === 0 &&
+            configDialog.current?.timeAccumulatedAttrs?.length &&
+            (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime))
+          "
+          :label="t('mainPlan.accumulatedRunTime')"
+          prop="accumulatedTimeOption"
+          :rules="[
+            {
+              required: Boolean(
+                configDialog.current?.runningTimeRule === 0 &&
+                  configDialog.current?.timeAccumulatedAttrs?.length &&
+                  (configDialog.current.totalRunTime === null ||
+                    isNaN(configDialog.current.totalRunTime))
+              ),
+              message: '请选择累计运行时长',
+              trigger: 'change'
+            }
+          ]">
+          <el-select
+            :disabled="isReadonly"
+            v-model="configDialog.form.accumulatedTimeOption"
+            placeholder="请选择累计运行时长"
+            style="width: 80%"
+            clearable
+            @change="handleAccumulatedTimeChange">
+            <el-option
+              v-for="(item, index) in configDialog.current.timeAccumulatedAttrs"
+              :key="`time-${item.pointName}-${index}`"
+              :label="item.pointName"
+              :value="item.pointName" />
+          </el-select>
+        </el-form-item>
+        <!-- 累计运行公里数 -->
+        <el-form-item
+          v-if="
+            configDialog.current?.mileageRule === 0 &&
+            configDialog.current?.mileageAccumulatedAttrs?.length &&
+            (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
+          "
+          :label="t('mainPlan.accumulatedMileage')"
+          prop="accumulatedMileageOption"
+          :rules="[
+            {
+              required: Boolean(
+                configDialog.current?.mileageRule === 0 &&
+                  configDialog.current?.mileageAccumulatedAttrs?.length &&
+                  (configDialog.current.totalMileage == null ||
+                    isNaN(configDialog.current.totalMileage))
+              ),
+              message: '请选择累计运行公里数',
+              trigger: 'change'
+            }
+          ]">
+          <el-select
+            :disabled="isReadonly"
+            v-model="configDialog.form.accumulatedMileageOption"
+            placeholder="请选择累计运行公里数"
+            style="width: 80%"
+            clearable
+            @change="handleAccumulatedMileageChange">
+            <el-option
+              v-for="(item, index) in configDialog.current.mileageAccumulatedAttrs"
+              :key="`mileage-${item.pointName}-${index}`"
+              :label="item.pointName"
+              :value="item.pointName" />
+          </el-select>
+        </el-form-item>
+      </div>
+    </el-form>
+    <template #footer>
+      <el-button @click="configDialog.visible = false">{{ t('common.cancel') }}</el-button>
+      <el-button v-if="!isReadonly" type="primary" @click="saveConfig">{{
+        t('common.save')
+      }}</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script setup lang="ts">
+import { IotDeviceApi } from '@/api/pms/device'
+import { useUserStore } from '@/store/modules/user'
+import { ref, computed, watch } from 'vue'
+import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
+import { IotMaintenancePlanApi } 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 { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import dayjs from 'dayjs'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const route = useRoute()
+const mode = computed(() => {
+  if (route.name === 'IotMaintenancePlanDetail') return 'detail'
+  if (route.name === 'IotMainPlanEdit') return 'edit'
+  return 'create'
+})
+const isReadonly = computed(() => mode.value === 'detail')
+const tableMaxHeight = computed(() =>
+  isReadonly.value ? 'calc(100vh - 360px)' : 'calc(100vh - 390px)'
+)
+const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintenanceBomVO>()
+const dept = ref() // 当前登录人所属部门对象
+const configFormRef = ref() // 配置弹出框对象
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const list = ref<IotMaintenanceBomVO[]>([]) // 设备bom关联列表的数据
+const loading = ref(false)
+
+const lastNaturalDateWatchers = ref(new Map())
+
+const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
+
+const tableRef = ref()
+
+const cellStyle: any = ({ row, column }: any) => {
+  if (['remainH', 'remainKm', 'remainDay'].includes(column.property)) {
+    if (row[column.property] < 0) {
+      return {
+        color: 'var(--el-color-danger)'
+      }
+    }
+  }
+}
+
+const id = computed(() => route.params.id as string | undefined)
+const formData = ref<any>({
+  id: undefined,
+  deptId: undefined,
+  name: '',
+  serialNumber: undefined,
+  responsiblePerson: undefined,
+  remark: undefined,
+  failureName: undefined,
+  status: undefined,
+  devicePersons: ''
+})
+
+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: null,
+    // 保养规则 周期
+    nextRunningKilometers: 0,
+    nextRunningTime: 0,
+    nextNaturalDate: 0,
+    // 提前量
+    kiloCycleLead: 0,
+    timePeriodLead: 0,
+    naturalDatePeriodLead: 0,
+    // 多个累计时长 累计里程 匹配
+    accumulatedTimeOption: null, // 累计运行时长选项
+    accumulatedMileageOption: null // 累计运行公里数选项
+  } as any
+})
+
+// 打开配置对话框
+const openConfigDialog = (row: IotMaintenanceBomVO) => {
+  // 新增规则校验:至少一个规则开启
+  if (row.mileageRule !== 0 && row.runningTimeRule !== 0 && row.naturalDateRule !== 0) {
+    message.error('请先设置保养规则')
+    return
+  }
+
+  configDialog.current = row
+
+  configDialog.form = {
+    lastRunningKilometers: row.lastRunningKilometers || 0,
+    lastRunningTime: row.lastRunningTime || 0,
+    lastNaturalDate: row.lastNaturalDate || null,
+    // 保养规则 周期值
+    nextRunningKilometers: row.nextRunningKilometers || 0,
+    nextRunningTime: row.nextRunningTime || 0,
+    nextNaturalDate: row.nextNaturalDate || 0,
+    // 提前量
+    kiloCycleLead: row.kiloCycleLead || 0,
+    timePeriodLead: row.timePeriodLead || 0,
+    naturalDatePeriodLead: row.naturalDatePeriodLead || 0,
+    // 多个累计时长 累计里程 匹配
+    accumulatedTimeOption: null, // 累计运行时长选项
+    accumulatedMileageOption: null // 累计运行公里数选项
+  }
+
+  // 初始化累计参数选择
+  /* configDialog.form.accumulatedTimeOption = row.isRuntimeFromTemp
+    ? row.code
+    : null;
+
+  configDialog.form.accumulatedMileageOption = row.isMileageFromTemp
+    ? row.type
+    : null; */
+
+  configDialog.form.accumulatedTimeOption = row.code || null
+  configDialog.form.accumulatedMileageOption = row.type || null
+
+  configDialog.visible = true
+}
+
+// 列宽调整后的处理
+const handleHeaderDragEnd = () => {
+  nextTick(() => {
+    tableRef.value?.elTableRef?.doLayout?.()
+  })
+}
+
+// 保存配置
+const saveConfig = () => {
+  if (isReadonly.value) {
+    configDialog.visible = false
+    return
+  }
+  ;(configFormRef.value as any).validate((valid: boolean) => {
+    if (!valid) return
+    if (!configDialog.current) return
+
+    // 累计运行时长配置 校验逻辑
+    if (
+      configDialog.current.runningTimeRule === 0 &&
+      configDialog.current.timeAccumulatedAttrs?.length &&
+      !configDialog.form.accumulatedTimeOption &&
+      (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime))
+    ) {
+      message.error('请选择累计运行时长')
+      return
+    }
+    // 累计运行公里数配置 校验逻辑
+    if (
+      configDialog.current.mileageRule === 0 &&
+      configDialog.current.mileageAccumulatedAttrs?.length &&
+      !configDialog.form.accumulatedMileageOption &&
+      (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
+    ) {
+      message.error('请选择累计运行公里数')
+      return
+    }
+
+    // 动态校验逻辑
+    const requiredFields: any = []
+    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(Number(configDialog.form.lastNaturalDate))
+      if (!dateValue.isValid()) {
+        message.error('日期格式不正确')
+        return
+      }
+    }
+
+    // 转换逻辑(关键修改)
+    const finalDate = configDialog.form.lastNaturalDate
+      ? Number(configDialog.form.lastNaturalDate)
+      : null // 改为null而不是0
+
+    const updateData = {
+      ...configDialog.form,
+      lastNaturalDate: finalDate
+    }
+
+    // 关闭保养规则,无论是否有选择值,都清除相关数据
+    // 处理累计运行时长
+    if (configDialog.current.runningTimeRule !== 0) {
+      configDialog.current.code = null
+      configDialog.current.totalRunTime = null
+      configDialog.form.accumulatedTimeOption = null // 清除选择值
+    } else if (configDialog.form.accumulatedTimeOption) {
+      // 查找选中的累计运行时长项
+      const selectedTimeOption = configDialog.current.timeAccumulatedAttrs?.find(
+        (item) => item.pointName === configDialog.form.accumulatedTimeOption
+      )
+      if (selectedTimeOption) {
+        configDialog.current.code = selectedTimeOption.pointName
+        // 优先使用接口值,没有则使用临时值
+        configDialog.current.tempTotalRunTime = selectedTimeOption.totalRunTime
+        configDialog.current.isRuntimeFromTemp = true
+        // 只有接口未提供值时才使用临时值
+        if (!configDialog.current.totalRunTime) {
+        }
+      }
+    }
+    // 处理累计运行公里数
+    if (configDialog.current.mileageRule !== 0) {
+      configDialog.current.type = null
+      configDialog.current.totalMileage = null
+      configDialog.form.accumulatedMileageOption = null // 清除选择值
+    } else if (configDialog.form.accumulatedMileageOption) {
+      // 查找选中的累计运行公里数项
+      const selectedMileageOption = configDialog.current.mileageAccumulatedAttrs?.find(
+        (item) => item.pointName === configDialog.form.accumulatedMileageOption
+      )
+      if (selectedMileageOption) {
+        configDialog.current.type = selectedMileageOption.pointName
+        configDialog.current.tempTotalMileage = selectedMileageOption.totalRunTime
+        configDialog.current.isMileageFromTemp = true
+        if (!configDialog.current.totalMileage) {
+        }
+      }
+    }
+
+    // 更新当前行的数据
+    if (configDialog.current) {
+      Object.assign(configDialog.current, updateData)
+      // 重新计算 下次保养公里数 剩余公里数
+      configDialog.current.nextMaintenanceKm = calculateNextMaintenanceKm(configDialog.current)
+      configDialog.current.remainKm = calculateRemainKm(configDialog.current)
+      // 重新计算 下次保养运行时长 剩余时长
+      configDialog.current.nextMaintenanceH = calculateNextMaintenanceH(configDialog.current)
+      configDialog.current.remainH = calculateRemainH(configDialog.current)
+      // 重新计算 下次保养日期 剩余天数
+      if (configDialog.form.lastNaturalDate) {
+        configDialog.current.tempLastNaturalDate = dayjs(configDialog.form.lastNaturalDate).format(
+          'YYYY-MM-DD'
+        )
+        configDialog.current.nextMaintenanceDate = calculateNextMaintenanceDate(
+          configDialog.current
+        )
+        configDialog.current.remainDay = calculateRemainDay(configDialog.current)
+      }
+    }
+    configDialog.visible = false
+  })
+}
+
+const queryParams = reactive<any>({
+  deviceIds: undefined,
+  planId: undefined,
+  bomFlag: 'b'
+})
+
+// 处理保养规则变化 取消保养规则 时 清空已经设置的相应保养规则数据
+const handleRuleChange = (
+  row: IotMaintenanceBomVO,
+  ruleType: 'mileage' | 'runningTime' | 'date'
+) => {
+  // 当规则关闭时(inactive-value=1)
+  console.log('执行了保养规则变化事件' + row.totalRunTime + ' - ' + row.totalMileage)
+  // 当前保养项行已经返回了 totalRunTime totalMileage 数据 不需要再清空 累计运行时长 累计公里数
+
+  // 选择完设备匹配了保养项后 不能直接置空 因为可能是正常的累计时长 累计公里数
+  if (ruleType === 'runningTime' && row.runningTimeRule === 1) {
+    // 清除临时来源的值
+    if (row.isRuntimeFromTemp) {
+      row.totalRunTime = null
+      row.tempTotalRunTime = null
+      row.code = null
+      // row.isRuntimeFromTemp = false;
+    }
+    // 强制清除配置对话框中的值(如果打开的是当前行)
+    if (
+      configDialog.current?.deviceId === row.deviceId &&
+      configDialog.current?.bomNodeId === row.bomNodeId
+    ) {
+      configDialog.form.accumulatedTimeOption = null
+    }
+  } else if (ruleType === 'mileage' && row.mileageRule === 1) {
+    if (row.isMileageFromTemp) {
+      row.totalMileage = null
+      row.tempTotalMileage = null
+      row.type = null
+      // row.isMileageFromTemp = false;
+    }
+    // 强制清除配置对话框中的值(如果打开的是当前行)
+    if (
+      configDialog.current?.deviceId === row.deviceId &&
+      configDialog.current?.bomNodeId === row.bomNodeId
+    ) {
+      configDialog.form.accumulatedMileageOption = null
+    }
+  }
+
+  // 如果配置对话框打开的是当前行,同步清除对话框中的选择值
+  if (
+    configDialog.visible &&
+    configDialog.current &&
+    configDialog.current.deviceId === row.deviceId &&
+    configDialog.current.bomNodeId === row.bomNodeId
+  ) {
+    if (ruleType === 'runningTime') {
+      configDialog.form.accumulatedTimeOption = null
+    } else if (ruleType === 'mileage') {
+      configDialog.form.accumulatedMileageOption = null
+    }
+  }
+
+  // 规则变化后按新条件重新计算 下次保养公里数 剩余公里数
+  if (ruleType === 'mileage') {
+    if (row.mileageRule === 0) {
+      row.nextMaintenanceKm = calculateNextMaintenanceKm(row)
+      row.remainKm = calculateRemainKm(row)
+    } else {
+      row.nextMaintenanceKm = null
+      row.remainKm = null
+    }
+  }
+
+  // 规则变化后按新条件重新计算 下次保养时长 剩余时长
+  if (ruleType === 'runningTime') {
+    if (row.runningTimeRule === 0) {
+      row.nextMaintenanceH = calculateNextMaintenanceH(row)
+      row.remainH = calculateRemainH(row)
+    } else {
+      row.nextMaintenanceH = null
+      row.remainH = null
+    }
+  }
+
+  // 规则变化后按新条件重新计算 下次保养日期 剩余天数
+  if (ruleType === 'date') {
+    if (row.naturalDateRule === 0) {
+      row.nextMaintenanceDate = calculateNextMaintenanceDate(row)
+      row.remainDay = calculateRemainDay(row)
+    } else {
+      row.nextMaintenanceDate = null
+      row.remainDay = null
+    }
+  }
+}
+
+const deviceChoose = async (selectedDevices) => {
+  const newIds = selectedDevices.map((device) => device.id)
+  deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
+  const params = {
+    deviceIds: newIds.join(',') // 明确传递数组参数
+  }
+  queryParams.deviceIds = JSON.parse(JSON.stringify(params.deviceIds))
+  queryParams.bomFlag = 'b'
+  // 根据选择的设备筛选出设备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 existingKeys = new Set(list.value.map((item) => `${item.deviceId}-${item.bomNodeId}`))
+
+  // 转换数据结构(根据你的接口定义调整)
+  const newItems = rawData
+    .filter((device) => {
+      // 排除已存在的项(设备ID+bom节点ID)
+      const key = `${device.id}-${device.bomNodeId}`
+      return !existingKeys.has(key)
+    })
+    .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,
+      tempTotalRunTime: null,
+      tempTotalMileage: null,
+      isRuntimeFromTemp: false,
+      isMileageFromTemp: false,
+      // 添加累计时长参数列表 属性
+      timeAccumulatedAttrs: device.timeAccumulatedAttrs || [],
+      // 添加累计里程参数列表 属性
+      mileageAccumulatedAttrs: device.mileageAccumulatedAttrs || []
+    }))
+  // 获取选择的设备相关的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 as any)
+    }
+  })
+  // 排序保养项
+  applySorting()
+}
+
+// 计算下次保养公里数(通用函数)
+const calculateNextMaintenanceKm = (row: IotMaintenanceBomVO) => {
+  // 验证条件:规则开启 + 两个值都存在且 > 0
+  const isValid =
+    row.mileageRule === 0 && row.lastRunningKilometers > 0 && row.nextRunningKilometers > 0
+
+  return isValid ? row.lastRunningKilometers + row.nextRunningKilometers : null // 不满足条件返回null
+}
+
+// 计算剩余保养公里数(通用函数)
+const calculateRemainKm = (row: IotMaintenanceBomVO) => {
+  // 确定使用的里程值(优先totalMileage)
+  const mileageValue = row.totalMileage ?? row.tempTotalMileage ?? 0
+  // 验证条件:规则开启 + 3个值都存在且 > 0
+  const isValid =
+    row.mileageRule === 0 &&
+    row.lastRunningKilometers > 0 &&
+    mileageValue > 0 &&
+    row.nextRunningKilometers > 0
+
+  return isValid
+    ? parseFloat(
+        (row.nextRunningKilometers - (mileageValue - row.lastRunningKilometers)).toFixed(2)
+      )
+    : null // 不满足条件返回null
+}
+
+// 计算下次保养运行时长(通用函数)
+const calculateNextMaintenanceH = (row: IotMaintenanceBomVO) => {
+  // 验证条件:规则开启 + 两个值都存在且 > 0
+  const isValid = row.runningTimeRule === 0 && row.lastRunningTime > 0 && row.nextRunningTime > 0
+
+  return isValid ? row.lastRunningTime + row.nextRunningTime : null // 不满足条件返回null
+}
+
+// 计算剩余运行时间(通用函数)
+const calculateRemainH = (row: IotMaintenanceBomVO) => {
+  // 确定使用的 运行时长 值(优先 totalRunTime)
+  const runTimeValue = row.totalRunTime ?? row.tempTotalRunTime ?? 0
+  // 验证条件:规则开启 + 3个值都存在且 > 0
+  const isValid =
+    row.runningTimeRule === 0 &&
+    row.lastRunningTime > 0 &&
+    runTimeValue > 0 &&
+    row.nextRunningTime > 0
+
+  return isValid
+    ? parseFloat((row.nextRunningTime - (runTimeValue - row.lastRunningTime)).toFixed(2))
+    : null // 不满足条件返回null
+}
+
+// 计算下次保养日期(通用函数)
+const calculateNextMaintenanceDate = (row: IotMaintenanceBomVO) => {
+  // 验证条件:规则开启 + 两个值都存在且 > 0
+  const isValid = row.naturalDateRule === 0 && row.lastNaturalDate && row.nextNaturalDate
+
+  return isValid
+    ? dayjs(row.lastNaturalDate)
+        .add(row.nextNaturalDate as any, 'day')
+        .format('YYYY-MM-DD')
+    : null // 不满足条件返回null
+}
+
+// 计算 自然日期保养 剩余天数(通用函数)
+const calculateRemainDay = (row: IotMaintenanceBomVO) => {
+  // 验证条件:规则开启 + 两个值都存在且有效
+  const isValid =
+    row.naturalDateRule === 0 &&
+    row.lastNaturalDate !== null &&
+    row.nextNaturalDate !== null &&
+    row.nextNaturalDate > 0
+
+  if (!isValid) {
+    return null
+  }
+
+  try {
+    // 上次保养日期:将时间戳转换为 Day.js 对象
+    const lastNaturalDate = dayjs(row.lastNaturalDate)
+
+    // 计算下次保养日期
+    const nextMaintenanceDate = lastNaturalDate.add(row.nextNaturalDate as any, 'day')
+
+    // 计算剩余天数(当前日期到下次保养日期的天数差)
+    return nextMaintenanceDate.diff(dayjs(), 'day')
+  } catch (error) {
+    console.error('计算保养剩余天数错误:', error)
+    return null
+  }
+}
+
+const formatDate = (value?: number | string | null) => {
+  if (!value) return '-'
+  const date = dayjs(Number(value))
+  return date.isValid() ? date.format('YYYY-MM-DD') : '-'
+}
+
+// 单元格类名回调方法
+const cellClassName = ({ row, column }) => {
+  // 只对序号列进行处理
+  if (column.type === 'index') {
+    // 检查该行所有启用的规则是否都已配置完整
+    if (checkRowFilled(row)) {
+      return 'all-filled' // 返回自定义类名
+    }
+  }
+  return ''
+}
+
+// 检查行数据是否完整填写
+const checkRowFilled = (row: IotMaintenanceBomVO) => {
+  // 检查是否启用了至少一个规则
+  const hasRuleEnabled =
+    row.mileageRule === 0 || row.runningTimeRule === 0 || row.naturalDateRule === 0
+
+  if (!hasRuleEnabled) {
+    return false // 没有任何规则启用,不显示背景色
+  }
+  // 检查里程规则
+  const mileageFilled =
+    row.mileageRule !== 0
+      ? true // 规则未启用,视为已"填写"
+      : row.lastRunningKilometers > 0 &&
+        row.nextRunningKilometers > 0 &&
+        row.kiloCycleLead > 0 &&
+        // 检查累计里程参数是否已选择(当条件满足时)
+        (!(
+          row.mileageAccumulatedAttrs?.length &&
+          (row.totalMileage == null || isNaN(row.totalMileage))
+        ) ||
+          (row.mileageAccumulatedAttrs?.length &&
+            (row.totalMileage == null || isNaN(row.totalMileage)) &&
+            row.type))
+
+  // 检查运行时间规则
+  const runningTimeFilled =
+    row.runningTimeRule !== 0
+      ? true
+      : row.lastRunningTime > 0 &&
+        row.nextRunningTime > 0 &&
+        row.timePeriodLead > 0 &&
+        // 检查累计时间参数是否已选择(当条件满足时)
+        (!(
+          row.timeAccumulatedAttrs?.length &&
+          (row.totalRunTime == null || isNaN(row.totalRunTime))
+        ) ||
+          (row.timeAccumulatedAttrs?.length &&
+            (row.totalRunTime == null || isNaN(row.totalRunTime)) &&
+            row.code))
+
+  // 检查自然日期规则
+  const naturalDateFilled =
+    row.naturalDateRule !== 0
+      ? true
+      : row.lastNaturalDate && (row.nextNaturalDate ?? 0) > 0 && row.naturalDatePeriodLead > 0
+
+  return mileageFilled && runningTimeFilled && naturalDateFilled
+}
+
+// 计算属性 - 检查是否有开启的里程规则
+const hasMileageRule = computed(() => {
+  return list.value.some((row) => row.mileageRule === 0)
+})
+
+// 计算属性 - 检查是否有开启的运行时间规则
+const hasTimeRule = computed(() => {
+  return list.value.some((row) => row.runningTimeRule === 0)
+})
+
+// 计算属性 - 检查是否有开启的自然日期规则
+const hasDateRule = computed(() => {
+  return list.value.some((row) => row.naturalDateRule === 0)
+})
+
+// 为每一行建立lastNaturalDate到tempLastNaturalDate的同步
+const setupNaturalDateSync = (row: IotMaintenanceBomVO) => {
+  // 如果该行已有watcher则跳过
+  if (lastNaturalDateWatchers.value.has(row.id)) return
+
+  // 为该行创建单独的watcher
+  const unwatch = watch(
+    () => row.lastNaturalDate,
+    (newVal) => {
+      // 转换日期格式 (时间戳 -> YYYY-MM-DD)
+      row.tempLastNaturalDate = newVal ? dayjs(newVal).format('YYYY-MM-DD') : null
+    },
+    { immediate: true, deep: true }
+  )
+
+  // 保存watcher用于后续清理
+  lastNaturalDateWatchers.value.set(row.id, unwatch)
+}
+
+// 监听规则列变化,重新布局表格
+watch(
+  [list, hasMileageRule, hasTimeRule, hasDateRule],
+  () => {
+    nextTick(() => {
+      tableRef.value?.elTableRef?.doLayout?.()
+    })
+  },
+  { deep: true }
+)
+
+const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
+const openForm = () => {
+  if (isReadonly.value) return
+  deviceFormRef.value?.open()
+}
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotMaintenancePlan', params: {} })
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if (isReadonly.value) return
+  // 校验表单
+  await formRef.value.validate()
+  // 校验表格数据
+  const isValid = validateTableData()
+  if (!isValid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    // 将值为NULL 的保养规则 设置为 1
+    list.value.forEach((item) => {
+      // 确保保养规则不为null
+      item.mileageRule = item.mileageRule ?? 1
+      item.runningTimeRule = item.runningTimeRule ?? 1
+      item.naturalDateRule = item.naturalDateRule ?? 1
+    })
+
+    // 转换日期格式
+    const convertedList = list.value.map((item) => ({
+      ...item,
+      lastNaturalDate:
+        typeof item.lastNaturalDate === 'number'
+          ? item.lastNaturalDate
+          : item.lastNaturalDate
+            ? dayjs(item.lastNaturalDate).valueOf()
+            : null
+    }))
+
+    const data = {
+      mainPlan: formData.value,
+      mainPlanBom: convertedList
+    }
+    if (formType.value === 'create') {
+      await IotMaintenancePlanApi.createIotMaintenancePlan(data)
+      message.success(t('common.createSuccess'))
+      close()
+    } else {
+      await IotMaintenancePlanApi.updatePlan(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}` // 设备标识
+
+    // 累计参数校验逻辑
+    if (
+      row.mileageRule === 0 &&
+      row.mileageAccumulatedAttrs?.length &&
+      (row.totalMileage == null || isNaN(row.totalMileage)) &&
+      !row.type
+    ) {
+      errorMessages.push(`第 ${rowNumber} 行(${deviceIdentifier}):请选择累计运行公里数参数`)
+      isValid = false
+    }
+
+    if (
+      row.runningTimeRule === 0 &&
+      row.timeAccumulatedAttrs?.length &&
+      (row.totalRunTime == null || isNaN(row.totalRunTime)) &&
+      !row.code
+    ) {
+      errorMessages.push(`第 ${rowNumber} 行(${deviceIdentifier}):请选择累计运行时长参数`)
+      isValid = false
+    }
+
+    // 校验逻辑
+    const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
+      if (ruleValue === 0) {
+        // 规则开启
+        if (!row[configField] || (row[configField] as any) <= 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 applySorting = () => {
+  // 创建新数组并排序
+  const sortedList = sortDeviceList(list.value)
+
+  // 使用Vue的响应式方法更新数组
+  list.value = sortedList
+}
+
+// 保养项排序函数
+const sortDeviceList = (devices: IotMaintenanceBomVO[]) => {
+  // 使用slice()创建数组副本,避免修改原数组
+  return devices.slice().sort((a, b) => {
+    // 处理可能的空值
+    const aCode = a.deviceCode || ''
+    const bCode = b.deviceCode || ''
+    const aName = a.name || ''
+    const bName = b.name || ''
+
+    // 设备编码排序
+    if (aCode !== bCode) {
+      return aCode.localeCompare(bCode)
+    }
+
+    // 保养项名称排序
+    return aName.localeCompare(bName)
+  })
+}
+
+// 累计运行时长变更
+const handleAccumulatedTimeChange = (option) => {}
+
+// 累计运行公里数变更
+const handleAccumulatedMileageChange = (option) => {}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deptId: undefined,
+    name: '',
+    serialNumber: undefined,
+    responsiblePerson: undefined,
+    remark: undefined,
+    failureName: undefined,
+    status: undefined,
+    devicePersons: ''
+  }
+  formRef.value?.resetFields()
+}
+
+const normalizePlanBom = (item: IotMaintenanceBomVO) => {
+  if (item.mileageRule === 0) {
+    item.nextMaintenanceKm = calculateNextMaintenanceKm(item)
+    item.remainKm = calculateRemainKm(item)
+  }
+  if (item.runningTimeRule === 0) {
+    item.nextMaintenanceH = calculateNextMaintenanceH(item)
+    item.remainH = calculateRemainH(item)
+  }
+  if (item.naturalDateRule === 0) {
+    item.nextMaintenanceDate = calculateNextMaintenanceDate(item)
+    item.remainDay = calculateRemainDay(item)
+  }
+  setupNaturalDateSync(item)
+  return item
+}
+
+const initPage = async () => {
+  loading.value = true
+  formLoading.value = true
+  list.value = []
+  deviceIds.value = []
+  lastNaturalDateWatchers.value.clear()
+  resetForm()
+
+  queryParams.planId = id.value
+  queryParams.deviceIds = undefined
+  queryParams.bomFlag = 'b'
+
+  const deptId = useUserStore().getUser.deptId
+  dept.value = await DeptApi.getDept(deptId)
+  formData.value.name = dept.value.name + ' - 保养计划'
+  formData.value.deptId = deptId
+
+  try {
+    if (mode.value === 'create') {
+      formType.value = 'create'
+      const { wsCache } = useCache()
+      const userInfo = wsCache.get(CACHE_KEY.USER)
+      formData.value.responsiblePerson = userInfo.user.id
+    } else if (id.value) {
+      formType.value = mode.value === 'detail' ? 'detail' : 'update'
+      const plan = await IotMaintenancePlanApi.getIotMaintenancePlan(Number(id.value))
+      deviceLabel.value = plan.deviceName
+      formData.value = plan
+      const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
+      if (Array.isArray(data)) {
+        list.value = data.map(normalizePlanBom)
+        applySorting()
+      }
+    }
+  } finally {
+    loading.value = false
+    formLoading.value = false
+  }
+}
+
+watch(
+  () => route.fullPath,
+  () => {
+    initPage()
+  },
+  { immediate: true }
+)
+
+onUnmounted(async () => {})
+
+const handleDelete = async (row: IotMaintenanceBomVO) => {
+  if (isReadonly.value) return
+  try {
+    const deviceId = row.deviceId
+    const bomNodeId = row.bomNodeId
+    // 删除列表项
+    const index = list.value.findIndex(
+      (item) => item.deviceId === deviceId && item.bomNodeId === bomNodeId
+    )
+    if (index !== -1) {
+      list.value.splice(index, 1)
+      // 删除保养项后对保养项重新排序
+      applySorting()
+      deviceIds.value = []
+    }
+    // 更新设备ID列表(需要检查是否还有该设备的其他项)
+    const hasOtherItems = list.value.some((item) => item.deviceId === deviceId)
+    if (!hasOtherItems) {
+      deviceIds.value = deviceIds.value.filter((id) => id !== deviceId)
+    }
+    // message.success('移除成功')
+  } catch (error) {
+    console.error('移除失败:', error)
+    message.error('移除失败')
+  }
+}
+</script>
+<style scoped>
+.maintenance-plan-page {
+  display: flex;
+  min-height: calc(100vh - 160px);
+  padding: 10px;
+  background: var(--el-bg-color-page);
+  flex-direction: column;
+  gap: 10px;
+}
+
+.plan-section {
+  padding: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+}
+
+.plan-form-section {
+  flex: 0 0 auto;
+}
+
+.plan-form :deep(.el-form-item) {
+  margin-bottom: 12px;
+}
+
+.plan-form :deep(.el-textarea__inner) {
+  resize: none;
+}
+
+.plan-table-section {
+  display: flex;
+  min-height: 0;
+  flex: 1 1 auto;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.table-toolbar {
+  display: flex;
+  justify-content: flex-start;
+}
+
+.maintenance-plan-table {
+  --zm-table-radius: 2px;
+  --zm-table-cell-height: 32px;
+  --zm-table-header-cell-height: 38px;
+  --zm-table-header-group-cell-height: 42px;
+  --zm-table-font-size: 12px;
+
+  flex: 1 1 auto;
+}
+
+.full-content-cell {
+  overflow: visible;
+  white-space: nowrap;
+}
+
+.table-actions {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  white-space: nowrap;
+}
+
+.plan-footer {
+  display: flex;
+  flex: 0 0 auto;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 10px 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+}
+
+:deep(.el-input-number .el-input__inner) {
+  padding-left: 10px;
+  text-align: left !important;
+}
+
+.form-group {
+  position: relative;
+  padding: 20px 15px 10px;
+  margin-bottom: 18px;
+  border: 1px solid var(--el-border-color);
+  border-radius: 4px;
+}
+
+.group-title {
+  position: absolute;
+  top: -10px;
+  left: 20px;
+  padding: 0 8px;
+  font-size: 12px;
+  font-weight: 500;
+  color: var(--el-text-color-regular);
+  background: var(--el-bg-color);
+}
+
+:deep(.zm-table .all-filled) {
+  background-color: #67c23a !important;
+}
+
+:deep(.zm-table .all-filled .cell) {
+  color: #fff;
+}
+</style>