Преглед на файлове

pms 保养工单 修改退回的保养工单

zhangcl преди 1 ден
родител
ревизия
70ed7d67d6

+ 2 - 1
src/locales/en.ts

@@ -1230,7 +1230,8 @@ export default {
     InspectPlanEdit:'InspectPlanEdit',
     InspectOrderDetail:'InspectOrderDetail',
     FailureDetail:'FailureDetail',
-    MaterialManagement: 'MaterialManagement'
+    MaterialManagement: 'MaterialManagement',
+    modifyOrder: 'ModifyOrder'
   },
 }
 

+ 2 - 1
src/locales/ru.ts

@@ -78,7 +78,8 @@ export default {
     hasRole: `请设置角色权限标签值`
   },
   rem: {
-    MaterialManagement: '物料管理'
+    MaterialManagement: '物料管理',
+    modifyOrder: '修改工单'
   },
   setting: {
     projectSetting: '项目配置',

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

@@ -1227,9 +1227,8 @@ export default {
     InspectOrderDetail:'巡检工单详情',
     InspectOrder:'巡检工单',
     FailureDetail:'查看故障详情',
-    MaterialManagement: '物料管理'
-
-
+    MaterialManagement: '物料管理',
+    modifyOrder: '修改工单'
   },
 
   'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错

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

@@ -844,6 +844,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/mainworkorder/bom'
         }
       },
