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

pms 保养物料消耗 功能优化

zhangcl преди 1 седмица
родител
ревизия
e075dc9bbb

+ 1 - 1
src/api/pms/iotmainworkorderbom/index.ts

@@ -10,7 +10,7 @@ export interface IotMainWorkOrderBomVO {
   deviceId: number // 设备id
   deviceCode: string  // 设备编码
   deviceName: string  // 设备名称
-  rule: string // 保养规则(量程 运行时间 自然日期) 可多选
+  rule: string // 是否需要更换备件(0需要 1不需要)
   mileageRule: number // 保养规则-里程(0启用 1停用)
   naturalDateRule: number // 保养规则-自然日期(0启用 1停用)
   runningTimeRule: number // 保养规则-运行时间(0启用 1停用)

+ 2 - 1
src/locales/en.ts

@@ -1081,7 +1081,8 @@ export default {
     runningTimeCycleError: 'MCT must be greater than 0',
     maintenanceQuery: 'Maintenance Query',
     notGenerated: "Not Generated",
-    generatedNotExecuted: "Not Executed"
+    generatedNotExecuted: "Not Executed",
+    consumeMaterials: "Consumables"
   },
   inspect:{
     InspectionItems:'InspectionItems',

+ 2 - 1
src/locales/ru.ts

@@ -996,7 +996,8 @@ export default {
     runningTimeCycleError: '运行时间周期必须大于0',
     maintenanceQuery: '保养查询',
     notGenerated: "未生成工单",
-    generatedNotExecuted: "已生成工单未执行"
+    generatedNotExecuted: "已生成工单未执行",
+    consumeMaterials: "消耗物料"
   },
   inspect:{
     InspectionItems:'巡检项',

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

@@ -1077,7 +1077,8 @@ export default {
     runningTimeCycleError: '运行时间周期必须大于0',
     maintenanceQuery: '保养查询',
     notGenerated: "未生成工单",
-    generatedNotExecuted: "已生成工单未执行"
+    generatedNotExecuted: "已生成工单未执行",
+    consumeMaterials: "消耗物料"
   },
   inspect:{
     InspectionItems:'巡检项',

+ 2 - 2
src/router/modules/remaining.ts

@@ -833,8 +833,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'mainworkorder/bom/:id(\\d+)',
-        component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrder.vue'),
-        name: 'IotMainWorkOrderBom',
+        component: () => import('@/views/pms/iotmainworkorder/IotMainWorkOrderOptimize.vue'),
+        name: 'IotMainWorkOrderOptimize',
         meta: {
           noCache: false,
           hidden: true,

+ 2438 - 0
src/views/pms/iotmainworkorder/IotMainWorkOrderOptimize.vue

@@ -0,0 +1,2438 @@
+<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" />
+            </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>
+                <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')"
+              />
+            </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')"
+              />
+            </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="其他费用"
+              />
+            </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 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">
+        <!-- 序号列 -->
+        <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"
+            />
+          </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('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 }">
+              {{ row.remainKm != null ? row.remainKm.toFixed(2) : '-' }}
+            </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 }">
+              {{ row.remainH != null ? row.remainH.toFixed(2) : '-' }}
+            </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)"
+              />
+              <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 }">
+              {{ row.remainDay ?? '-' }}
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column :label="t('common.status')" align="center" width="100">
+          <template #default="scope">
+            {{ getStatusText(scope.row) }}
+          </template>
+        </el-table-column>
+        <!--
+        <el-table-column :label="t('iotMaintain.numberOfMaterials')" align="center" width="90">
+          <template #default="scope">
+            {{ getMaterialCount(scope.row.bomNodeId) }}
+          </template>
+        </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.deviceBomMaterials && scope.row.deviceBomMaterials.length > 0"
+                effect="dark"
+                :content="t('mainPlan.deviceBomMaterials')"
+                placement="top"
+                :show-after="300"
+              >
+                <el-button
+                  type="primary"
+                  link
+                  :icon="View"
+                  @click="handleDropdownCommand({ action: 'deviceBomMaterials', row: scope.row })"
+                  class="action-button"
+                />
+              </el-tooltip> -->
+
+              <!-- 延迟保养按钮 -->
+              <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>
+
+              <!-- 选择物料按钮
+              <el-tooltip
+                v-if="scope.row.status === 0"
+                effect="dark"
+                :content="t('stock.selectMaterial')"
+                placement="top"
+                :show-after="300"
+              >
+                <el-button
+                  type="primary"
+                  link
+                  :icon="Box"
+                  @click="handleDropdownCommand({ action: 'material', row: scope.row })"
+                  class="action-button"
+                />
+              </el-tooltip> -->
+
+              <!-- 物料详情按钮
+              <el-tooltip
+                effect="dark"
+                :content="t('bomList.materialDetail')"
+                placement="top"
+                :show-after="300"
+              >
+                <el-button
+                  type="primary"
+                  link
+                  :icon="Document"
+                  @click="handleDropdownCommand({ action: 'detail', 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"
+        >
+          {{ 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"
+        >
+          新增物料
+        </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%"
+            />
+            <span v-else>{{ row.materialCode }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="物料名称" align="center" prop="materialName" >
+          <template #default="{ row }">
+            <el-input
+              v-if="row.isNew"
+              v-model="row.materialName"
+              placeholder="请输入物料名称"
+              style="width: 100%"
+            />
+            <span v-else>{{ row.materialName }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" prop="unit" width="60px">
+          <template #default="{ row }">
+            <el-input
+              v-if="row.isNew"
+              v-model="row.unit"
+              placeholder="单位"
+              style="width: 100%"
+            />
+            <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-if="row.isNew || !row.unitPrice || row.unitPrice === 0"
+                v-model="row.unitPrice"
+                :precision="2"
+                :min="0"
+                :controls="false"
+                style="width: 100%"
+                class="unit-price-input"
+                :disabled="isMaterialDisabled(row)"
+              />
+              <span v-else class="unit-price-text">{{ row.unitPrice }}</span>
+            </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%"
+              :disabled="isMaterialDisabled(row)"
+            />
+          </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: 'IotMainWorkOrderOptimize' })
+
+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 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);
+  // 如果保养项存在且rule=1(不消耗物料),则禁用
+  return bomItem && bomItem.rule === 1;
+};
+
+// 物料表格行类名 - 用于设置灰色背景
+const getMaterialRowClassName = ({ row }) => {
+  if (row.isNew) {
+    return 'new-material-row';
+  }
+  if (isMaterialDisabled(row)) {
+    return 'disabled-material-row';
+  }
+  return '';
+};
+
+// 切换显示所有物料/当前保养项物料
+const toggleShowAllMaterials = () => {
+  showAllMaterials.value = !showAllMaterials.value;
+
+  // 切换显示模式时,需要同步更新 materialList
+  if (!showAllMaterials.value && currentBomItem.value) {
+    // 切换到显示当前保养项物料
+    const uniqueKey = `${currentBomItem.value.deviceId}${currentBomItem.value.bomNodeId}`;
+    materialList.value = bomMaterialsMap.value[uniqueKey] || [];
+  } else if (showAllMaterials.value) {
+    // 切换到显示所有物料
+    const allMaterials = [];
+    for (const key in bomMaterialsMap.value) {
+      allMaterials.push(...bomMaterialsMap.value[key]);
+    }
+    materialList.value = allMaterials;
+  }
+};
+
+// 为表格行添加类名,实现高亮效果
+const tableRowClassName = ({ row }) => {
+  return row.isSelected ? 'highlight-row' : '';
+};
+
+// 分组合并计算逻辑
+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 // 重置到第一页
+}
+
+// 运行时间周期 全局校验方法(在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;
+};
+
+/** 新增物料 */
+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() // 临时ID用于唯一标识
+  };
+
+  // 生成唯一键
+  const uniqueKey = `${currentBomItem.value.deviceId}${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('已新增空白物料记录,请填写相关信息');
+};
+
+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 selectChoose = (selectedMaterial) => {
+  selectedMaterial.bomNodeId = bomNodeId.value
+  // 关联 bomNodeId
+  const processedMaterials = selectedMaterial.map(material => ({
+    ...material,
+    bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
+  }));
+
+  // 避免重复添加
+  processedMaterials.forEach(newMaterial => {
+    // 检查是否已存在相同 工厂+成本中心+库存地点+bomNodeId + materialCode 的条目
+    const isExist = materialList.value.some(item =>
+      item.bomNodeId === bomNodeId.value &&
+      item.materialCode === newMaterial.materialCode &&
+      item.factoryId === newMaterial.factoryId &&
+      item.costCenterId === newMaterial.costCenterId &&
+      item.storageLocationId === newMaterial.storageLocationId
+    );
+
+    if (!isExist) {
+      materialList.value.push(newMaterial);
+    }
+  });
+}
+
+// 计算属性:判断是否需要显示运行时间周期列
+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 = () => {
+  // 物料总金额 = ∑(单价 * 消耗数量)
+  const materialTotal = materialList.value.reduce((sum, item) => {
+    const price = Number(item.unitPrice) || 0
+    const quantity = Number(item.quantity) || 0
+    return sum + (price * quantity)
+  }, 0)
+
+  // 保养 = 物料总金额
+  formData.value.cost = (materialTotal).toFixed(2)
+}
+
+// 监听物料列表变化
+watch(
+  () => materialList.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) => {
+  console.log('当前BOM节点:' + bomNodeId)
+  return materialList.value.filter(item => item.bomNodeId === bomNodeId).length
+}
+
+// 删除物料(在表格中)
+const handleDeleteMaterialInTable = (material) => {
+  // 确认删除对话框
+  message.confirm('确认删除该物料吗?', { title: '提示' }).then(() => {
+    let targetBomNodeId = material.bomNodeId;
+
+    // 如果没有选中保养项(显示所有物料),需要根据物料信息找到对应的bomNodeId
+    if (showAllMaterials.value && !currentBomItem.value) {
+      // 从物料中提取bomNodeId
+      targetBomNodeId = material.bomNodeId;
+    } else if (currentBomItem.value) {
+      // 使用当前选中保养项的bomNodeId
+      targetBomNodeId = currentBomItem.value.bomNodeId;
+    }
+
+    // 从materialList中删除
+    const index = materialList.value.findIndex(item =>
+      // 对于新记录,使用tempId进行比较;对于已有记录,使用原有逻辑
+      (item.isNew && item.tempId === material.tempId) ||
+      (!item.isNew &&
+        item.bomNodeId === targetBomNodeId &&
+        item.factoryId === material.factoryId &&
+        item.costCenterId === material.costCenterId &&
+        item.storageLocationId === material.storageLocationId &&
+        item.materialCode === material.materialCode)
+    );
+
+    if (index !== -1) {
+      materialList.value.splice(index, 1);
+
+      // 同时从bomMaterialsMap中删除
+      const uniqueKey = `${material.deviceId}${targetBomNodeId}`;
+      if (bomMaterialsMap.value[uniqueKey]) {
+        const mapIndex = bomMaterialsMap.value[uniqueKey].findIndex(item =>
+          (item.isNew && item.tempId === material.tempId) ||
+          (!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('物料删除成功');
+    } else {
+      message.error('未找到要删除的物料');
+    }
+  }).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; // 校验失败则终止提交
+  }
+
+  // 校验表格数据
+  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)
+    }));
+    const data = {
+      mainWorkOrder: formData.value,
+      mainWorkOrderBom: convertedList,
+      mainWorkOrderMaterials: materialList.value
+    }
+    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: '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 handleSelectMaterial = () => {
+  // 校验是否已选中保养项(避免无选中项时点击按钮报错)
+  if (!currentBomItem.value) {
+    message.warning('请先在上方表格中选择一个保养项');
+    return;
+  }
+  // 调用现有方法打开物料选择窗口,传入当前选中的保养项
+  openMaterialForm(currentBomItem.value);
+};
+
+// 监听分页数据和规则变化 - 重新布局表格
+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) {
+    // 取消之前选中的保养项(如果存在)
+    if (currentBomItem.value) {
+      currentBomItem.value.isSelected = false;
+    }
+
+    // 选中新分页的第一条
+    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}-${row.name}`;
+
+      // 检查消耗物料规则,如果为1(不消耗物料)则跳过校验
+      if (row.rule === 1) {
+        return; // 跳过当前保养项的物料校验
+      }
+
+      // 检查是否设置了推迟保养
+      const hasDelay =
+        (row.delayKilometers || 0) > 0 ||
+        (row.delayNaturalDate || 0) > 0 ||
+        (row.delayDuration || 0) > 0;
+
+      // 未设置推迟保养且未选择物料
+      if (!hasDelay && getMaterialCount(row.bomNodeId) === 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 group = item.name && item.name.includes('->')
+            ? item.name.split('->')[0].trim()
+            : '';
+
+          // 处理物料数据映射
+          if (item.deviceBomMaterials && item.deviceBomMaterials.length > 0) {
+            // 生成唯一键
+            const uniqueKey = `${item.deviceId}${item.bomNodeId}`;
+
+            // 转换物料字段映射
+            const mappedMaterials = item.deviceBomMaterials.map(material => ({
+              ...material,
+              materialName: material.name, // name -> materialName
+              materialCode: material.code, // code -> materialCode
+              projectDepartment: material.storageLocation, // storageLocation -> projectDepartment
+              totalInventoryQuantity: material.stockQuantity // stockQuantity -> totalInventoryQuantity
+            }));
+
+            // 存储到映射表中
+            bomMaterialsMap.value[uniqueKey] = mappedMaterials;
+          }
+
+          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,
+            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);
+    materialList.value = []
+    if (Array.isArray(materials)) {
+      materialList.value = materials
+        .map(item => ({
+          ...item,
+        }))
+    }
+    if (list.value.length > 0) {
+      handleBomItemClick(list.value[0]);
+    }
+  } 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;
+  // 生成唯一键
+  const uniqueKey = `${item.deviceId}${item.bomNodeId}`;
+
+  // 更新物料列表
+  materialList.value = bomMaterialsMap.value[uniqueKey] || [];
+}
+
+// 为表格行添加点击事件处理
+// 在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; /* 移除默认边框 */
+}
+
+/* 添加选中行高亮样式 - 增强版 */
+:deep(.el-table .highlight-row) {
+  background-color: #d1eaff !important;
+}
+
+:deep(.el-table .highlight-row:hover>td) {
+  background-color: #b8dfff !important;
+}
+
+:deep(.el-table .highlight-row.current-row>td) {
+  background-color: #99ccff !important;
+  border-bottom: 2px solid #409eff !important;
+}
+
+/* 增强高亮行样式的特异性,确保覆盖Element UI的默认样式 */
+:deep(.el-table__body tr.highlight-row td) {
+  background-color: #d1eaff !important;
+}
+
+:deep(.el-table__body tr.highlight-row:hover td) {
+  background-color: #d9ecff !important;
+}
+
+:deep(.el-table__body tr.highlight-row.current-row td) {
+  background-color: #c6e2ff !important;
+  border-bottom: 2px solid #409eff !important;
+}
+
+/* 针对斑马纹表格的特殊处理 */
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped td) {
+  background-color: #d1eaff !important;
+}
+
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped:hover td) {
+  background-color: #b8dfff !important;
+}
+
+:deep(.el-table--striped .el-table__body tr.highlight-row.el-table__row--striped.current-row td) {
+  background-color: #99ccff !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;
+}
+
+:deep(.disabled-material-row:hover>td) {
+  background-color: #f5f7fa !important;
+}
+
+:deep(.disabled-material-row .el-input-number.is-disabled) {
+  opacity: 0.6;
+}
+
+:deep(.disabled-material-row .el-button.is-disabled) {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+/* 新增物料行的样式 */
+: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;
+}
+
+</style>

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

@@ -120,10 +120,10 @@
             ><Icon icon="ep:check" class="mr-5px" />
             {{ t('workOrderMaterial.confirm') }}</el-button
           >
-          <el-button @click="handleView" type="warning" size="small"
+          <!-- <el-button @click="handleView" type="warning" size="small"
             ><Icon icon="ep:plus" class="mr-5px" />
             {{ t('workOrderMaterial.added') }}</el-button
-          >
+          > -->
         </el-form-item>
       </el-form>
     </ContentWrap>

+ 1 - 1
src/views/pms/iotmainworkorder/index.vue

@@ -427,7 +427,7 @@ const formRef = ref()
 const openForm = (type: string, id?: number) => {
   // 修改
   if (typeof id === 'number') {
-    push({ name: 'IotMainWorkOrderBom', params: {id } })
+    push({ name: 'IotMainWorkOrderOptimize', params: {id } })
     return
   } else {
     push({ name: 'IotMainWorkOrderAdd', params:{} })