+      {
+        path: 'mainworkorder/modify/:id(\\d+)',
+        component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrderModify.vue'),
+        name: 'IotMainWorkOrderModify',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: t('rem.modifyOrder'),
+          activeMenu: '/mainworkorder/modify'
+        }
+      },
       {
         path: 'mainworkorder/add',
         component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue'),

+ 3118 - 0
src/views/pms/iotmainworkorder/IotMainWorkOrderModify.vue

@@ -0,0 +1,3118 @@
+<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="t('bomList.name')" prop="name">
+              <el-input type="text" v-model="formData.name" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('mainPlan.MaintenanceMethod')" prop="type">
+              <el-select v-model="formData.outsourcingFlag" :placeholder="t('faultForm.choose')" clearable disabled>
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.PMS_ORDER_PROCESS_MODE)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('mainPlan.MaintenanceCost')" prop="cost">
+              <el-input
+                v-model="formData.cost"
+                placeholder="根据物料消耗自动生成"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('fault.start')" prop="actualStartTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualStartTime"
+                type="datetime"
+                value-format="x"
+                :placeholder="t('fault.start')"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('fault.end')" prop="actualEndTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualEndTime"
+                type="datetime"
+                value-format="x"
+                :placeholder="t('fault.end')"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('mainPlan.otherCost')" prop="otherCost">
+              <el-input
+                v-model="formData.otherCost"
+                @input="handleInput(formData.otherCost, 'otherCost')"
+                placeholder="其他费用"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
+              <el-input type="text" v-model="formData.deviceCode" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.name')" prop="deviceName">
+              <el-input type="text" v-model="formData.deviceName" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('faultForm.remark')" prop="remark">
+              <el-input v-model="formData.remark" type="textarea" :placeholder="t('faultForm.rHolder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+  </ContentWrap>
+  <ContentWrap>
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table ref="mainTableRef"
+                v-loading="loading"
+                :data="paginatedList"
+                :stripe="true"
+                @row-click="handleRowClick"
+                :row-class-name="tableRowClassName"
+                :show-overflow-tooltip="true"
+                :header-cell-style="tableHeaderStyle"
+                :span-method="handleSpanMethod"
+                highlight-current-row
+                @current-change="handleCurrentChangeTable">
+        <!-- 序号列 -->
+        <el-table-column
+          type="index"
+          :label="t('maintain.serial')"
+          align="center"
+          prop="serial"
+          :width="columnWidths.serial"
+          fixed="left"
+        />
+        <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
+        <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" :width="columnWidths.deviceCode" fixed="left" v-if="false">
+          <template #default="{ row }">
+            <div class="full-content-cell"> <!-- 自定义样式 -->
+              {{ row.deviceCode }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" :width="columnWidths.deviceName" fixed="left" v-if="false">
+          <template #default="{ row }">
+            <div class="full-content-cell"> <!-- 自定义样式 -->
+              {{ row.deviceName }}
+            </div>
+          </template>
+        </el-table-column>
+        <!-- 保养项组列 -->
+        <el-table-column
+          v-if="hasGroupInCurrentPage"
+          :label="t('mainPlan.MaintItemsGroup')"
+          align="center"
+          prop="group"
+          fixed="left"
+          :width="columnWidths.group"
+          :cell-class-name="groupCellClassName"
+        >
+          <template #default="{ row }">
+            <div class="full-content-cell">
+              {{ row.group }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('mainPlan.MaintItems')" align="center" prop="name"
+                         :show-overflow-tooltip="false" :width="columnWidths.name" fixed="left">
+          <template #default="{ row }">
+            <div class="full-content-cell">
+              <!-- 保养项 只显示'->'后的内容 -->
+              {{ formatMaintItemName(row.name) }}
+            </div>
+          </template>
+        </el-table-column>
+
+        <!-- 消耗物料规则列 -->
+        <el-table-column :label="t('mainPlan.consumeMaterials')" align="center" width="80">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.rule"
+              :active-value="0"
+              :inactive-value="1"
+              disabled
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column :label="t('mainPlan.mainStatus')" align="center" width="140">
+          <template #default="scope">
+            <div class="status-container">
+              <el-switch
+                v-model="scope.row.status"
+                :active-value="1"
+                :inactive-value="0"
+                :disabled="scope.row.initialStatus === 1"
+                @change="handleStatusChange(scope.row)"
+                class="status-switch"
+              />
+              <span class="status-text">
+                {{ getStatusText(scope.row) }}
+              </span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column :label="t('main.mileage')" key="mileageRule" align="center"
+                         :width="columnWidths.mileageRule" v-if="false">
+          <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="t('main.runTime')" key="runningTimeRule" align="center"
+                         :width="columnWidths.runningTimeRule" v-if="false">
+          <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="t('main.date')" key="naturalDateRule" align="center"
+                         :width="columnWidths.naturalDateRule" v-if="false">
+          <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="t('operationFillForm.sumTime')" align="center" prop="totalRunTime" v-if="hasTimeRuleInCurrentPage"
+                         :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalRunTime">
+          <template #default="{ row }">
+            {{ row.totalRunTime ?? row.tempTotalRunTime }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage" v-if="hasMileageRuleInCurrentPage"
+                         :formatter="erpPriceTableColumnFormatter" :width="columnWidths.totalMileage">
+          <template #default="{ row }">
+            {{ row.totalMileage ?? row.tempTotalMileage }}
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('operationFillForm.mainSumTime')" align="center" prop="mainRuntime" v-if="hasTimeRuleInCurrentPage"
+                         :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainRuntime">
+          <template #default="{ row }">
+            <el-tooltip
+              :disabled="!row.mainRuntimeError"
+              :content="row.mainRuntimeError"
+              placement="top"
+              effect="light"
+              popper-class="main-runtime-tooltip"
+              :show-after="0"
+            >
+              <div class="main-runtime-input-wrapper">
+                <el-input-number
+                  v-model="row.mainRuntime"
+                  :precision="2"
+                  :min="0"
+                  :controls="false"
+                  style="width: 100%"
+                  :disabled="row.status === 1"
+                  @change="validateMainRuntime(row)"
+                  @blur="validateMainRuntime(row)"
+                  :class="{
+                    'is-required-input': row.mainRuntimeError,
+                    'error-input': row.mainRuntimeError
+                  }"
+                />
+              </div>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('operationFillForm.mainSumKil')" align="center" prop="mainMileage" v-if="hasMileageRuleInCurrentPage"
+                         :formatter="erpPriceTableColumnFormatter" :width="columnWidths.mainMileage">
+          <template #default="{ row }">
+            <el-tooltip
+              :disabled="!row.mainMileageError"
+              :content="row.mainMileageError"
+              placement="top"
+              effect="light"
+              popper-class="main-runtime-tooltip"
+              :show-after="0"
+            >
+              <div class="main-runtime-input-wrapper">
+                <el-input-number
+                  v-model="row.mainMileage"
+                  :precision="2"
+                  :min="0"
+                  :controls="false"
+                  style="width: 100%"
+                  :disabled="row.status === 1"
+                  @change="validateMainMileage(row)"
+                  @blur="validateMainMileage(row)"
+                  :class="{
+                    'is-required-input': row.mainMileageError,
+                    'error-input': row.mainMileageError
+                  }"
+                />
+              </div>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('mainPlan.lastMaintenanceDate')" prop="lastMaintenanceDate" :width="columnWidths.lastMaintenanceDate">
+          <template #default="{ row }">
+            <div class="full-content-cell">
+              {{ row.lastMaintenanceDate }}
+            </div>
+          </template>
+        </el-table-column>
+        <!-- 保养里程 分组 -->
+        <el-table-column v-if="hasMileageRuleInCurrentPage" label="保养里程" align="center">
+          <el-table-column :label="t('mainPlan.lastMaintenanceMileage')" align="center" prop="lastRunningKilometers"
+                           :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningKilometers">
+            <template #default="{ row }">
+              {{ row.lastRunningKilometers }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.nextMaintenanceKm')"
+                           align="center" prop="nextMaintenanceKm" :width="columnWidths.nextMaintenanceKm">
+            <template #default="{ row }">
+              {{ row.nextMaintenanceKm ?? '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.remainKm')"
+                           align="center" prop="remainKm" :width="columnWidths.remainKm">
+            <template #default="{ row }">
+              <span :class="{ 'negative-value': row.remainKm != null && row.remainKm < 0 }">
+                {{ row.remainKm != null ? row.remainKm.toFixed(2) : '-' }}
+              </span>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <!-- 保养时长 分组 -->
+        <el-table-column v-if="hasTimeRuleInCurrentPage" label="保养时长" align="center">
+          <el-table-column :label="t('mainPlan.lastMaintenanceOperationTime')" align="center" prop="lastRunningTime"
+                           :formatter="erpPriceTableColumnFormatter" :width="columnWidths.lastRunningTime">
+            <template #default="{ row }">
+              {{ row.lastRunningTime }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.nextMaintenanceH')"
+                           align="center" prop="nextMaintenanceH" :width="columnWidths.nextMaintenanceH">
+            <template #default="{ row }">
+              {{ row.nextMaintenanceH ?? '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.remainH')"
+                           align="center" prop="remainH" :width="columnWidths.remainH">
+            <template #default="{ row }">
+              <span :class="{ 'negative-value': row.remainH != null && row.remainH < 0 }">
+                {{ row.remainH != null ? row.remainH.toFixed(2) : '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column
+            v-if="shouldShowRunningTimeColumn"
+            :label="t('mainPlan.RunTimeCycle')"
+            align="center"
+            prop="nextRunningTime"
+            :width="columnWidths.nextRunningTime"
+          >
+            <template #default="scope">
+              <el-input-number
+                v-if="scope.row.runningTimeRule === 0"
+                v-model="scope.row.nextRunningTime"
+                :precision="1"
+                :min="1"
+                :controls="false"
+                style="width: 100%"
+                @change="validateRunningTime(scope.row)"
+                disabled
+              />
+              <span v-else>-</span>
+              <!-- 错误提示 -->
+              <div v-if="scope.row.timeError" class="error-text">
+                {{ scope.row.timeError }}
+              </div>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <!-- 保养日期 分组 -->
+        <el-table-column v-if="hasDateRuleInCurrentPage" label="保养日期" align="center">
+          <el-table-column :label="t('mainPlan.lastMaintenanceNaturalDate')" align="center" prop="tempLastNaturalDate"
+                           :formatter="erpPriceTableColumnFormatter" :width="columnWidths.tempLastNaturalDate">
+            <template #default="{ row }">
+              {{ row.tempLastNaturalDate }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.nextMaintDate')"
+                           align="center" prop="nextMaintenanceDate" :width="columnWidths.nextMaintenanceDate">
+            <template #default="{ row }">
+              {{ row.nextMaintenanceDate ?? '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('mainPlan.remainDay')"
+                           align="center" prop="remainDay" :width="columnWidths.remainDay">
+            <template #default="{ row }">
+              <span :class="{ 'negative-value': row.remainDay != null && row.remainDay < 0 }">
+                {{ row.remainDay ?? '-' }}
+              </span>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column :label="t('iotMaintain.operation')" align="center" prop="operation" :width="columnWidths.operation" fixed="right">
+          <template #default="scope">
+            <div class="horizontal-actions">
+              <!-- 延迟保养按钮 -->
+              <el-tooltip
+                v-if="scope.row.status === 0"
+                effect="dark"
+                :content="t('stock.DelayMaintenance')"
+                placement="top"
+                :show-after="300"
+              >
+                <el-button
+                  type="primary"
+                  link
+                  :icon="Clock"
+                  @click="handleDropdownCommand({ action: 'delay', row: scope.row })"
+                  class="action-button"
+                />
+              </el-tooltip>
+
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10]"
+          :background="true"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="list.length"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </ContentWrap>
+
+    <!-- 选择的物料列表 -->
+    <ContentWrap>
+
+      <div style="margin-bottom: 15px; display: flex; justify-content: flex-start; align-items: center; gap: 12px;">
+        <!-- 2. 新增“选择物料”按钮,与现有按钮平齐 -->
+        <el-button
+          type="primary"
+          :icon="Box"
+          @click="handleSelectMaterial"
+          :disabled="!currentBomItem || isButtonsDisabled"
+        >
+          {{ t('stock.selectMaterial') }}
+        </el-button>
+        <!-- 原有“显示所有物料/当前保养项物料”按钮 -->
+        <el-button
+          type="primary"
+          @click="toggleShowAllMaterials"
+          :icon="showAllMaterials ? Star : StarFilled"
+        >
+          {{ showAllMaterials ? '显示当前保养项物料' : '显示所有物料' }}
+        </el-button>
+        <!-- 新增物料按钮 -->
+        <el-button
+          type="primary"
+          :icon="Plus"
+          @click="handleAddMaterial"
+          :disabled="!currentBomItem || isButtonsDisabled"
+        >
+          新增物料
+        </el-button>
+      </div>
+
+      <el-table v-loading="false" :data="displayedMaterialList" :stripe="true" :show-overflow-tooltip="true"
+                v-if="showMaterialTable" :row-class-name="getMaterialRowClassName">
+        <el-table-column :label="t('iotDevice.serial')" align="center" width="50px">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="" align="center" prop="deviceId" v-if="false"/>
+        <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
+        <el-table-column label="工厂id" align="center" prop="factoryId" v-if="false"/>
+        <el-table-column label="工厂名称" align="center" prop="factory"/>
+        <el-table-column label="成本中心id" align="center" prop="costCenterId" v-if="false"/>
+        <el-table-column label="成本中心名称" align="center" prop="costCenter"/>
+        <el-table-column label="库存地点id" align="center" prop="storageLocationId" v-if="false"/>
+        <el-table-column label="库存地点名称" align="center" prop="projectDepartment"/>
+        <el-table-column label="物料编码" align="center" prop="materialCode" >
+          <template #default="{ row }">
+            <el-input
+              v-if="row.isNew"
+              v-model="row.materialCode"
+              placeholder="请输入物料编码"
+              style="width: 100%"
+              :disabled="isMaterialDisabled(row)"
+            />
+            <span v-else>{{ row.materialCode }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="物料名称" align="center" prop="materialName" >
+          <template #default="{ row }">
+            <!-- 新增物料行:显示tooltip和必填红色边框 -->
+            <el-tooltip
+              v-if="row.isNew"
+              effect=""
+              content=""
+              placement="top"
+              :disabled="row.materialName?.trim()"
+            >
+            <el-input
+              v-model="row.materialName"
+              placeholder="请输入物料名称(必填)"
+              style="width: 100%"
+              :class="{ 'is-required-input': row.isNew && !row.materialName?.trim() }"
+              :disabled="isMaterialDisabled(row)"
+            />
+            </el-tooltip>
+            <!-- 非新增行:正常显示 -->
+            <span v-else>{{ row.materialName }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" prop="unit" width="60px">
+          <template #default="{ row }">
+            <el-tooltip
+              v-if="row.isNew"
+              effect=""
+              content=""
+              placement=""
+              :disabled="row.unit?.trim()"
+            >
+              <el-input
+                v-model="row.unit"
+                placeholder="单位(必填)"
+                style="width: 100%"
+                :class="{
+                  'is-required-input': row.isNew && !row.unit?.trim()
+                }"
+                :disabled="isMaterialDisabled(row)"
+              />
+            </el-tooltip>
+            <span v-else>{{ row.unit }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter" width="120px">
+          <template #default="{ row }">
+            <div class="unit-price-container">
+              <el-input-number
+                v-model="row.unitPrice"
+                :precision="2"
+                :min="0"
+                :controls="false"
+                style="width: 100%"
+                class="unit-price-input"
+                :class="{
+                  'is-required-input': row.unitPrice <= 0
+                }"
+                :disabled="isMaterialDisabled(row)"
+              />
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="消耗数量" align="center" prop="quantity" width="120px">
+          <template #default="{ row }">
+            <el-input-number
+              v-model="row.quantity"
+              :precision="4"
+              :min="0"
+            :controls="false"
+            style="width: 100%"
+            :class="{ 'is-required-input': row.quantity <= 0 }"
+            :disabled="isMaterialDisabled(row)"
+            placeholder="请输入(必填)"
+            />
+            <!-- </el-tooltip> -->
+          </template>
+        </el-table-column>
+        <el-table-column label="库存数量" align="center" prop="totalInventoryQuantity" width="80px"/>
+        <el-table-column label="来源" align="center" prop="materialSource" width="100px"/>
+        <el-table-column label="操作" align="center" width="80" fixed="right">
+          <template #default="scope">
+            <el-tooltip
+              effect="dark"
+              content="删除物料"
+              placement="top"
+              :show-after="300"
+            >
+              <el-button
+                type="danger"
+                link
+                :icon="Delete"
+                @click="handleDeleteMaterialInTable(scope.row)"
+                :disabled="isMaterialDisabled(scope.row)"
+                class="action-button"
+              />
+            </el-tooltip>
+          </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">{{t('common.save')}}</el-button>
+        <el-button @click="close">{{t('common.cancel')}}</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 新增配置对话框 -->
+  <el-dialog
+    v-model="configDialog.visible"
+    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
+    width="600px"
+  >
+    <!-- 使用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
+            v-model="configDialog.form.lastRunningKilometers"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <!-- 推迟公里数 -->
+        <el-form-item
+          v-if="configDialog.current?.mileageRule === 0"
+          :label="t('mainPlan.DelayKil')"
+          prop="delayKilometers"
+        >
+          <el-input-number
+            v-model="configDialog.form.delayKilometers"
+            :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
+            v-model="configDialog.form.lastRunningTime"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <!-- 推迟时长 -->
+        <el-form-item
+          v-if="configDialog.current?.runningTimeRule === 0"
+          :label="t('mainPlan.DelayDuration')"
+          prop="delayDuration"
+        >
+          <el-input-number
+            v-model="configDialog.form.delayDuration"
+            :precision="2"
+            :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
+            v-model="configDialog.form.lastNaturalDate"
+            type="date"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <!-- 推迟自然日期 -->
+        <el-form-item
+          v-if="configDialog.current?.naturalDateRule === 0"
+          :label="t('mainPlan.DelayDate')"
+          prop="delayNaturalDate"
+        >
+          <el-input-number
+            v-model="configDialog.form.delayNaturalDate"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+          />
+        </el-form-item>
+        <el-form-item
+          :label="t('stock.DelayReason')"
+          prop="delayReason"
+          v-if="configDialog.current?.mileageRule === 0 ||
+              configDialog.current?.runningTimeRule === 0 ||
+              configDialog.current?.naturalDateRule === 0"
+        >
+          <el-input
+            v-model="configDialog.form.delayReason"
+            type="textarea"
+            :rows="2"
+            :placeholder="t('stock.DelayReason')"
+            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
+            v-model="configDialog.form.nextRunningKilometers"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.mileageRule === 0"
+          :label="t('mainPlan.OperatingMileageCycle_lead')"
+          prop="kiloCycleLead"
+        >
+          <el-input-number
+            v-model="configDialog.form.kiloCycleLead"
+            :precision="2"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </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
+            v-model="configDialog.form.nextRunningTime"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.runningTimeRule === 0"
+          :label="t('mainPlan.RunTimeCycle_Lead')"
+          prop="timePeriodLead"
+        >
+          <el-input-number
+            v-model="configDialog.form.timePeriodLead"
+            :precision="1"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </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
+            v-model="configDialog.form.nextNaturalDate"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="configDialog.current?.naturalDateRule === 0"
+          :label="t('mainPlan.NaturalDailyCycle_Lead') "
+          prop="naturalDatePeriodLead"
+        >
+          <el-input-number
+            v-model="configDialog.form.naturalDatePeriodLead"
+            :min="0"
+            controls-position="right"
+            :controls="false"
+            style="width: 60%"
+            :disabled="true"
+          />
+        </el-form-item>
+      </div>
+    </el-form>
+    <template #footer>
+      <el-button @click="configDialog.visible = false">{{ t('common.cancel')}}</el-button>
+      <el-button type="primary" @click="saveConfig">{{ t('common.ok')}}</el-button>
+    </template>
+  </el-dialog>
+  <!-- 表单弹窗:添加/修改 -->
+  <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
+  <!-- 设备BOM节点绑定的物料列表 -->
+  <DeviceBomMaterials ref="deviceBomMaterialsRef" />
+  <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
+  <MaterialListDrawer
+    :model-value="drawerVisible"
+    @update:model-value="val => drawerVisible = val"
+    :node-id="currentBomNodeId"
+    :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
+    @delete="handleDeleteMaterial"
+    :hide-extra-columns="hideExtraColumnsInDrawer"
+  />
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
+import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
+import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
+import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+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 DeviceBomMaterials from "@/views/pms/iotmainworkorder/DeviceBomMaterials.vue";
+import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
+// 引入图标
+import { View, Clock, Box, Document, Star, StarFilled, Delete, Plus } from '@element-plus/icons-vue';
+
+/** 保养工单 退回后修改 */
+defineOptions({ name: 'IotMainWorkOrderModify' })
+
+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 hideExtraColumnsInDrawer = ref(false)
+
+// 分页相关变量
+const currentPage = ref(1)
+const pageSize = ref(10)
+
+const tableRef = ref();
+const mainTableRef = ref(); // 添加表格ref
+
+// 新增响应式变量
+const maintItemsWidth = ref('auto')
+
+const lastNaturalDateWatchers = ref(new Map())
+
+const columnWidths = ref<Record<string, string>>({});
+
+// 物料列表相关变量
+const showMaterialTable = ref(true) // 控制物料列表显示
+const currentBomItem = ref(null) // 当前选中的保养项
+const bomMaterialsMap = ref({}) // 存储所有保养项物料列表的映射
+const showAllMaterials = ref(false) // 控制是否显示所有物料
+
+// 计算当前页是否有分组数据
+const hasGroupInCurrentPage = computed(() => {
+  return paginatedList.value.some(item => item.group && item.group !== '');
+});
+
+// 计算属性:根据showAllMaterials决定显示的物料列表
+const displayedMaterialList = computed(() => {
+  if (showAllMaterials.value) {
+    // 显示所有物料
+    const allMaterials = [];
+    for (const key in bomMaterialsMap.value) {
+      allMaterials.push(...bomMaterialsMap.value[key]);
+    }
+    return allMaterials;
+  } else {
+    // 显示当前选中保养项的物料
+    return materialList.value;
+  }
+});
+
+// 判断物料是否被禁用
+const isMaterialDisabled = (material) => {
+  // 根据物料的bomNodeId找到对应的保养项
+  const bomItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
+  // 如果保养项不存在,默认不禁用
+  if (!bomItem) return false;
+  // 如果保养项状态为已完成(status=1),则禁用该物料
+  if (bomItem.status === 1) return true;
+  // 如果保养项rule=1(不消耗物料),则禁用
+  if (bomItem.rule === 1) return true;
+  return false;
+};
+
+// 物料表格行类名 - 用于设置灰色背景
+const getMaterialRowClassName = ({ row }) => {
+  if (row.isNew) {
+    return 'new-material-row';
+  }
+  if (isMaterialDisabled(row)) {
+    return 'disabled-material-row';
+  }
+  return '';
+};
+
+// 计算属性:判断按钮是否应该禁用
+const isButtonsDisabled = computed(() => {
+  if (!currentBomItem.value) return true;
+
+  // 条件1:保养状态为已完成(status=1)
+  if (currentBomItem.value.status === 1) return true;
+
+  // 条件2:消耗物料字段为不消耗物料(rule=1)
+  if (currentBomItem.value.rule === 1) return true;
+
+  return false;
+});
+
+// 切换显示所有物料/当前保养项物料
+const toggleShowAllMaterials = () => {
+  showAllMaterials.value = !showAllMaterials.value;
+
+  // 切换显示模式时,需要同步更新 materialList
+  if (!showAllMaterials.value && currentBomItem.value) {
+    // 切换到显示当前保养项物料时,使用新的刷新方法
+    refreshCurrentBomItemMaterials();
+  } else if (showAllMaterials.value) {
+    // 切换到显示所有物料
+    const allMaterials = [];
+    for (const key in bomMaterialsMap.value) {
+      allMaterials.push(...bomMaterialsMap.value[key]);
+    }
+    materialList.value = allMaterials;
+  }
+  console.log('切换显示模式:', {
+    showAllMaterials: showAllMaterials.value,
+    materialCount: materialList.value.length
+  });
+};
+
+// 分组合并计算逻辑
+const groupSpans = ref<Record<string, { span: number, index: number }>>({})
+
+const formData = ref({
+  id: undefined,
+  deptId: undefined,
+  name: '',
+  orderNumber: undefined,
+  responsiblePerson: undefined,
+  actualStartTime: undefined,
+  actualEndTime: undefined,
+  cost: undefined,
+  otherCost: undefined,
+  outsourcingFlag: 0,
+  remark: undefined,
+  status: undefined,
+  devicePersons: '',
+  deviceCode: '',
+  deviceName: ''
+})
+
+const formRules = reactive({
+  name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
+  actualStartTime: [{
+    required: true,
+    message: t('fault.start') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const start = value ? dayjs(Number(value)) : null;
+
+      // 验证开始时间 <= 当前日期
+      if (start && start.isAfter(now)) {
+        callback(new Error(t('fault.start') + '不能超过当前日期'));
+        return;
+      }
+
+      const end = formData.value.actualEndTime
+        ? dayjs(Number(formData.value.actualEndTime))
+        : null;
+
+      // 只有当结束时间有效时,才比较开始和结束时间
+      if (end && end.isValid() && !end.isAfter(now)) {
+        if (start && start.isAfter(end)) {
+          callback(new Error(t('fault.start') + '不能超过' + t('fault.end')));
+          return;
+        }
+      }
+
+      // 触发结束时间的重新校验
+      if (formRef.value) {
+        formRef.value.validateField('actualEndTime', () => {});
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }],
+  actualEndTime: [{
+    required: true,
+    message: t('fault.end') + '不能为空',
+    trigger: 'change'
+  }, {
+    validator: (rule, value, callback) => {
+      const now = dayjs();
+      const end = value ? dayjs(Number(value)) : null;
+
+      // 验证结束时间 <= 当前日期
+      if (end && end.isAfter(now)) {
+        callback(new Error(t('fault.end') + '不能超过当前日期'));
+        return;
+      }
+
+      const start = formData.value.actualStartTime
+        ? dayjs(Number(formData.value.actualStartTime))
+        : null;
+
+      // 验证结束时间 >= 开始时间(仅当开始时间存在时)
+      if (start && end && end.isBefore(start)) {
+        callback(new Error(t('fault.end') + '必须大于等于' + t('fault.start')));
+        return;
+      }
+
+      callback();
+    },
+    trigger: 'change'
+  }]
+})
+
+const formRef = ref() // 表单 Ref
+
+interface MaterialFormExpose {
+  open: (deptId: number, bomNodeId: number, row: any, type: string) => void
+}
+
+const materialFormRef = ref<MaterialFormExpose>();
+
+const deviceBomMaterialsRef = 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,
+    // 推迟原因
+    delayReason: ''
+  }
+})
+
+// 打开配置对话框
+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,
+    // 推迟原因
+    delayReason: row.delayReason || ''
+  }
+  configDialog.visible = true
+}
+
+// 格式化 保养项名称 方法 只显示 -> 后面的内容
+const formatMaintItemName = (name: string) => {
+  if (!name) return '';
+
+  // 包含'->'时只取后半部分
+  if (name.includes('->')) {
+    return name.split('->').pop()?.trim() || name;
+  }
+
+  // 不含'->'时显示完整内容
+  return name;
+};
+
+// 行合并方法 (优化后符合图片效果)
+const handleSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
+  // 只处理保养项组列
+  if (column.property === 'group' && row.group) {
+    const groupKey = `${currentPage.value}-${row.group}`;
+    const spanInfo = groupSpans.value[groupKey];
+
+    if (spanInfo && rowIndex === spanInfo.index) {
+      return {
+        rowspan: spanInfo.span,
+        colspan: 1
+      };
+    } else {
+      return {
+        rowspan: 0,
+        colspan: 0
+      };
+    }
+  }
+
+  // 其他列不合并
+  return {
+    rowspan: 1,
+    colspan: 1
+  };
+};
+
+// 计算分组合并信息 (优化后符合图片效果)
+const calculateGroupSpans = () => {
+  // 重置分组信息
+  groupSpans.value = {};
+
+  const pageKey = currentPage.value;
+  let currentGroup = '';
+  let groupStartIndex = 0;
+  let groupCount = 0;
+
+  // 先清除所有行的分组标记
+  paginatedList.value.forEach(item => {
+    delete item.isGroupFirstRow;
+  });
+
+  paginatedList.value.forEach((item, index) => {
+    // 获取当前行的分组(从name中提取)
+    const group = item.name && item.name.includes('->')
+      ? item.name.split('->')[0].trim()
+      : '';
+
+    // 如果分组变化
+    if (group !== currentGroup) {
+      // 保存上一个分组的信息
+      if (currentGroup && groupCount > 0) {
+        const groupKey = `${pageKey}-${currentGroup}`;
+        groupSpans.value[groupKey] = {
+          span: groupCount,
+          index: groupStartIndex
+        };
+
+        // 标记上一个分组的起始行(添加这行)
+        paginatedList.value[groupStartIndex].isGroupFirstRow = true;
+      }
+
+      // 开始新分组
+      currentGroup = group;
+      groupStartIndex = index;
+      groupCount = 1;
+    } else {
+      // 相同分组,计数增加
+      groupCount++;
+    }
+  });
+
+  // 保存最后一个分组的信息
+  if (currentGroup && groupCount > 0) {
+    const groupKey = `${pageKey}-${currentGroup}`;
+    groupSpans.value[groupKey] = {
+      span: groupCount,
+      index: groupStartIndex
+    };
+
+    // 标记最后一个分组的起始行(添加这行)
+    paginatedList.value[groupStartIndex].isGroupFirstRow = true;
+  }
+};
+
+// 运行时间周期 单行校验方法
+const validateRunningTime = (row: IotMainWorkOrderBomVO) => {
+  if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
+    row.timeError = t('mainPlan.runningTimeCycleError');
+    return false;
+  }
+  row.timeError = '';
+  return true;
+};
+
+// 计算属性:获取当前页的数据
+const paginatedList = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value
+  const end = start + pageSize.value
+  return list.value.slice(start, end)
+})
+
+// 页码改变处理
+const handleCurrentChange = (newPage: number) => {
+  currentPage.value = newPage
+}
+
+// 每页条数改变处理(保持10条)
+const handleSizeChange = (newSize: number) => {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+// 处理状态变化
+const handleStatusChange = (row: IotMainWorkOrderBomVO) => {
+  // 如果是从未完成(0)切换到完成(1)
+  if (row.status === 1 && row.initialStatus === 0) {
+    // 校验 mainRuntime
+    if (!validateMainRuntime(row)) {
+      message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainRuntimeError')}`);
+      row.status = 0; // 重置状态
+      return;
+    }
+
+    // 校验 mainMileage
+    if (!validateMainMileage(row)) {
+      message.error(`${row.deviceCode}-${formatMaintItemName(row.name)} ${t('mainPlan.mainMileageError')}`);
+      row.status = 0; // 重置状态
+      return;
+    }
+
+    // 检查消耗物料规则:如果设置为不消耗物料(rule=1),则跳过物料校验
+    if (row.rule === 1) {
+      console.log(`保养项 ${row.name} 设置为不消耗物料,跳过物料校验`);
+      message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
+      return;
+    }
+
+    // 校验物料数据
+    const isValid = validateBomItemMaterials(row.bomNodeId);
+
+    if (!isValid) {
+      // 获取无效物料信息用于提示
+      const invalidMaterials = getInvalidMaterialsInfo(row.bomNodeId);
+      const errorMessage = `请填写物料必填项\n无效物料:\n${invalidMaterials.join('\n')}`;
+
+      message.error(errorMessage);
+
+      // 重置状态为未完成
+      row.status = 0;
+      return;
+    }
+  }
+
+  console.log(`保养项 ${row.name} 状态改为: ${row.status === 1 ? '完成' : '未完成'}`);
+
+  // 这里可以添加状态变化后的业务逻辑
+  // 例如:如果状态变为完成,可以自动填充完成时间等
+  if (row.status === 1) {
+    // 保养完成时的逻辑
+    message.success(`${row.deviceCode}-${formatMaintItemName(row.name)} 已标记为完成`);
+  }
+};
+
+// 运行时间周期 全局校验方法(在submitForm中调用)
+const validateAllRunningTimes = (): boolean => {
+  let isValid = true;
+  list.value.forEach(row => {
+    if (row.runningTimeRule === 0 && (!row.nextRunningTime || row.nextRunningTime <= 0)) {
+      isValid = false;
+      // 高亮标记错误行
+      row.timeError = t('mainPlan.runningTimeCycleError');
+      message.error(`${t('mainPlan.runningTimeCycleError')}: ${row.deviceCode}-${row.name}`);
+    }
+  });
+  return isValid;
+};
+
+// 校验 mainRuntime 的方法
+const validateMainRuntime = (row: IotMainWorkOrderBomVO) => {
+  // 清除之前的错误
+  row.mainRuntimeError = '';
+
+  const mainRuntime = Number(row.mainRuntime) || 0;
+  const totalRunTime = Number(row.totalRunTime ?? row.tempTotalRunTime) || 0;
+  const lastRunningTime = Number(row.lastRunningTime) || 0;
+
+  // 如果 mainRuntime 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainRuntime 必填
+  /* if (!mainRuntime || mainRuntime === 0) {
+    return true;
+  } */
+
+  // 校验规则:lastRunningTime ≤ mainRuntime ≤ totalRunTime
+  if (mainRuntime < lastRunningTime) {
+    row.mainRuntimeError = `保养时累计运行时间不能小于上次保养时长(${lastRunningTime})`;
+    return false;
+  }
+
+  if (mainRuntime > totalRunTime) {
+    row.mainRuntimeError = `保养时累计运行时间不能大于累计运行时间(${totalRunTime})`;
+    return false;
+  }
+  // 校验通过时确保错误信息被清除
+  row.mainRuntimeError = '';
+  return true;
+};
+
+// 校验 mainMileage 的方法
+const validateMainMileage = (row: IotMainWorkOrderBomVO) => {
+  // 清除之前的错误
+  row.mainMileageError = '';
+
+  const mainMileage = Number(row.mainMileage) || 0;
+  const totalMileage = Number(row.totalMileage ?? row.tempTotalMileage) || 0;
+  const lastRunningKilometers = Number(row.lastRunningKilometers) || 0;
+
+  // 如果 mainMileage 为 0 或空,不显示错误(允许为空)如果设置了累计运行时长保养规则 mainMileage 必填
+  /* if (!mainMileage || mainMileage === 0) {
+    return true;
+  } */
+
+  // 校验规则:lastRunningKilometers ≤ mainMileage ≤ totalMileage
+  if (mainMileage < lastRunningKilometers) {
+    row.mainMileageError = `保养时累计运行公里数不能小于上次保养里程数(${lastRunningKilometers})`;
+    return false;
+  }
+
+  if (mainMileage > totalMileage) {
+    row.mainMileageError = `保养时累计运行公里数不能大于累计运行公里数(${totalMileage})`;
+    return false;
+  }
+  // 校验通过时确保错误信息被清除
+  row.mainMileageError = '';
+  return true;
+};
+
+// 全局校验所有 mainRuntime
+const validateAllMainRuntimes = (): boolean => {
+  let isValid = true;
+  list.value.forEach(row => {
+    if (!validateMainRuntime(row)) {
+      isValid = false;
+    }
+  });
+  return isValid;
+};
+
+// 全局校验所有 mainMileage
+const validateAllMainMileages = (): boolean => {
+  let isValid = true;
+  list.value.forEach(row => {
+    if (!validateMainMileage(row)) {
+      isValid = false;
+    }
+  });
+  return isValid;
+};
+
+/** 新增物料 */
+const handleAddMaterial = () => {
+  // 检查是否选中保养项
+  if (!currentBomItem.value) {
+    message.warning('请先在上方表格中选择一个保养项');
+    return;
+  }
+
+  // 创建空记录
+  const newMaterial = {
+    // 关联字段
+    deviceId: currentBomItem.value.deviceId,
+    bomNodeId: currentBomItem.value.bomNodeId,
+
+    // 不可编辑字段 - 设置为空
+    factory: '',
+    costCenter: '',
+    projectDepartment: '',
+    factoryId: undefined,
+    costCenterId: undefined,
+    storageLocationId: undefined,
+    totalInventoryQuantity: '',
+
+    // 可编辑字段 - 设置为初始值
+    materialCode: '',
+    materialName: '',
+    unit: '',
+    unitPrice: 0,
+    quantity: 0,
+
+    // 固定字段
+    materialSource: '手动添加',
+
+    // 临时标识(用于区分新记录)
+    isNew: true,
+    tempId: Date.now() + Math.random() // 临时ID用于唯一标识
+  };
+
+  // 生成唯一键
+  const uniqueKey = `${currentBomItem.value.bomNodeId}`;
+
+  // 更新 bomMaterialsMap
+  if (!bomMaterialsMap.value[uniqueKey]) {
+    bomMaterialsMap.value[uniqueKey] = [];
+  }
+
+  // 添加到物料映射表的最前面
+  bomMaterialsMap.value[uniqueKey].unshift(newMaterial);
+
+  // 关键修复:同步更新materialList引用(确保视图实时刷新)
+  if (!showAllMaterials.value) {
+    // 显示当前保养项物料时,强制让materialList指向最新的bomMaterialsMap数据
+    materialList.value = bomMaterialsMap.value[uniqueKey];
+  } else {
+    // 显示所有物料时,重新赋值触发响应式(因displayedMaterialList依赖bomMaterialsMap)
+    materialList.value = [...materialList.value];
+  }
+
+  message.success('已新增空白物料记录,请填写相关信息');
+
+  // 新增物料后重新计算费用
+  nextTick(() => {
+    calculateTotalCost();
+  });
+};
+
+const openMaterialForm = (row: any) => {
+  bomNodeId.value = row.bomNodeId;
+  console.log('这是一个对象:', row.bomNodeId)
+  const type = 'maintenance'
+  materialFormRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
+}
+
+// 查看当前保养项已经绑定的物料列表
+const openDeviceBomMaterials = (row: any) => {
+  bomNodeId.value = row.bomNodeId;
+  const type = 'maintenance'
+  deviceBomMaterialsRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
+}
+
+// 刷新当前保养项的物料列表显示
+const refreshCurrentBomItemMaterials = () => {
+  if (!currentBomItem.value) {
+    materialList.value = [];
+    return;
+  }
+
+  const uniqueKey = `${currentBomItem.value.bomNodeId}`;
+
+  // 确保从映射中获取最新数据
+  const currentMaterials = bomMaterialsMap.value[uniqueKey] || [];
+
+  // 强制更新 materialList 的引用,确保响应式更新
+  materialList.value = [...currentMaterials];
+
+  console.log('刷新物料列表:', {
+    currentBomItem: currentBomItem.value.name,
+    materialCount: materialList.value.length,
+    uniqueKey: uniqueKey,
+    showAllMaterials: showAllMaterials.value
+  });
+}
+
+const selectChoose = (selectedMaterial) => {
+  // 检查当前是否有选中的保养项
+  if (!currentBomItem.value) {
+    message.warning('请先选择一个保养项');
+    return;
+  }
+
+  const targetBomNodeId = currentBomItem.value.bomNodeId;
+  const targetDeviceId = currentBomItem.value.deviceId;
+
+  selectedMaterial.bomNodeId = bomNodeId.value
+  // 关联 bomNodeId
+  const processedMaterials = selectedMaterial.map(material => ({
+    ...material,
+    bomNodeId: targetBomNodeId, // 统一关联当前行的 bomNodeId
+    deviceId: targetDeviceId // 确保 deviceId 也正确关联
+  }));
+
+  // 生成唯一键
+  const uniqueKey = `${targetBomNodeId}`;
+  // 初始化物料映射(如果不存在)
+  if (!bomMaterialsMap.value[uniqueKey]) {
+    bomMaterialsMap.value[uniqueKey] = [];
+  }
+
+  // 避免重复添加
+  processedMaterials.forEach(newMaterial => {
+    // 检查是否已存在相同 工厂+成本中心+库存地点+bomNodeId + materialCode 的条目
+    const isExist = bomMaterialsMap.value[uniqueKey].some(item =>
+      item.bomNodeId === bomNodeId.value &&
+      item.materialCode === newMaterial.materialCode &&
+      item.factoryId === newMaterial.factoryId &&
+      item.costCenterId === newMaterial.costCenterId &&
+      item.storageLocationId === newMaterial.storageLocationId
+    );
+
+    if (!isExist) {
+      bomMaterialsMap.value[uniqueKey].push(newMaterial);
+    }
+  });
+
+  // 立即更新当前显示的物料列表
+  refreshCurrentBomItemMaterials();
+
+  // 选择物料后立即计算费用
+  nextTick(() => {
+    calculateTotalCost();
+  });
+}
+
+// 计算属性:判断是否需要显示运行时间周期列
+const shouldShowRunningTimeColumn = computed(() => {
+  return list.value.some(item => item.runningTimeRule === 0);
+});
+
+/** 查看已经选择的物料 并编辑 */
+const handleView = (row: IotMainWorkOrderBomVO) => {
+  currentBomNodeId.value = row.bomNodeId
+  drawerVisible.value = true
+  // 根据状态值设置是否隐藏额外列
+  hideExtraColumnsInDrawer.value = row.status === 1
+  console.log('当前bom节点:', currentBomNodeId.value)
+}
+
+// 计算保养金额
+const calculateTotalCost = () => {
+  try {
+    // 创建保养项ID到消耗规则的映射
+    const ruleMap = new Map<number, number>();
+    list.value.forEach(item => {
+      ruleMap.set(item.bomNodeId, item.rule);
+    });
+
+    // 从 bomMaterialsMap 中获取所有保养项的所有物料
+    let allMaterials = [];
+    for (const key in bomMaterialsMap.value) {
+      allMaterials = allMaterials.concat(bomMaterialsMap.value[key]);
+    }
+
+    // 物料总金额 = ∑(单价 * 消耗数量)
+    const materialTotal = allMaterials.reduce((sum, item) => {
+      // 获取物料所属保养项的消耗规则
+      const rule = ruleMap.get(item.bomNodeId);
+
+      // 如果保养项设置为不消耗物料(rule=1),跳过计算
+      if (rule === 1) return sum;
+
+      const price = Number(item.unitPrice) || 0
+      const quantity = Number(item.quantity) || 0
+      return sum + (price * quantity)
+    }, 0)
+
+    // 保养费用 = 物料总金额
+    formData.value.cost = (materialTotal).toFixed(2)
+
+    console.log('计算保养费用:', {
+      物料总数: allMaterials.length,
+      参与计算的物料数: allMaterials.filter(item => {
+        const rule = ruleMap.get(item.bomNodeId);
+        return rule !== 1; // 过滤掉不消耗物料的项
+      }).length,
+      总金额: materialTotal
+    });
+  } catch (error) {
+    console.error('计算保养费用错误:', error);
+    formData.value.cost = '0.00';
+  }
+}
+
+// 同时监听物料列表和保养项列表变化
+watch(
+  [() => materialList.value, () => list.value],
+  () => {
+    calculateTotalCost();
+  },
+  { deep: true }
+);
+
+const handleInput = (value, obj) => {
+  // 1. 过滤非法字符(只允许数字和小数点)
+  let filtered = value.replace(/[^\d.]/g, '')
+
+  // 2. 处理多个小数点的情况
+  filtered = filtered.replace(/\.{2,}/g, '.')
+
+  // 3. 限制小数点后最多两位
+  let decimalParts = filtered.split('.')
+  if (decimalParts.length > 1) {
+    decimalParts = decimalParts.slice(0, 2)
+    filtered = decimalParts.join('.')
+  }
+
+  // 4. 处理以小数点开头的情况(自动补0)
+  if (filtered.startsWith('.')) {
+    filtered = '0' + filtered
+  }
+
+  // 5. 更新绑定值(同时处理连续输入多个0的情况)
+  formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
+}
+
+// 保存配置
+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
+})
+
+// 获取指定bomNodeId的物料数量
+const getMaterialCount = (bomNodeId: number) => {
+  const uniqueKey = `${bomNodeId}`;
+  const materials = bomMaterialsMap.value[uniqueKey] || [];
+
+  // 只计算有效的物料数量
+  return materials.filter(material =>
+    !isMaterialDisabled(material) &&
+    material.materialName &&
+    material.unit &&
+    (material.unitPrice || 0) > 0 &&
+    (material.quantity || 0) > 0
+  ).length;
+}
+
+// 校验保养项下物料数据的方法
+const validateBomItemMaterials = (bomNodeId: number): boolean => {
+  const uniqueKey = `${bomNodeId}`;
+  const materials = bomMaterialsMap.value[uniqueKey] || [];
+
+  // 如果没有物料数据,直接返回true(允许完成)
+  if (materials.length === 0) {
+    return true;
+  }
+
+  // 检查所有物料的单价和消耗数量是否都大于0
+  const hasInvalidMaterial = materials.some(material => {
+    const unitPrice = Number(material.unitPrice) || 0;
+    const quantity = Number(material.quantity) || 0;
+
+    return unitPrice <= 0 || quantity <= 0;
+  });
+
+  return !hasInvalidMaterial;
+};
+
+// 获取校验失败的物料信息(用于错误提示)
+const getInvalidMaterialsInfo = (bomNodeId: number): string[] => {
+  const uniqueKey = `${bomNodeId}`;
+  const materials = bomMaterialsMap.value[uniqueKey] || [];
+  const invalidMaterials = [];
+
+  materials.forEach(material => {
+    const unitPrice = Number(material.unitPrice) || 0;
+    const quantity = Number(material.quantity) || 0;
+
+    if (unitPrice <= 0 || quantity <= 0) {
+      invalidMaterials.push(`${material.materialName || material.materialCode} (单价: ${unitPrice}, 数量: ${quantity})`);
+    }
+  });
+
+  return invalidMaterials;
+};
+
+const handleDeleteMaterialInTable = (material) => {
+  // 确认删除对话框
+  message.confirm('确认删除该物料吗?', { title: '提示' }).then(() => {
+    const targetBomNodeId = material.bomNodeId;
+
+    // 修复:统一使用 bomNodeId 作为键(与新增物料保持一致)
+    const uniqueKey = `${targetBomNodeId}`;
+
+    console.log('删除物料调试信息:', {
+      material,
+      uniqueKey,
+      bomMaterialsMapKeys: Object.keys(bomMaterialsMap.value),
+      currentMaterials: bomMaterialsMap.value[uniqueKey] || []
+    });
+
+    // 从 bomMaterialsMap 中删除
+    if (bomMaterialsMap.value[uniqueKey]) {
+      let mapIndex = -1;
+
+      // 简化查找逻辑
+      if (material.isNew && material.tempId) {
+        // 新增物料:通过 tempId 查找
+        mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
+          item.isNew && item.tempId === material.tempId
+        );
+      } else {
+        // 已有物料:通过关键字段查找
+        mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
+          !item.isNew &&
+          item.factoryId === material.factoryId &&
+          item.costCenterId === material.costCenterId &&
+          item.storageLocationId === material.storageLocationId &&
+          item.materialCode === material.materialCode
+        );
+      }
+
+      if (mapIndex !== -1) {
+        // 执行删除
+        bomMaterialsMap.value[uniqueKey].splice(mapIndex, 1);
+        message.success('物料删除成功');
+
+        console.log('删除成功后的物料列表:', bomMaterialsMap.value[uniqueKey]);
+
+        // 关键修复:立即更新当前显示的物料列表
+        refreshCurrentBomItemMaterials();
+
+        // 删除物料后重新计算费用
+        nextTick(() => {
+          calculateTotalCost();
+        });
+      } else {
+        message.warning('未找到要删除的物料记录');
+        console.error('未找到匹配的物料记录:', material);
+      }
+    } else {
+      message.warning('未找到对应的物料列表');
+    }
+  }).catch(() => {
+    // 用户取消删除
+  });
+};
+
+const handleDeleteMaterial = (material) => {
+  // 根据唯一标识查找要删除的物料索引
+  const index = materialList.value.findIndex(item =>
+    item.bomNodeId === material.bomNodeId &&
+    item.factoryId === material.factoryId &&
+    item.costCenterId === material.costCenterId &&
+    item.storageLocationId === material.storageLocationId &&
+    item.materialCode === material.materialCode
+  );
+
+  if (index !== -1) {
+    materialList.value.splice(index, 1);
+    message.success('物料删除成功');
+  }
+};
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotMainWorkOrder', params:{}})
+}
+
+// 分组单元格类名方法
+const groupCellClassName = ({ row, column }) => {
+  if (column.property === 'group' && row.isGroupFirstRow) {
+    return 'group-first-row';
+  }
+  return '';
+};
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+
+  // 运行时间周期全局校验
+  if (!validateAllRunningTimes()) {
+    return; // 校验失败则终止提交
+  }
+
+  // 保养时 累计运行时长 mainRuntime 全局校验
+  if (!validateAllMainRuntimes()) {
+    message.error(t('mainPlan.mainRuntimeError'));
+    return;
+  }
+
+  // 保养时 累计运行公里数 mainMileage 全局校验
+  if (!validateAllMainMileages()) {
+    message.error(t('mainPlan.mainMileageError'));
+    return;
+  }
+
+  // 校验所有设置为完成状态的保养项的物料数据
+  const invalidBomItems = [];
+
+  list.value.forEach(row => {
+    // 只校验状态为完成(1)且消耗物料规则为消耗(0)的保养项
+    if (row.status === 1 && row.rule === '0') {
+      const isValid = validateBomItemMaterials(row.bomNodeId);
+      if (!isValid) {
+        invalidBomItems.push({
+          name: `${row.deviceCode}-${formatMaintItemName(row.name)}`,
+          invalidMaterials: getInvalidMaterialsInfo(row.bomNodeId)
+        });
+      }
+    }
+  });
+
+  // 如果有校验失败的保养项,显示错误信息并终止提交
+  if (invalidBomItems.length > 0) {
+    let errorMessage = '请填写物料必填项\n\n';
+
+    invalidBomItems.forEach((item, index) => {
+      errorMessage += `${index + 1}. ${item.name}:\n`;
+      item.invalidMaterials.forEach(material => {
+        errorMessage += `   - ${material}\n`;
+      });
+      errorMessage += '\n';
+    });
+
+    message.error(errorMessage);
+    return;
+  }
+
+  // 校验表格数据
+  const isValid = validateTableData()
+  if (!isValid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const convertedList = list.value.map(item => ({
+      ...item,
+      lastNaturalDate: typeof item.lastNaturalDate === 'number'
+        ? item.lastNaturalDate
+        : (item.lastNaturalDate ? dayjs(item.lastNaturalDate).valueOf() : null)
+    }));
+
+    // 从 bomMaterialsMap 获取所有物料数据,而不是 materialList
+    const allMaterials = [];
+    for (const key in bomMaterialsMap.value) {
+      allMaterials.push(...bomMaterialsMap.value[key]);
+    }
+
+    const data = {
+      mainWorkOrder: formData.value,
+      mainWorkOrderBom: convertedList,
+      mainWorkOrderMaterials: allMaterials
+    }
+    await IotMainWorkOrderApi.fillWorkOrder(data)
+    message.success(t('common.createSuccess'))
+    close()
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const validateDelayReason = (rule: any, value: any, callback: any) => {
+  const form = configDialog.form
+  const hasDelay =
+    (form.delayKilometers > 0) ||
+    (form.delayDuration > 0) ||
+    (form.delayNaturalDate > 0)
+
+  if (hasDelay && (!value || value.trim() === '')) {
+    callback(new Error('请填写推迟原因'))
+  } else {
+    callback()
+  }
+}
+
+// 新增表单校验规则
+const configFormRules = reactive({
+  nextRunningKilometers: [{
+    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'
+  }],
+  // 新增推迟原因验证规则
+  delayReason: [
+    { validator: validateDelayReason, trigger: ['blur', 'change'] }
+  ]
+})
+
+// 计算文本宽度的辅助函数
+const getTextWidth = (text: string, fontSize = 14): number => {
+  if (!text) return 0;
+
+  const span = document.createElement('span')
+  span.style.visibility = 'hidden'
+  span.style.position = 'absolute'
+  span.style.whiteSpace = 'nowrap'
+  span.style.fontSize = `${fontSize}px` // 与表格实际字体一致
+  span.style.fontFamily = 'inherit'
+  span.style.fontWeight = 'normal';
+  span.innerText = text
+  document.body.appendChild(span)
+  const width = span.offsetWidth
+  document.body.removeChild(span)
+  return width
+}
+
+// 计算列宽的主函数
+const calculateMaintItemsWidth = () => {
+  if (list.value.length === 0) {
+    maintItemsWidth.value = 'auto'
+    return
+  }
+
+  // 1. 计算表头文本宽度
+  const headerText = t('mainPlan.MaintItems')
+  const headerWidth = getTextWidth(headerText)
+
+  // 2. 计算内容最大宽度
+  let contentMaxWidth = 0
+  list.value.forEach(item => {
+    if (item.name) {
+      const width = getTextWidth(item.name.toString())
+      if (width > contentMaxWidth) {
+        contentMaxWidth = width
+      }
+    }
+  })
+
+  // 3. 取最大值 + 内边距(20px)
+  const maxWidth = Math.max(headerWidth, contentMaxWidth) + 20
+  maintItemsWidth.value = `${maxWidth}px`
+}
+
+// 计算下次保养公里数(通用函数)
+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;
+  // 验证条件:规则开启 + 3个值都存在且 > 0
+  const isValid = row.mileageRule === 0 &&
+    row.lastRunningKilometers > 0 &&
+    mileageValue > 0 &&
+    row.nextRunningKilometers > 0;
+
+  if (!isValid) return 0;
+  const result = row.nextRunningKilometers - (mileageValue - row.lastRunningKilometers);
+  return parseFloat(result.toFixed(2));
+};
+
+// 计算下次保养运行时长(通用函数)
+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;
+  // 验证条件:规则开启 + 3个值都存在且 > 0
+  const isValid = row.runningTimeRule === 0 &&
+    row.lastRunningTime > 0 &&
+    runTimeValue > 0 &&
+    row.nextRunningTime > 0;
+  if (!isValid) return 0;
+  const result = row.nextRunningTime - (runTimeValue - row.lastRunningTime);
+  return parseFloat(result.toFixed(2));
+};
+
+// 计算下次保养日期(通用函数)
+const calculateNextMaintenanceDate = (row: IotMaintenanceBomVO) => {
+  // 验证条件:规则开启 + 两个值都存在且 > 0
+  const isValid = row.naturalDateRule === 0 &&
+    row.lastNaturalDate &&
+    row.nextNaturalDate;
+
+  return isValid
+    ? dayjs(row.lastNaturalDate).add(row.nextNaturalDate, '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 '-';
+  }
+
+  try {
+    // 上次保养日期:将时间戳转换为 Day.js 对象
+    const lastNaturalDate = dayjs(row.lastNaturalDate);
+
+    // 计算下次保养日期
+    const nextMaintenanceDate = lastNaturalDate.add(row.nextNaturalDate, 'day');
+
+    // 计算剩余天数(当前日期到下次保养日期的天数差)
+    return nextMaintenanceDate.diff(dayjs(), 'day');
+  } catch (error) {
+    console.error('计算保养剩余天数错误:', error);
+    return null;
+  }
+
+};
+
+const getStatusText = (row: any) => {
+  // 状态为1直接返回"完成"
+  if (row.status === 1) return t('mainPlan.completed');
+
+  // 状态为0时判断延迟字段
+  const delayDuration = Number(row.delayDuration) || 0;
+  const delayKilometers = Number(row.delayKilometers) || 0;
+  const delayNaturalDate = Number(row.delayNaturalDate) || 0;
+
+  // 任意延迟字段大于0 -> 延时
+  if (delayDuration > 0 || delayKilometers > 0 || delayNaturalDate > 0) {
+    return t('mainPlan.delayed');
+  }
+
+  // 否则显示保养中
+  return t('mainPlan.maintaining');
+};
+
+// 计算属性 - 检查当前页是否有开启的里程规则
+const hasMileageRuleInCurrentPage = computed(() => {
+  return paginatedList.value.some(row => row.mileageRule === 0);
+});
+
+// 计算属性 - 检查当前页是否有开启的 运行时间 规则
+const hasTimeRuleInCurrentPage = computed(() => {
+  return paginatedList.value.some(row => row.runningTimeRule === 0);
+});
+
+// 计算属性 - 检查当前页是否有开启的 自然日期 规则
+const hasDateRuleInCurrentPage = computed(() => {
+  return paginatedList.value.some(row => row.naturalDateRule === 0);
+});
+
+// 统一计算所有列宽
+const calculateAllColumnsWidth = () => {
+  const MIN_WIDTH = 70; // 最小列宽
+  const PADDING = 10; // 列内边距
+  const FIXED_COLUMN_PADDING = 10;  // 固定列额外内边距
+  const GROUP_COLUMN_EXTRA = 10; // 分组列额外宽度
+
+  // 需要自适应的列配置
+  const autoColumns = [
+    { prop: 'serial', label: t('iotDevice.serial') },
+    // { prop: 'deviceCode', label: t('iotMaintain.deviceCode') },
+    // { prop: 'deviceName', label: t('iotMaintain.deviceName') },
+    { prop: 'group', label: t('mainPlan.MaintItemsGroup') },
+    {
+      prop: 'totalRunTime',
+      label: t('operationFillForm.sumTime'),
+      getValue: (row) => row.totalRunTime ?? row.tempTotalRunTime
+    },
+    {
+      prop: 'totalMileage',
+      label: t('operationFillForm.sumKil'),
+      getValue: (row) => row.totalMileage ?? row.tempTotalMileage
+    },
+    {
+      prop: 'mainRuntime',
+      label: t('operationFillForm.mainSumTime'),
+      getValue: (row) => row.mainRuntime
+    },
+    {
+      prop: 'mainMileage',
+      label: t('operationFillForm.mainSumKil'),
+      getValue: (row) => row.mainMileage
+    },
+    { prop: 'name', label: t('bomList.bomNode') },
+    { prop: 'lastMaintenanceDate', label: t('mainPlan.lastMaintenanceDate') },
+    { prop: 'mileageRule', label: t('main.mileage') },
+    { prop: 'runningTimeRule', label: t('main.runTime') },
+    { prop: 'naturalDateRule', label: t('main.date') },
+    { prop: 'lastRunningKilometers', label: t('mainPlan.lastMaintenanceMileage') },
+    { prop: 'nextMaintenanceKm', label: t('mainPlan.nextMaintenanceKm') },
+    { prop: 'remainKm', label: t('mainPlan.remainKm') },
+    { prop: 'lastRunningTime', label: t('mainPlan.lastMaintenanceOperationTime') },
+    { prop: 'nextMaintenanceH', label: t('mainPlan.nextMaintenanceH') },
+    { prop: 'remainH', label: t('mainPlan.remainH') },
+    { prop: 'nextRunningTime', label: t('mainPlan.RunTimeCycle') },
+    { prop: 'tempLastNaturalDate', label: t('mainPlan.lastMaintenanceNaturalDate') },
+    { prop: 'nextMaintenanceDate', label: t('mainPlan.nextMaintDate') },
+    { prop: 'remainDay', label: t('mainPlan.remainDay') },
+    { prop: 'rule', label: t('mainPlan.consumeMaterials') }, // 消耗物料规则列
+    { prop: 'operation', label: t('operationFill.operation') }
+  ];
+
+  const newWidths: Record<string, number> = {};
+
+  autoColumns.forEach(col => {
+    const headerText = col.label;
+    // 计算表头宽度
+    const headerWidth = getTextWidth(headerText) * 1.2;
+
+    // 计算内容最大宽度
+    let contentMaxWidth = 0;
+    if (col.prop === 'operation') {
+      // 操作列固定宽度(根据按钮数量)
+      contentMaxWidth = 50;
+    } else if (['mileageRule', 'runningTimeRule', 'naturalDateRule', 'rule'].includes(col.prop)) {
+      // 开关列固定宽度
+      contentMaxWidth = 80;
+    } else {
+      list.value.forEach(row => {
+        const text = col.getValue ? String(col.getValue(row)) : String(row[col.prop] || '');
+        const textWidth = getTextWidth(text);
+        if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
+      });
+    }
+    // 取最大值并添加内边距
+    let finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
+    // 为分组列增加额外宽度
+    if ([
+      'group',
+      'lastRunningKilometers',
+      'nextMaintenanceKm',
+      'remainKm',
+      'lastRunningTime',
+      'nextMaintenanceH',
+      'remainH',
+      'tempLastNaturalDate',
+      'nextMaintenanceDate',
+      'remainDay'
+    ].includes(col.prop)) {
+      finalWidth += GROUP_COLUMN_EXTRA;
+    }
+
+    newWidths[col.prop] = finalWidth;
+  });
+
+  // 固定列特殊处理 - 增加额外空间
+  ['serial', 'name'].forEach(prop => {
+    if (newWidths[prop]) {
+      newWidths[prop] += FIXED_COLUMN_PADDING;
+    }
+  });
+
+  // 转换为CSS宽度值
+  Object.keys(newWidths).forEach(prop => {
+    columnWidths.value[prop] = `${newWidths[prop]}px`;
+  });
+};
+
+// 为每一行建立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)
+}
+
+const tableHeaderStyle = ({ row, rowIndex }) => {
+  return {
+    border: '1px solid #333',
+    backgroundColor: rowIndex === 0 ? '#f0f9eb' : '#f5f7fa' // 分组行特殊背景
+  }
+}
+
+// 更新列表数据的函数
+const updateListWithCumulativeData = (cumulativeData: any[]) => {
+  cumulativeData.forEach(item => {
+    // 根据 bomNodeId 找到对应的行
+    const targetRow = list.value.find(row => row.bomNodeId === item.bomNodeId)
+    if (targetRow) {
+      // 按照优先级设置 mainRuntime 字段的值 保养时长
+      // 优先级:mainRuntime > tempMainRunTime
+      if (item.mainRuntime && Number(item.mainRuntime) > 0) {
+        // 如果 mainRuntime 有值且大于0,使用 mainRuntime
+        targetRow.mainRuntime = Number(item.mainRuntime)
+      } else if (item.tempMainRunTime && Number(item.tempMainRunTime) > 0) {
+        // 如果 mainRuntime 没有有效值,但 tempMainRunTime 有值且大于0,使用 tempMainRunTime
+        targetRow.mainRuntime = Number(item.tempMainRunTime)
+      } else {
+        // 两个字段都没有有效值,保持原值或设置为0
+        targetRow.mainRuntime = 0
+      }
+
+      // 按照优先级设置 mainMileage 字段的值 保养里程数
+      // 优先级:mainMileage > tempMainMileage
+      if (item.mainMileage && Number(item.mainMileage) > 0) {
+        // 如果 mainMileage 有值且大于0,使用 mainMileage
+        targetRow.mainMileage = Number(item.mainMileage)
+      } else if (item.tempMainMileage && Number(item.tempMainMileage) > 0) {
+        // 如果 mainMileage 没有有效值,但 tempMainMileage 有值且大于0,使用 tempMainMileage
+        targetRow.mainMileage = Number(item.tempMainMileage)
+      } else {
+        // 两个字段都没有有效值,保持原值或设置为0
+        targetRow.mainMileage = 0
+      }
+
+      // 更新数据后立即重新校验,清除可能的错误状态
+      nextTick(() => {
+        validateMainRuntime(targetRow)
+      })
+    }
+  })
+}
+
+const fetchCumulativeData = async () => {
+  if (!formData.value.actualStartTime) {
+    console.warn('actualStartTime is empty, skip fetching cumulative data');
+    return;
+  }
+
+  try {
+    // 确保时间戳格式正确
+    const startTime = Number(formData.value.actualStartTime);
+    if (isNaN(startTime)) {
+      console.error('Invalid actualStartTime:', formData.value.actualStartTime);
+      return;
+    }
+
+    const queryParams = {
+      workOrderId: id,
+      actualStartTime: dayjs(startTime).format('YYYY-MM-DD')
+    }
+
+    console.log('Fetching cumulative data with params:', queryParams);
+
+    const response = await IotMainWorkOrderBomApi.maintenanceCumulativeValue(queryParams)
+
+    if (response && Array.isArray(response)) {
+      updateListWithCumulativeData(response);
+      // 在所有数据更新完成后,重新校验所有行的 mainRuntime
+      nextTick(() => {
+        list.value.forEach(row => {
+          // 校验 保养时累计运行时长
+          if (row.mainRuntime) {
+            validateMainRuntime(row);
+          }
+          // 校验 保养时累计运行公里数
+          if (row.mainMileage) {
+            validateMainMileage(row);
+          }
+        });
+      });
+      message.success('累计运行数据更新成功');
+    }
+  } catch (error) {
+    console.error('获取累计运行数据失败:', error);
+    message.error('获取累计运行数据失败');
+  }
+}
+
+// 监听保养开始时间变化
+watch(
+  () => formData.value.actualStartTime,
+  async (newVal, oldVal) => {
+    console.log('actualStartTime changed:', { newVal, oldVal });
+
+    if (newVal && newVal !== oldVal) {
+      // await fetchCumulativeData();
+    }
+  },
+  { immediate: false, deep: true }
+);
+
+// 原有代码保持不变,新增“选择物料”按钮的点击处理方法
+const handleSelectMaterial = () => {
+  // 校验是否已选中保养项(避免无选中项时点击按钮报错)
+  if (!currentBomItem.value) {
+    message.warning('请先在上方表格中选择一个保养项');
+    return;
+  }
+  // 调用现有方法打开物料选择窗口,传入当前选中的保养项
+  openMaterialForm(currentBomItem.value);
+};
+
+// Element UI表格当前行变化事件
+const handleCurrentChangeTable = (currentRow) => {
+  if (currentRow) {
+    handleBomItemClick(currentRow);
+  }
+};
+
+// 监听分页数据和规则变化 - 重新布局表格
+watch([paginatedList, hasMileageRuleInCurrentPage, hasTimeRuleInCurrentPage, hasDateRuleInCurrentPage], () => {
+  nextTick(() => {
+    tableRef.value?.doLayout();
+    calculateAllColumnsWidth(); // 重新计算列宽
+  });
+});
+
+// 监听分页变化时重新计算分组信息
+watch([paginatedList, currentPage], () => {
+  calculateGroupSpans();
+  calculateAllColumnsWidth();
+  tableRef.value?.doLayout(); // 确保表格重新布局
+});
+
+// 监听分页数据变化,自动选中第一条
+watch(() => paginatedList.value, (newList) => {
+  if (newList && newList.length > 0) {
+    // 使用Element UI的方法设置当前行
+    nextTick(() => {
+      if (mainTableRef.value) {
+        mainTableRef.value.setCurrentRow(newList[0]);
+      }
+    });
+
+    // 选中新分页的第一条
+    handleBomItemClick(newList[0]);
+  } else {
+    // 如果没有数据,清空选中状态
+    currentBomItem.value = null;
+    materialList.value = [];
+  }
+}, { immediate: true });
+
+// 下拉菜单命令处理
+const handleDropdownCommand = (command: { action: string; row: IotMainWorkOrderBomVO }) => {
+  switch (command.action) {
+    case 'delay':
+      openConfigDialog(command.row);
+      break;
+    case 'material':
+      openMaterialForm(command.row);
+      break;
+    case 'deviceBomMaterials':
+      openDeviceBomMaterials(command.row);
+      break;
+    case 'detail':
+      handleView(command.row);
+      break;
+  }
+};
+
+/** 校验表格数据 */
+const validateTableData = (): boolean => {
+  let isValid = true;
+  const errorMessages: string[] = []; // 通用错误集合
+  const materialRequiredErrors: string[] = []; // 物料缺失专用错误集合
+
+  // 1. 基础校验:工单明细是否存在
+  if (list.value.length === 0) {
+    message.error('工单明细不存在');
+    return false;
+  }
+
+  // 2. 校验设备状态
+  list.value.forEach((row, index) => {
+    const rowNumber = index + 1;
+    if (row.deviceCode === null || row.deviceName === null) {
+      errorMessages.push(`第${rowNumber}行设备状态错误`);
+      isValid = false;
+    }
+  });
+
+  // 3. 校验物料必填(仅在非委外模式下)
+  if (formData.value.outsourcingFlag !== 1) {
+    list.value.forEach((row, index) => {
+      const rowNumber = index + 1;
+      const deviceIdentifier = `${row.deviceCode}-${formatMaintItemName(row.name)}`;
+
+      // 检查消耗物料规则,如果为1(不消耗物料)则跳过校验
+      if (row.rule === 1) {
+        return; // 跳过当前保养项的物料校验
+      }
+
+      // 检查是否设置了推迟保养
+      const hasDelay =
+        (row.delayKilometers || 0) > 0 ||
+        (row.delayNaturalDate || 0) > 0 ||
+        (row.delayDuration || 0) > 0;
+
+      // 未设置推迟保养时,需要校验物料
+      if (!hasDelay) {
+        // 使用 bomMaterialsMap 获取物料,更可靠
+        const uniqueKey = `${row.bomNodeId}`;
+        const materials = bomMaterialsMap.value[uniqueKey] || [];
+
+        // 过滤有效的物料(非禁用、非无效状态)
+        const validMaterials = materials.filter(material =>
+          material.materialName && // 物料名称不为空
+          material.unit && // 单位不为空
+          (material.unitPrice || 0) > 0 && // 单价大于0
+          (material.quantity || 0) > 0 // 数量大于0
+        );
+
+        if (validMaterials.length === 0) {
+          materialRequiredErrors.push(`第${rowNumber}行【${deviceIdentifier}】未添加有效物料`);
+          isValid = false;
+        }
+      }
+    });
+  }
+
+  // 4. 智能错误提示
+  if (!isValid) {
+    // 构建错误消息HTML
+    let errorHtml = '';
+
+    // 添加通用错误
+    if (errorMessages.length > 0) {
+      errorHtml += errorMessages.join('<br>');
+    }
+
+    // 添加物料错误(带智能截断)
+    if (materialRequiredErrors.length > 0) {
+      if (errorHtml) errorHtml += '<br>'; // 添加换行分隔
+
+      if (materialRequiredErrors.length > 3) {
+        errorHtml += materialRequiredErrors.slice(0, 3).join('<br>');
+        errorHtml += `<br>...等共 ${materialRequiredErrors.length} 个保养项未添加物料`;
+      } else {
+        errorHtml += materialRequiredErrors.join('<br>');
+      }
+    }
+
+    // 添加标题
+    const title = "<span style='font-weight:bold;color:#f56c6c'></span>";
+    errorHtml = title + errorHtml;
+
+    // 显示带格式的错误消息
+    message.error({
+      message: errorHtml,
+      dangerouslyUseHTMLString: true,
+      duration: 8000  // 延长显示时间
+    });
+  }
+
+  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()
+}
+
+// 防抖函数实现
+function debounce(func: Function, wait: number) {
+  let timeout: ReturnType<typeof setTimeout> | null
+  return function executedFunction(...args: any[]) {
+    const later = () => {
+      clearTimeout(timeout!)
+      func(...args)
+    }
+    clearTimeout(timeout!)
+    timeout = setTimeout(later, wait)
+  }
+}
+
+// 响应窗口大小变化
+const handleResize = debounce(calculateMaintItemsWidth, 300)
+
+onMounted(async () => {
+  materialList.value = []
+  const deptId = useUserStore().getUser.deptId
+  // 查询当前登录人所属部门名称
+  dept.value = await DeptApi.getDept(deptId)
+  deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+  formData.value.deptId = deptId
+  try{
+    formType.value = 'update'
+    // 查询保养工单 主表数据
+    const workOrder = await IotMainWorkOrderApi.getIotMainWorkOrder(id);
+    formData.value = workOrder
+    // 查询保养工单 明细数据
+    const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams);
+    list.value = []
+    if (Array.isArray(data)) {
+      list.value = data.map(item => {
+          // 记录初始状态,用于判断是否可编辑
+          const initialStatus = item.status;
+
+          // 提取分组名称
+          const group = item.name && item.name.includes('->')
+            ? item.name.split('->')[0].trim()
+            : '';
+
+          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,
+            group,
+            initialStatus, // 保存初始状态
+            lastNaturalDate: item.lastNaturalDate,
+            lastMaintenanceDate: item.lastMaintenanceDate
+              ? dayjs(item.lastMaintenanceDate).format("YYYY-MM-DD")  // 时间戳 → 日期字符串
+              : null, // 处理空值
+            // 设置consumableRule默认值,如果后端没有返回则默认为0
+            rule: item.rule !== undefined ? Number(item.rule) : 0
+          }
+        })
+      // 设备信息提取逻辑
+      if (list.value.length > 0) {
+        // 从第一个保养项提取设备信息(所有保养项共享相同设备)
+        formData.value.deviceCode = list.value[0].deviceCode;
+        formData.value.deviceName = list.value[0].deviceName;
+      }
+    }
+    // 查询当前保养工单已经关联的所有物料
+    const materials = await IotMainWorkOrderBomMaterialApi.getWorkOrderBomMaterials(queryParams);
+    // 重新初始化物料映射,确保接口数据完全覆盖
+    const tempBomMaterialsMap = {};
+    // 首先,用 getWorkOrderBomMaterials 返回的数据初始化映射(主数据源)
+    if (Array.isArray(materials) && materials.length > 0) {
+      materials.forEach(material => {
+        const uniqueKey = `${material.bomNodeId}`;
+        if (!tempBomMaterialsMap[uniqueKey]) {
+          tempBomMaterialsMap[uniqueKey] = [];
+        }
+
+        // 通过 bomNodeId 在 list 中查找对应的保养项,获取 deviceId
+        const correspondingItem = list.value.find(item => item.bomNodeId === material.bomNodeId);
+        const deviceId = correspondingItem ? correspondingItem.deviceId : null;
+
+        const materialWithDeviceId = {
+          ...material,
+          deviceId: deviceId
+        };
+
+        // 避免重复添加
+        const existingIndex = tempBomMaterialsMap[uniqueKey].findIndex(item =>
+          item.materialCode === material.materialCode &&
+          item.factoryId === material.factoryId &&
+          item.costCenterId === material.costCenterId &&
+          item.storageLocationId === material.storageLocationId
+        );
+
+        if (existingIndex === -1) {
+          tempBomMaterialsMap[uniqueKey].push(materialWithDeviceId);
+        } else {
+          tempBomMaterialsMap[uniqueKey][existingIndex] = materialWithDeviceId;
+        }
+      });
+    }
+    // 其次,对于 getWorkOrderBOMs 接口返回的 deviceBomMaterials,
+    // 只有当 getWorkOrderBomMaterials 中没有对应保养项的物料时,才使用 deviceBomMaterials 作为备用
+    list.value.forEach(item => {
+      const uniqueKey = `${item.bomNodeId}`;
+
+      // 如果 getWorkOrderBomMaterials 中没有这个保养项的物料,且 deviceBomMaterials 有值,则使用备用数据
+      if ((!tempBomMaterialsMap[uniqueKey] || tempBomMaterialsMap[uniqueKey].length === 0) &&
+        item.deviceBomMaterials && item.deviceBomMaterials.length > 0) {
+
+        tempBomMaterialsMap[uniqueKey] = item.deviceBomMaterials.map(material => ({
+          ...material,
+          materialName: material.name,
+          materialCode: material.code,
+          projectDepartment: material.storageLocation,
+          totalInventoryQuantity: material.stockQuantity,
+          deviceId: item.deviceId
+        }));
+      } else if (!tempBomMaterialsMap[uniqueKey]) {
+        // 确保每个保养项在映射中都有对应的数组(即使是空的)
+        tempBomMaterialsMap[uniqueKey] = [];
+      }
+    });
+
+    // 更新响应式映射
+    bomMaterialsMap.value = tempBomMaterialsMap;
+
+    // 如果有数据,选中第一个保养项并显示其物料
+    if (list.value.length > 0) {
+      // 确保第一个保养项的物料正确显示
+      const firstItem = list.value[0];
+      const uniqueKey = `${firstItem.bomNodeId}`;
+      materialList.value = bomMaterialsMap.value[uniqueKey] || [];
+
+      // 使用Element UI的方法设置当前行
+      nextTick(() => {
+        if (mainTableRef.value) {
+          mainTableRef.value.setCurrentRow(firstItem);
+        }
+      });
+
+      handleBomItemClick(firstItem);
+    }
+    // 页面初始化完成后立即计算费用
+    nextTick(() => {
+      calculateTotalCost();
+    });
+  } catch (error) {
+    console.error('数据加载失败:', error)
+    message.error('数据加载失败,请重试')
+  }
+  nextTick(() => {
+    calculateAllColumnsWidth()
+    window.addEventListener('resize', calculateAllColumnsWidth);
+  })
+
+  // 初始化 保养项 分组信息
+  calculateGroupSpans();
+})
+
+onUnmounted(async () => {
+  window.removeEventListener('resize', calculateAllColumnsWidth);
+})
+
+// 处理保养项点击事件
+const handleBomItemClick = (item) => {
+  // 移除之前的高亮
+  /* if (currentBomItem.value) {
+    currentBomItem.value.isSelected = false;
+  } */
+
+  // 设置当前选中项
+  // item.isSelected = true;
+  currentBomItem.value = item;
+  // 切换到当前保养项物料视图
+  showAllMaterials.value = false;
+
+  // 使用新的刷新方法
+  refreshCurrentBomItemMaterials();
+
+  // 切换保养项时重新计算费用
+  nextTick(() => {
+    calculateTotalCost();
+  });
+}
+
+// 为表格行添加点击事件处理
+// 在el-table上添加row-click事件
+// <el-table ... @row-click="handleRowClick">
+const handleRowClick = (row) => {
+  handleBomItemClick(row);
+}
+
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+
+:deep(.el-input-number .el-input__inner) {
+  text-align: left !important;
+  padding-left: 10px; /* 保持左侧间距 */
+}
+
+/* 分组容器样式 */
+.form-group {
+  position: relative;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 20px 15px 10px;
+  margin-bottom: 18px;
+  transition: border-color 0.2s;
+}
+
+/* 分组标题样式 */
+.group-title {
+  position: absolute;
+  top: -10px;
+  left: 20px;
+  background: white;
+  padding: 0 8px;
+  color: #606266;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.error-text {
+  color: #f56c6c;
+  font-size: 12px;
+  margin-top: 5px;
+}
+
+:deep(.el-table__body) {
+  .el-table__cell {
+    .cell {
+      word-break: break-word;
+      max-width: 600px; /* 最大宽度限制 */
+    }
+  }
+}
+
+.full-content-cell {
+  white-space: nowrap; /* 禁止换行 */
+  overflow: visible;   /* 允许内容溢出单元格 */
+}
+
+/* 分组表头样式 */
+:deep(.el-table__header) {
+  border: 1px solid #dcdfe6 !important;
+}
+:deep(.el-table__header th) {
+  border-right: 1px solid #dcdfe6 !important;
+  border-bottom: 1px solid #dcdfe6 !important;
+}
+
+:deep(.el-table__header .is-group th) {
+  background-color: #f5f7fa !important;
+  border-bottom: 1px solid #dcdfe6 !important;
+  font-weight: 600;
+  position: relative;
+}
+:deep(.el-table__header .is-group th::after) {
+  display: none !important;
+}
+/* 分组标题下的子表头单元格 */
+:deep(.el-table__header .el-table__cell:not(.is-group)) {
+  border-top: 1px solid #dcdfe6 !important; /* 添加顶部边框连接分组标题 */
+}
+
+/* 分组行样式 - 符合图片效果 */
+:deep(.el-table .group-row) {
+  background-color: #f8f8f9; /* 轻微的背景色区分 */
+}
+
+/* 分组单元格样式 */
+:deep(.el-table .group-cell) {
+  font-weight: 600; /* 加粗字体 */
+  vertical-align: middle; /* 垂直居中 */
+}
+
+/* 分组第一行单元格样式 - 添加底部边框 */
+:deep(.el-table .group-first-row) {
+  position: relative;
+}
+
+/* 使用伪元素创建更明显的底部边框 */
+:deep(.el-table .group-first-row::after) {
+  content: '';
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 2px;
+  background-color: #606266; /* 深灰色边框 */
+  z-index: 1;
+}
+
+/* 调整分组行的高度,使边框更明显 */
+:deep(.el-table .el-table__row.group-first-row) {
+  border-bottom: none; /* 移除默认边框 */
+}
+
+/* 增强高亮行样式的特异性,确保覆盖Element UI的默认样式 */
+:deep(.el-table__body tr.current-row>td) {
+  background-color: #d1eaff !important;
+}
+
+:deep(.el-table__body tr.current-row:hover>td) {
+  background-color: #b8dfff !important;
+}
+
+/* 物料列表操作区域样式 */
+.material-list-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+.material-list-title {
+  margin: 0;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+/* 单价列统一容器样式:强制居左,固定宽度,对齐基准一致 */
+.unit-price-container {
+  width: 100%;
+  text-align: left; /* 强制内容居左 */
+  padding: 0 4px; /* 可选:添加轻微内边距,避免内容贴边 */
+  box-sizing: border-box;
+}
+
+/* 单价输入框样式:消除默认内边距差异,与静态文本对齐 */
+:deep(.unit-price-input .el-input__inner) {
+  text-align: left !important; /* 覆盖el-input默认居中对齐 */
+  padding-left: 8px !important; /* 统一输入框内边距,与文本缩进匹配 */
+  padding-right: 8px !important;
+}
+
+/* 单价静态文本样式:与输入框保持一致的内边距和对齐 */
+.unit-price-text {
+  display: inline-block; /* 转为行内块,支持padding */
+  width: 100%;
+  padding: 4px 8px; /* 与输入框内边距匹配(输入框默认height约32px,padding上下4px) */
+  box-sizing: border-box;
+  vertical-align: middle; /* 确保与输入框垂直对齐 */
+}
+
+/* 禁用物料行的样式 */
+:deep(.disabled-material-row) {
+  background-color: #f5f7fa !important;
+  color: #c0c4cc !important;
+  cursor: not-allowed !important;
+}
+
+:deep(.disabled-material-row:hover>td) {
+  background-color: #f5f7fa !important;
+}
+
+:deep(.disabled-material-row .el-input.is-disabled .el-input__inner) {
+  background-color: #f5f7fa !important;
+  color: #c0c4cc !important;
+  cursor: not-allowed !important;
+}
+
+:deep(.disabled-material-row .el-input-number.is-disabled) {
+  opacity: 0.6;
+  cursor: not-allowed !important;
+}
+
+:deep(.disabled-material-row .el-button.is-disabled) {
+  opacity: 0.6;
+  cursor: not-allowed !important;
+}
+
+/* 新增物料行的样式 */
+:deep(.el-table__body tr.new-material-row) {
+  background-color: #f0f9ff !important;
+}
+
+:deep(.el-table__body tr.new-material-row:hover>td) {
+  background-color: #e6f7ff !important;
+}
+
+/* 新增物料输入框样式 */
+:deep(.new-material-row .el-input .el-input__inner) {
+  background-color: white !important;
+  border: 1px solid #dcdfe6 !important;
+}
+
+:deep(.new-material-row .el-input-number .el-input__inner) {
+  background-color: white !important;
+  border: 1px solid #dcdfe6 !important;
+}
+
+/* 必填输入框红色边框样式(含hover状态) */
+:deep(.is-required-input .el-input__inner) {
+  border-color: #f56c6c !important; /* Element错误色 */
+  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important; /* 错误阴影 */
+}
+:deep(.is-required-input .el-input-number__input),
+:deep(.error-input .el-input-number__input) {
+  border-color: #f56c6c !important;
+  background-color: #fef0f0 !important;
+  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important;
+}
+
+/* 鼠标悬停时保持红色边框(覆盖Element默认hover样式) */
+:deep(.is-required-input .el-input__inner:hover) {
+  border-color: #f56c6c !important;
+}
+:deep(.is-required-input .el-input-number__input:hover),
+:deep(.error-input .el-input-number__input:hover) {
+  border-color: #f56c6c !important;
+  background-color: #fef0f0 !important;
+}
+
+:deep(.is-required-input .el-input-number__input:focus),
+:deep(.error-input .el-input-number__input:focus) {
+  border-color: #f56c6c !important;
+  background-color: #fef0f0 !important;
+  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.2) !important;
+}
+
+/* 状态列容器样式 - 水平排列 */
+.status-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px; /* 设置开关和文本之间的间距 */
+  white-space: nowrap; /* 防止换行 */
+}
+
+/* 状态开关样式 */
+.status-switch {
+  flex-shrink: 0; /* 防止开关被压缩 */
+}
+
+/* 状态列样式优化 */
+.status-text {
+  font-size: 12px;
+  color: #666;
+  min-width: 40px; /* 为文本设置最小宽度,确保对齐 */
+  text-align: left;
+  flex-shrink: 0; /* 防止文本被压缩 */
+}
+
+/* 禁用状态的switch样式 */
+:deep(.el-switch.is-disabled) {
+  opacity: 0.6;
+}
+
+/* 已完成状态的特殊样式 */
+:deep(.status-completed .el-switch__core) {
+  background-color: #67c23a;
+  border-color: #67c23a;
+}
+
+/* 保养中状态的样式 */
+:deep(.status-maintaining .el-switch__core) {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+
+/* 延迟状态的样式 */
+:deep(.status-delayed .el-switch__core) {
+  background-color: #e6a23c;
+  border-color: #e6a23c;
+}
+
+/* 自定义淡红色背景的 tooltip */
+:deep(.main-runtime-tooltip) {
+  background: #fef0f0 !important;
+  border: 1px solid #fbc4c4 !important;
+  color: #f56c6c !important;
+  max-width: 300px;
+  font-size: 12px;
+  padding: 8px 12px;
+}
+
+/* 隐藏 Tooltip 菱形箭头(核心需求) */
+:deep(.main-runtime-tooltip .el-tooltip__arrow),
+:deep(.main-runtime-tooltip .el-tooltip__arrow::before) {
+  display: none !important; /* 完全隐藏箭头及伪元素 */
+  width: 0 !important;
+  height: 0 !important;
+}
+
+/* 新增包装层样式,确保tooltip正确触发 */
+.main-runtime-input-wrapper {
+  width: 100%;
+  position: relative;
+}
+
+/* 负数值红色样式 */
+:deep(.negative-value) {
+  color: #f56c6c !important;
+  font-weight: 600;
+}
+
+/* 确保在表格单元格中正确显示 */
+:deep(.el-table .cell .negative-value) {
+  display: inline-block;
+  width: 100%;
+}
+</style>

+ 26 - 0
src/views/pms/iotmainworkorder/index.vue

@@ -171,6 +171,16 @@
               >
                 {{ t('workOrderMaterial.back')  }}
               </el-button>
+              <el-button
+                link
+                type="warning"
+                class="warning-btn"
+                @click="openModifyForm('modify', scope.row.id)"
+                v-hasPermi="['pms:iot-main-work-order:update']"
+                v-if="scope.row.result === 2 && scope.row.status === 1 && currentUserId?.toString() === scope.row.responsiblePerson"
+              >
+                {{ t('modelTemplate.update')  }}
+              </el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -226,6 +236,7 @@ import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkor
 import IotMainWorkOrderForm from './IotMainWorkOrderForm.vue'
 import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
 import DeptTree from "@/views/system/user/DeptTree.vue";
+import { useUserStore } from "@/store/modules/user";
 const { push } = useRouter() // 路由跳转
 
 /** 保养工单 列表 */
@@ -234,6 +245,9 @@ defineOptions({ name: 'IotMainWorkOrder' })
 // 表单引用
 const delayReasonFormRef = ref<InstanceType<typeof ElForm>>()
 
+// 定义响应式变量存储当前登录人ID
+const currentUserId = ref<number | undefined>(undefined);
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const tableRef = ref() // 表格引用
@@ -596,6 +610,14 @@ const openForm = (type: string, id?: number) => {
   }
 }
 
+const openModifyForm = (type: string, id?: number) => {
+  // 修改
+  if (typeof id === 'number') {
+    push({ name: 'IotMainWorkOrderModify', params: {id } })
+    return
+  }
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
@@ -633,6 +655,10 @@ let resizeObserver: ResizeObserver | null = null;
 
 /** 初始化 **/
 onMounted(() => {
+  // 获取当前登录人的id 与 保养工单创建人id 匹配修改退回工单 的权限
+  const userId = useUserStore().getUser.id
+  currentUserId.value = userId;
+
   getList()
   window.addEventListener('resize', calculateColumnWidths);
   // 创建 ResizeObserver 监听表格容器尺寸变化