Bläddra i källkod

feat(pms-maintenance,zmtable): 重构保养计划配置流程

- 新增新版保养计划管理、保养项配置弹窗和设备选择组件
- 优化保养项配置表单布局,支持按规则动态展示配置项
- 接入累计运行时长、累计运行公里数参数选择
- 使用 Element Plus 表单校验保养项配置,保存前校验启用规则必填项
- 新增保养计划保存/取消流程,并接入创建、编辑保存接口
- 增加明细列表完整性校验,配置完成的序号列显示绿色状态
- 优化保养明细原始数据缓存和累计参数缓存刷新逻辑
- 扩展 ZmTable/ZmTableColumn 支持列级 visible 控制
- 优化 ZmTable 父子列显隐计算,支持动态规则列展示
Zimo 1 vecka sedan
förälder
incheckning
b62cc4e141

+ 13 - 16
src/components/ZmTable/ZmTableColumn.vue

@@ -12,6 +12,7 @@ interface Props
   > {
   prop?: (keyof T & string) | (string & {})
   action?: boolean
+  visible?: boolean
   hideInColumnSettings?: boolean
   isParent?: boolean
   zmSortable?: boolean
@@ -21,7 +22,7 @@ interface Props
   zmSortMethod?: (prop: string, order: SortOrder | null) => void
   filterActive?: boolean
   filterModelValue?: any
-  realValue?: (...args: any[]) => any
+  realValue?: (...args: T[]) => any
   coverFormatter?: boolean
 }
 
@@ -60,12 +61,14 @@ const forwardedSlots = computed(() => {
 
 const defaultOptions = ref<Partial<Props>>({
   align: 'center',
-  resizable: true
+  resizable: true,
+  visible: true
 })
 
 const bindProps = computed(() => {
   const {
     action,
+    visible,
     hideInColumnSettings,
     zmSortable,
     zmFilterable,
@@ -221,6 +224,7 @@ const calculativeWidth = () => {
   if (hasHeaderAction.value) labelWidth += 8
   if (props.zmFilterable) labelWidth += 22
   if (props.zmSortable) labelWidth += 22
+
   const maxWidth = Math.min(
     Math.max(...values.map((value) => getTextWidth(String(value)) + 38), labelWidth),
     tableContext.columnMaxWidth.value
@@ -259,20 +263,17 @@ watch(
                     : '点击升序'
               "
               placement="top"
-              :show-after="500"
-            >
+              :show-after="500">
               <button
                 type="button"
                 class="icon-btn"
                 :class="{ 'is-active': isSortActive }"
-                @click.stop="handleSortClick"
-              >
+                @click.stop="handleSortClick">
                 <div v-if="currentOrder === 'asc'" class="sort-icon i-lucide:arrow-up-narrow-wide">
                 </div>
                 <div
                   v-else-if="currentOrder === 'desc'"
-                  class="sort-icon i-lucide:arrow-down-wide-narrow"
-                >
+                  class="sort-icon i-lucide:arrow-down-wide-narrow">
                 </div>
                 <div v-else class="sort-icon i-lucide:arrow-up-down"></div>
               </button>
@@ -285,15 +286,13 @@ watch(
               :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [16, 18] } }] }"
               trigger="click"
               :width="260"
-              :show-arrow="false"
-            >
+              :show-arrow="false">
               <template #reference>
                 <button
                   type="button"
                   class="icon-btn"
                   :class="{ 'is-active': isFilterActive }"
-                  @click.stop="handleFilterReferenceClick"
-                >
+                  @click.stop="handleFilterReferenceClick">
                   <div class="filter-icon i-lucide:list-filter"></div>
                 </button>
               </template>
@@ -307,8 +306,7 @@ watch(
               trigger="click"
               :width="360"
               :show-arrow="false"
-              popper-class="zm-table-column-setting-popper"
-            >
+              popper-class="zm-table-column-setting-popper">
               <template #reference>
                 <button type="button" class="icon-btn" title="列设置" @click.stop>
                   <div class="setting-icon i-lucide:settings"></div>
@@ -323,8 +321,7 @@ watch(
                     link
                     type="primary"
                     size="small"
-                    @click="tableContext.resetColumnSettings"
-                  >
+                    @click="tableContext.resetColumnSettings">
                     重置
                   </el-button>
                 </div>

+ 22 - 7
src/components/ZmTable/index.vue

@@ -20,6 +20,7 @@ import type { VNode } from 'vue'
 interface ColumnMeta {
   key: string
   label: string
+  visible?: boolean
   action: boolean
   configurable: boolean
   fixed: ColumnFixed
@@ -166,6 +167,7 @@ const getColumnMeta = (node: VNode, index: number, parentKey?: string): ColumnMe
   return {
     key,
     label: getColumnLabel(node, index, parentKey),
+    visible: getPropValue(node.props, 'visible'),
     action,
     configurable: !action && !hideInColumnSettings,
     fixed: normalizeFixed(getPropValue(node.props, 'fixed')),
@@ -318,10 +320,17 @@ const applyColumnSetting = (node: VNode, setting: ColumnSettingItem) => {
   } as VNode
 }
 
-const isColumnVisible = (setting: ColumnSettingItem) => {
-  if (setting.visible === false) return false
-  if (!setting.children?.length) return true
-  return setting.children.some(isColumnVisible)
+const getSettingVisible = (meta: ColumnMeta, setting?: ColumnSettingItem) => {
+  return meta.visible ?? setting?.visible ?? true
+}
+
+const isColumnVisible = (meta: ColumnMeta, setting?: ColumnSettingItem) => {
+  if (!getSettingVisible(meta, setting)) return false
+  if (!meta.children.length) return true
+
+  const childrenSettings = setting?.children || []
+  const childSettingMap = new Map(childrenSettings.map((item) => [item.key, item]))
+  return meta.children.some((child) => isColumnVisible(child, childSettingMap.get(child.key)))
 }
 
 const renderColumnNodes = (nodes: VNode[], settings: ColumnSettingItem[], parentKey?: string) => {
@@ -331,7 +340,7 @@ const renderColumnNodes = (nodes: VNode[], settings: ColumnSettingItem[], parent
     .map((node, index) => ({ node, meta: getColumnMeta(node, index, parentKey) }))
     .filter(({ meta }) => {
       const setting = settingMap.get(meta.key)
-      return meta.configurable && setting && isColumnVisible(setting)
+      return meta.configurable && setting && isColumnVisible(meta, setting)
     })
     .sort((a, b) => {
       const orderA = orderMap.get(a.meta.key) ?? Number.MAX_SAFE_INTEGER
@@ -342,13 +351,19 @@ const renderColumnNodes = (nodes: VNode[], settings: ColumnSettingItem[], parent
   return nodes.flatMap((node, index) => {
     const meta = getColumnMeta(node, index, parentKey)
 
+    const setting = settingMap.get(meta.key)
+
+    if (!isColumnVisible(meta, setting)) return []
+
     if (!meta.configurable) return [node]
 
     const nextColumn = sortedConfigurableNodes.shift()
     if (!nextColumn) return []
 
-    const setting = settingMap.get(nextColumn.meta.key)
-    return setting ? [applyColumnSetting(nextColumn.node, setting)] : [nextColumn.node]
+    const nextColumnSetting = settingMap.get(nextColumn.meta.key)
+    return nextColumnSetting
+      ? [applyColumnSetting(nextColumn.node, nextColumnSetting)]
+      : [nextColumn.node]
   })
 }
 

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

@@ -822,7 +822,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/add',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
+        component: () => import('@/views/pms/maintenance/maintenance-plan-manage.vue'),
         name: 'IotAddMainPlan',
         meta: {
           noCache: false,
@@ -835,7 +835,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/edit/:id(\\d+)',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
+        component: () => import('@/views/pms/maintenance/maintenance-plan-manage.vue'),
         name: 'IotMainPlanEdit',
         meta: {
           noCache: true,
@@ -848,7 +848,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'maintenanceplan/detail/:id(\\d+)',
-        component: () => import('@/views/pms/maintenance/IotMaintenancePlanManage.vue'),
+        component: () => import('@/views/pms/maintenance/maintenance-plan-manage.vue'),
         name: 'IotMaintenancePlanDetail',
         meta: {
           noCache: true,

+ 178 - 183
src/views/pms/device/maintenance/MaintenanceDetail.vue

@@ -5,30 +5,33 @@
       :model="formData"
       :rules="formRules"
       v-loading="formLoading"
-      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-      label-width="130px"
-    >
+      style="margin-top: 1em; margin-right: 4em; margin-left: 0.5em"
+      label-width="130px">
       <div class="base-expandable-content">
         <el-row>
           <el-col :span="8">
             <el-form-item label="工单名称" prop="name">
-              <el-input type="text" v-model="formData.name" disabled/>
+              <el-input type="text" v-model="formData.name" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="工单编号" prop="orderNumber">
-              <el-input type="text" v-model="formData.orderNumber" disabled/>
+              <el-input type="text" v-model="formData.orderNumber" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="责任人" prop="responsiblePerson">
-              <el-select v-model="formData.responsiblePerson" filterable clearable style="width: 100%" disabled>
+              <el-select
+                v-model="formData.responsiblePerson"
+                filterable
+                clearable
+                style="width: 100%"
+                disabled>
                 <el-option
                   v-for="item in deptUsers"
                   :key="item.id"
                   :label="item.nickname"
-                  :value="item.id"
-                />
+                  :value="item.id" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -39,8 +42,7 @@
                   v-for="dict in getIntDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -51,14 +53,13 @@
                   v-for="dict in getIntDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="保养费用(元)" prop="cost">
-              <el-input type="text" v-model="formData.cost" disabled/>
+              <el-input type="text" v-model="formData.cost" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
@@ -69,8 +70,7 @@
                 type="datetime"
                 value-format="x"
                 placeholder="实际保养开始时间"
-                disabled
-              />
+                disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
@@ -81,18 +81,21 @@
                 type="datetime"
                 value-format="x"
                 placeholder="实际保养结束时间"
-                disabled
-              />
+                disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="其他费用(元)" prop="otherCost">
-              <el-input type="text" v-model="formData.otherCost" disabled/>
+              <el-input type="text" v-model="formData.otherCost" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="备注" prop="remark">
-              <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" disabled/>
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                placeholder="请输入备注"
+                disabled />
             </el-form-item>
           </el-col>
         </el-row>
@@ -107,8 +110,7 @@
         :model="queryParams"
         ref="queryFormRef"
         :inline="true"
-        label-width="68px"
-      >
+        label-width="68px">
         <!--
         <el-form-item>
           <el-button @click="openForm" type="warning">
@@ -122,17 +124,20 @@
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
         <!-- 添加序号列 -->
-        <el-table-column
-          type="index"
-          label="序号"
-          width="60"
-          align="center"
-        />
-        <el-table-column label="bom节点" align="center" prop="bomNodeId" v-if="false"/>
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column label="bom节点" align="center" prop="bomNodeId" v-if="false" />
         <el-table-column label="设备编码" align="center" prop="deviceCode" />
         <el-table-column label="设备名称" align="center" prop="deviceName" />
-        <el-table-column label="累计运行时间(H)" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
-        <el-table-column label="累计运行公里数(KM)" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column
+          label="累计运行时间(H)"
+          align="center"
+          prop="totalRunTime"
+          :formatter="erpPriceTableColumnFormatter" />
+        <el-table-column
+          label="累计运行公里数(KM)"
+          align="center"
+          prop="totalMileage"
+          :formatter="erpPriceTableColumnFormatter" />
         <el-table-column label="保养项" align="center" prop="name" />
         <el-table-column label="运行里程" key="mileageRule" width="80">
           <template #default="scope">
@@ -140,8 +145,7 @@
               v-model="scope.row.mileageRule"
               :active-value="0"
               :inactive-value="1"
-              :disabled="true"
-            />
+              :disabled="true" />
           </template>
         </el-table-column>
         <el-table-column label="运行时间" key="runningTimeRule" width="80">
@@ -150,8 +154,7 @@
               v-model="scope.row.runningTimeRule"
               :active-value="0"
               :inactive-value="1"
-              :disabled="true"
-            />
+              :disabled="true" />
           </template>
         </el-table-column>
         <el-table-column label="自然日期" key="naturalDateRule" width="80">
@@ -160,8 +163,7 @@
               v-model="scope.row.naturalDateRule"
               :active-value="0"
               :inactive-value="1"
-              :disabled="true"
-            />
+              :disabled="true" />
           </template>
         </el-table-column>
         <el-table-column label="已选物料" align="center" width="100">
@@ -174,11 +176,7 @@
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
               <!-- 新增配置按钮 -->
               <div style="margin-left: 12px">
-                <el-button
-                  link
-                  type="primary"
-                  @click="openConfigDialog(scope.row)"
-                >
+                <el-button link type="primary" @click="openConfigDialog(scope.row)">
                   配置
                 </el-button>
               </div>
@@ -197,8 +195,7 @@
                 <el-button
                   link
                   type="primary"
-                  @click="handleView(scope.row.id, scope.row.bomNodeId)"
-                >
+                  @click="handleView(scope.row.id, scope.row.bomNodeId)">
                   物料详情
                 </el-button>
               </div>
@@ -210,17 +207,25 @@
 
     <!-- 选择的物料列表 -->
     <ContentWrap>
-      <el-table v-loading="false" :data="materialList" :stripe="true" :show-overflow-tooltip="true" v-if="false">
+      <el-table
+        v-loading="false"
+        :data="materialList"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        v-if="false">
         <el-table-column label="bom节点" align="center" prop="bomNodeId" />
         <el-table-column label="物料编码" align="center" prop="materialCode" />
         <el-table-column label="物料名称" align="center" prop="materialName" />
         <el-table-column label="单位" align="center" prop="unit" />
-        <el-table-column label="单价(CNY/元)" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter"/>
+        <el-table-column
+          label="单价(CNY/元)"
+          align="center"
+          prop="unitPrice"
+          :formatter="erpPriceTableColumnFormatter" />
         <el-table-column label="消耗数量" align="center" prop="quantity" />
         <el-table-column label="总库存数量" align="center" prop="totalInventoryQuantity" />
       </el-table>
     </ContentWrap>
-
   </ContentWrap>
   <ContentWrap>
     <el-form>
@@ -232,170 +237,149 @@
   <!-- 新增配置对话框 -->
   <el-dialog
     v-model="configDialog.visible"
-    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
-    width="600px"
-  >
-    <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
+    :title="`设备 ${configDialog.current?.deviceCode + '-' + configDialog.current?.name} 保养配置`"
+    width="600px">
+    <el-form
+      :model="configDialog.form"
+      label-width="200px"
+      :rules="configFormRules"
+      ref="configFormRef">
       <!-- 里程配置 -->
       <el-form-item
         v-if="configDialog.current?.mileageRule === 0"
         label="上次保养里程数(KM)"
-        prop="lastRunningKilometers"
-      >
+        prop="lastRunningKilometers">
         <el-input-number
           v-model="configDialog.form.lastRunningKilometers"
           :precision="2"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <!-- 推迟公里数 -->
       <el-form-item
         v-if="configDialog.current?.mileageRule === 0"
         label="推迟公里数(KM)"
-        prop="delayKilometers"
-      >
+        prop="delayKilometers">
         <el-input-number
           v-model="configDialog.form.delayKilometers"
           :precision="2"
           :min="0"
-          controls-position="right"
-        />
+          controls-position="right" />
       </el-form-item>
       <!-- 运行时间配置 -->
       <el-form-item
         v-if="configDialog.current?.runningTimeRule === 0"
         label="上次保养运行时间(H)"
-        prop="lastRunningTime"
-      >
+        prop="lastRunningTime">
         <el-input-number
           v-model="configDialog.form.lastRunningTime"
           :precision="1"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <!-- 推迟时长 -->
       <el-form-item
         v-if="configDialog.current?.runningTimeRule === 0"
         label="推迟时长(H)"
-        prop="delayDuration"
-      >
+        prop="delayDuration">
         <el-input-number
           v-model="configDialog.form.delayDuration"
           :precision="2"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <!-- 自然日期配置 -->
       <el-form-item
         v-if="configDialog.current?.naturalDateRule === 0"
         label="上次保养自然日期(D)"
-        prop="lastNaturalDate"
-      >
+        prop="lastNaturalDate">
         <el-date-picker
           v-model="configDialog.form.lastNaturalDate"
           type="date"
           placeholder="选择日期"
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <!-- 推迟自然日期 -->
       <el-form-item
         v-if="configDialog.current?.naturalDateRule === 0"
         label="推迟自然日期(D)"
-        prop="delayNaturalDate"
-      >
+        prop="delayNaturalDate">
         <el-input-number
           v-model="configDialog.form.delayNaturalDate"
           :precision="2"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <!-- 保养规则周期值 + 提前量 -->
       <el-form-item
         v-if="configDialog.current?.mileageRule === 0"
         label="运行里程周期(KM)"
-        prop="nextRunningKilometers"
-      >
+        prop="nextRunningKilometers">
         <el-input-number
           v-model="configDialog.form.nextRunningKilometers"
           :precision="2"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <el-form-item
         v-if="configDialog.current?.mileageRule === 0"
         label="运行里程周期-提前量(KM)"
-        prop="kiloCycleLead"
-      >
+        prop="kiloCycleLead">
         <el-input-number
           v-model="configDialog.form.kiloCycleLead"
           :precision="2"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <el-form-item
         v-if="configDialog.current?.runningTimeRule === 0"
         label="运行时间周期(H)"
-        prop="nextRunningTime"
-      >
+        prop="nextRunningTime">
         <el-input-number
           v-model="configDialog.form.nextRunningTime"
           :precision="1"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <el-form-item
         v-if="configDialog.current?.runningTimeRule === 0"
         label="运行时间周期-提前量(H)"
-        prop="timePeriodLead"
-      >
+        prop="timePeriodLead">
         <el-input-number
           v-model="configDialog.form.timePeriodLead"
           :precision="1"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <el-form-item
         v-if="configDialog.current?.naturalDateRule === 0"
         label="自然日周期(D)"
-        prop="nextNaturalDate"
-      >
+        prop="nextNaturalDate">
         <el-input-number
           v-model="configDialog.form.nextNaturalDate"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
       <el-form-item
         v-if="configDialog.current?.naturalDateRule === 0"
         label="自然日周期-提前量(D)"
-        prop="naturalDatePeriodLead"
-      >
+        prop="naturalDatePeriodLead">
         <el-input-number
           v-model="configDialog.form.naturalDatePeriodLead"
           :min="0"
           controls-position="right"
-          :disabled="true"
-        />
+          :disabled="true" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -407,33 +391,28 @@
   <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
   <MaterialListDrawer
     :model-value="drawerVisible"
-    @update:model-value="val => drawerVisible = val"
+    @update:model-value="(val) => (drawerVisible = val)"
     :node-id="currentBomNodeId"
-    :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
-  />
+    :materials="materialList.filter((item) => item.bomNodeId === currentBomNodeId)" />
 </template>
 <script setup lang="ts">
-import { IotMaintainApi, IotMaintainVO } from '@/api/pms/maintain'
-import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
 import { ref } from 'vue'
-import type { ComponentPublicInstance } from 'vue'
-import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
-import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
-import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
-import { IotMaintenancePlanApi, IotMaintenancePlanVO } from '@/api/pms/maintenance'
-import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
+import { IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
+import { IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
+import {
+  IotMainWorkOrderBomMaterialApi,
+  IotMainWorkOrderBomMaterialVO
+} from '@/api/pms/iotmainworkorderbommaterial'
+import { IotMainWorkOrderApi } from '@/api/pms/iotmainworkorder'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
-import MainPlanDeviceList from "@/views/pms/maintenance/MainPlanDeviceList.vue";
-import * as DeptApi from "@/api/system/dept";
-import {erpPriceTableColumnFormatter} from "@/utils";
+import * as DeptApi from '@/api/system/dept'
+import { erpPriceTableColumnFormatter } from '@/utils'
 import dayjs from 'dayjs'
-import MaterialListDrawer from "@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue";
-import WorkOrderMaterial from "@/views/pms/iotmainworkorder/WorkOrderMaterial.vue";
-import {IotMaintainMaterialsApi} from "@/api/pms/maintain/materials";
-import {DICT_TYPE, getIntDictOptions, getStrDictOptions} from "@/utils/dict";
+import MaterialListDrawer from '@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue'
+import WorkOrderMaterial from '@/views/pms/iotmainworkorder/WorkOrderMaterial.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 /** 保养计划 表单 */
 defineOptions({ name: 'IotMainWorkOrderDetail' })
@@ -468,11 +447,11 @@ const formData = ref({
   type: undefined,
   result: undefined,
   remark: undefined,
-  status: undefined,
+  status: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
-  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -480,7 +459,7 @@ interface MaterialFormExpose {
   open: (deptId: number, bomNodeId: number) => void
 }
 
-const materialFormRef = ref<MaterialFormExpose>();
+const materialFormRef = ref<MaterialFormExpose>()
 
 // 新增配置相关状态
 const configDialog = reactive({
@@ -539,7 +518,7 @@ const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
 
 // const materialFormRef = ref()
 const openMaterialForm = (row: any) => {
-  bomNodeId.value = row.bomNodeId;
+  bomNodeId.value = row.bomNodeId
   console.log('这是一个对象:', row.bomNodeId)
   materialFormRef.value.open(formData.value.deptId, bomNodeId.value)
 }
@@ -547,23 +526,22 @@ const openMaterialForm = (row: any) => {
 const selectChoose = (selectedMaterial) => {
   selectedMaterial.bomNodeId = bomNodeId.value
   // 关联 bomNodeId
-  const processedMaterials = selectedMaterial.map(material => ({
+  const processedMaterials = selectedMaterial.map((material) => ({
     ...material,
     bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
-  }));
+  }))
 
   // 避免重复添加
-  processedMaterials.forEach(newMaterial => {
+  processedMaterials.forEach((newMaterial) => {
     // 检查是否已存在相同 bomNodeId + materialCode 的条目
-    const isExist = materialList.value.some(item =>
-      item.bomNodeId === bomNodeId.value &&
-      item.materialCode === newMaterial.materialCode
-    );
+    const isExist = materialList.value.some(
+      (item) => item.bomNodeId === bomNodeId.value && item.materialCode === newMaterial.materialCode
+    )
 
     if (!isExist) {
-      materialList.value.push(newMaterial);
+      materialList.value.push(newMaterial)
     }
-  });
+  })
   console.log('选择完成的数据:', JSON.stringify(selectedMaterial))
   console.log('添加到本地列表的数据:', materialList.value)
 }
@@ -587,12 +565,12 @@ const handleView = (nodeId, bomId) => {
 }
 
 const hasMaterial = (bomNodeId: number) => {
-  return materialList.value.some(item => item.bomNodeId === bomNodeId)
+  return materialList.value.some((item) => item.bomNodeId === bomNodeId)
 }
 
 // 保存配置
 const saveConfig = () => {
-  (configFormRef.value as any).validate((valid: boolean) => {
+  ;(configFormRef.value as any).validate((valid: boolean) => {
     if (!valid) return
     if (!configDialog.current) return
 
@@ -608,8 +586,8 @@ const saveConfig = () => {
       requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
     }
 
-    const missingFields = requiredFields.filter(field =>
-      !configDialog.form[field as keyof typeof configDialog.form]
+    const missingFields = requiredFields.filter(
+      (field) => !configDialog.form[field as keyof typeof configDialog.form]
     )
 
     if (missingFields.length > 0) {
@@ -652,7 +630,7 @@ const queryParams = reactive({
 
 const close = () => {
   delView(unref(currentRoute))
-  push({ name: 'IotMainWorkOrder', params:{}})
+  push({ name: 'IotMainWorkOrder', params: {} })
 }
 
 /** 提交表单 */
@@ -693,46 +671,58 @@ const submitForm = async () => {
 
 // 新增表单校验规则
 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'
-  }]
+  nextRunningKilometers: [
+    {
+      required: true,
+      message: '里程周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  kiloCycleLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ],
+  nextRunningTime: [
+    {
+      required: true,
+      message: '时间周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  timePeriodLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ],
+  nextNaturalDate: [
+    {
+      required: true,
+      message: '自然日周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  naturalDatePeriodLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ]
 })
 
 /** 校验表格数据 */
 const validateTableData = (): boolean => {
   let isValid = true
   const errorMessages: string[] = []
-  const noRulesErrorMessages: string[] = []  // 未设置任何保养项规则 的错误提示信息
-  const noRules: string[] = []  // 行记录中设置了保养规则的记录数量
-  const configErrors: string[] = []   // 保养规则配置弹出框
-  let shouldBreak = false;
+  const noRulesErrorMessages: string[] = [] // 未设置任何保养项规则 的错误提示信息
+  const noRules: string[] = [] // 行记录中设置了保养规则的记录数量
+  const configErrors: string[] = [] // 保养规则配置弹出框
+  let shouldBreak = false
 
   if (list.value.length === 0) {
     errorMessages.push('请至少添加一条设备保养明细')
@@ -743,20 +733,24 @@ const validateTableData = (): boolean => {
   }
 
   list.value.forEach((row, index) => {
-    if (shouldBreak) return;
+    if (shouldBreak) return
     const rowNumber = index + 1 // 用户可见的行号从1开始
     const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
     // 校验逻辑
     const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
-      if (ruleValue === 0) { // 规则开启
+      if (ruleValue === 0) {
+        // 规则开启
         if (!row[configField] || row[configField] <= 0) {
-          configErrors.push(`第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`)
+          configErrors.push(
+            `第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`
+          )
           isValid = false
         }
       }
     }
     // 里程校验逻辑
-    if (row.mileageRule === 0) { // 假设 0 表示开启状态
+    if (row.mileageRule === 0) {
+      // 假设 0 表示开启状态
       if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
         errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
         isValid = false
@@ -789,10 +783,10 @@ const validateTableData = (): boolean => {
     // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
     if (noRules.length === 3) {
       isValid = false
-      shouldBreak = true; // 设置标志变量为true,退出循环
+      shouldBreak = true // 设置标志变量为true,退出循环
       noRulesErrorMessages.push('保养项至少设置1个保养规则')
     }
-    noRules.length = 0;
+    noRules.length = 0
   })
   if (errorMessages.length > 0) {
     message.error('设置保养规则后,请维护对应的周期值')
@@ -829,17 +823,17 @@ onMounted(async () => {
   deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
   formData.value.deptId = deptId
   // if (id){
-  try{
+  try {
     formType.value = 'update'
-    const workOrder = await IotMainWorkOrderApi.getDeviceIotWorkOrder(orderId, deviceId);
+    const workOrder = await IotMainWorkOrderApi.getDeviceIotWorkOrder(orderId, deviceId)
     formData.value = workOrder
     // 查询保养责任人
-    const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0;
+    const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0
     UserApi.getUser(personId).then((res) => {
-      formData.value.responsiblePerson = res.nickname;
+      formData.value.responsiblePerson = res.nickname
     })
 
-    const data = workOrder.workOrderBomS;
+    const data = workOrder.workOrderBomS
 
     // // 查询保养工单 主表数据
     // const workOrder = await IotMainWorkOrderApi.getIotMainWorkOrder(id);
@@ -853,10 +847,12 @@ onMounted(async () => {
     // const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams);
     list.value = []
     if (Array.isArray(data)) {
-      list.value = data.map(item => ({
+      list.value = data.map((item) => ({
         ...item,
         // 这里可以添加必要的字段转换(如果有日期等需要格式化的字段)
-        lastNaturalDate: item.lastNaturalDate ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD') : null
+        lastNaturalDate: item.lastNaturalDate
+          ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
+          : null
       }))
     }
   } catch (error) {
@@ -864,7 +860,6 @@ onMounted(async () => {
     message.error('数据加载失败,请重试')
   }
 })
-
 </script>
 <style scoped>
 .base-expandable-content {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 285 - 273
src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 311 - 293
src/views/pms/maintenance/IotMaintenancePlan.vue


+ 212 - 186
src/views/pms/maintenance/IotMaintenancePlanDetail.vue

@@ -5,24 +5,27 @@
       :model="formData"
       :rules="formRules"
       v-loading="formLoading"
-      style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-      label-width="130px"
-    >
+      style="margin-top: 1em; margin-right: 4em; margin-left: 0.5em"
+      label-width="130px">
       <div class="base-expandable-content">
         <el-row>
           <el-col :span="12">
             <el-form-item :label="t('main.planName')" prop="name">
-              <el-input type="text" v-model="formData.name" disabled/>
+              <el-input type="text" v-model="formData.name" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item :label="t('main.planCode')" prop="serialNumber">
-              <el-input type="text" v-model="formData.serialNumber" disabled/>
+              <el-input type="text" v-model="formData.serialNumber" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item :label="t('form.remark')" prop="remark">
-              <el-input v-model="formData.remark" type="textarea" :placeholder="t('deviceForm.remarkHolder')" disabled/>
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                :placeholder="t('deviceForm.remarkHolder')"
+                disabled />
             </el-form-item>
           </el-col>
         </el-row>
@@ -34,26 +37,39 @@
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
         <!-- 添加序号列 -->
-        <el-table-column
-          type="index"
-          :label="t('monitor.serial')"
-          width="70"
-          align="center"
-        />
+        <el-table-column type="index" :label="t('monitor.serial')" width="70" align="center" />
         <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
         <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName" />
-        <el-table-column :label="t('operationFillForm.sumTime')" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter">
+        <el-table-column
+          :label="t('operationFillForm.sumTime')"
+          align="center"
+          prop="totalRunTime"
+          :formatter="erpPriceTableColumnFormatter">
           <template #default="{ row }">
             {{ row.totalRunTime ?? row.tempTotalRunTime }}
           </template>
         </el-table-column>
-        <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter">
+        <el-table-column
+          :label="t('operationFillForm.sumKil')"
+          align="center"
+          prop="totalMileage"
+          :formatter="erpPriceTableColumnFormatter">
           <template #default="{ row }">
             {{ row.totalMileage ?? row.tempTotalMileage }}
           </template>
         </el-table-column>
-        <el-table-column label="tempTotalRunTime" align="center" prop="tempTotalRunTime" :formatter="erpPriceTableColumnFormatter" v-if="false"/>
-        <el-table-column label="tempTotalMileage" align="center" prop="tempTotalMileage" :formatter="erpPriceTableColumnFormatter" v-if="false"/>
+        <el-table-column
+          label="tempTotalRunTime"
+          align="center"
+          prop="tempTotalRunTime"
+          :formatter="erpPriceTableColumnFormatter"
+          v-if="false" />
+        <el-table-column
+          label="tempTotalMileage"
+          align="center"
+          prop="tempTotalMileage"
+          :formatter="erpPriceTableColumnFormatter"
+          v-if="false" />
         <el-table-column :label="t('bomList.bomNode')" align="center" prop="name" />
         <el-table-column :label="t('main.mileage')" key="mileageRule" width="80">
           <template #default="scope">
@@ -61,8 +77,7 @@
               v-model="scope.row.mileageRule"
               :active-value="0"
               :inactive-value="1"
-              disabled
-            />
+              disabled />
           </template>
         </el-table-column>
         <el-table-column :label="t('main.runTime')" key="runningTimeRule" width="90">
@@ -71,8 +86,7 @@
               v-model="scope.row.runningTimeRule"
               :active-value="0"
               :inactive-value="1"
-              disabled
-            />
+              disabled />
           </template>
         </el-table-column>
         <el-table-column :label="t('main.date')" key="naturalDateRule" width="80">
@@ -81,22 +95,16 @@
               v-model="scope.row.naturalDateRule"
               :active-value="0"
               :inactive-value="1"
-              disabled
-            />
+              disabled />
           </template>
         </el-table-column>
         <el-table-column :label="t('workplace.operation')" align="center" min-width="120px">
           <template #default="scope">
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
-              <div>
-              </div>
+              <div> </div>
               <!-- 新增配置按钮 -->
               <div style="margin-left: 12px">
-                <el-button
-                  link
-                  type="primary"
-                  @click="openConfigDialog(scope.row)"
-                >
+                <el-button link type="primary" @click="openConfigDialog(scope.row)">
                   {{ t('form.set') }}
                 </el-button>
               </div>
@@ -109,7 +117,7 @@
   <ContentWrap>
     <el-form>
       <el-form-item style="float: right">
-        <el-button @click="close">{{t('operationFillForm.cancel')}}</el-button>
+        <el-button @click="close">{{ t('operationFillForm.cancel') }}</el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -117,52 +125,54 @@
   <!-- 新增配置对话框 -->
   <el-dialog
     v-model="configDialog.visible"
-    :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养项配置`"
-    width="600px"
-  >
+    :title="`设备 ${configDialog.current?.deviceCode + '-' + configDialog.current?.name} 保养项配置`"
+    width="600px">
     <!-- 使用header插槽自定义标题 -->
     <template #header>
-      <span>设备 <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong> 保养项配置</span>
+      <span
+        >设备
+        <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong>
+        保养项配置</span
+      >
     </template>
-    <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
+    <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"
-        >
+          prop="lastRunningKilometers">
           <el-input-number
             v-model="configDialog.form.lastRunningKilometers"
             :precision="2"
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
         <!-- 运行时间配置 -->
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.lastMaintenanceOperationTime')"
-          prop="lastRunningTime"
-        >
+          prop="lastRunningTime">
           <el-input-number
             v-model="configDialog.form.lastRunningTime"
             :precision="1"
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
         <!-- 自然日期配置 -->
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
           :label="t('mainPlan.lastMaintenanceNaturalDate')"
-          prop="lastNaturalDate"
-        >
+          prop="lastNaturalDate">
           <el-date-picker
             v-model="configDialog.form.lastNaturalDate"
             type="date"
@@ -170,8 +180,7 @@
             format="YYYY-MM-DD"
             value-format="YYYY-MM-DD"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
       </div>
 
@@ -181,8 +190,7 @@
         <el-form-item
           v-if="configDialog.current?.mileageRule === 0"
           :label="t('mainPlan.operatingMileageCycle')"
-          prop="nextRunningKilometers"
-        >
+          prop="nextRunningKilometers">
           <el-input-number
             v-model="configDialog.form.nextRunningKilometers"
             :precision="2"
@@ -190,14 +198,12 @@
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.mileageRule === 0"
           :label="t('mainPlan.OperatingMileageCycle_lead')"
-          prop="kiloCycleLead"
-        >
+          prop="kiloCycleLead">
           <el-input-number
             v-model="configDialog.form.kiloCycleLead"
             :precision="2"
@@ -205,8 +211,7 @@
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
       </div>
 
@@ -215,8 +220,7 @@
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.RunTimeCycle')"
-          prop="nextRunningTime"
-        >
+          prop="nextRunningTime">
           <el-input-number
             v-model="configDialog.form.nextRunningTime"
             :precision="1"
@@ -224,14 +228,12 @@
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.RunTimeCycle_Lead')"
-          prop="timePeriodLead"
-        >
+          prop="timePeriodLead">
           <el-input-number
             v-model="configDialog.form.timePeriodLead"
             :precision="1"
@@ -239,8 +241,7 @@
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
       </div>
 
@@ -248,94 +249,104 @@
         <div class="group-title">{{ t('mainPlan.NaturalDayRuleConfig') }}</div>
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
-          :label="t('mainPlan.NaturalDailyCycle') "
-          prop="nextNaturalDate"
-        >
+          :label="t('mainPlan.NaturalDailyCycle')"
+          prop="nextNaturalDate">
           <el-input-number
             v-model="configDialog.form.nextNaturalDate"
             :min="0"
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
-          :label="t('mainPlan.NaturalDailyCycle_Lead') "
-          prop="naturalDatePeriodLead"
-        >
+          :label="t('mainPlan.NaturalDailyCycle_Lead')"
+          prop="naturalDatePeriodLead">
           <el-input-number
             v-model="configDialog.form.naturalDatePeriodLead"
             :min="0"
             :controls="false"
             controls-position="right"
             style="width: 60%"
-            disabled
-          />
+            disabled />
         </el-form-item>
       </div>
 
       <!-- 运行记录模板中 多个 累计运行时长 累计运行里程 属性匹配-->
-      <div class="form-group"
-           v-if="(configDialog.current?.runningTimeRule === 0 || configDialog.current?.mileageRule === 0)
-            && (configDialog.current?.timeAccumulatedAttrs?.length || configDialog.current?.mileageAccumulatedAttrs?.length)
-            && !configDialog.current.totalRunTime && !configDialog.current.totalMileage" >
+      <div
+        class="form-group"
+        v-if="
+          (configDialog.current?.runningTimeRule === 0 ||
+            configDialog.current?.mileageRule === 0) &&
+          (configDialog.current?.timeAccumulatedAttrs?.length ||
+            configDialog.current?.mileageAccumulatedAttrs?.length) &&
+          !configDialog.current.totalRunTime &&
+          !configDialog.current.totalMileage
+        ">
         <div class="group-title">{{ t('mainPlan.accumulatedParams') }}</div>
         <!-- 累计运行时长 -->
         <el-form-item
-          v-if="configDialog.current?.runningTimeRule === 0
-          && configDialog.current?.timeAccumulatedAttrs?.length && !configDialog.current.totalRunTime"
+          v-if="
+            configDialog.current?.runningTimeRule === 0 &&
+            configDialog.current?.timeAccumulatedAttrs?.length &&
+            !configDialog.current.totalRunTime
+          "
           :label="t('mainPlan.accumulatedRunTime')"
           prop="accumulatedTimeOption"
-          :rules="[{
-            required: configDialog.current?.runningTimeRule === 0 && configDialog.current?.timeAccumulatedAttrs?.length,
-            message: '请选择累计运行时长',
-            trigger: 'change'
-          }]"
-        >
+          :rules="[
+            {
+              required:
+                configDialog.current?.runningTimeRule === 0 &&
+                configDialog.current?.timeAccumulatedAttrs?.length,
+              message: '请选择累计运行时长',
+              trigger: 'change'
+            }
+          ]">
           <el-select
             v-model="configDialog.form.accumulatedTimeOption"
             placeholder="请选择累计运行时长"
             style="width: 80%"
             clearable
             @change="handleAccumulatedTimeChange"
-            disabled
-          >
+            disabled>
             <el-option
               v-for="(item, index) in configDialog.current.timeAccumulatedAttrs"
               :key="`time-${item.pointName}-${index}`"
               :label="item.pointName"
-              :value="item.pointName"
-            />
+              :value="item.pointName" />
           </el-select>
         </el-form-item>
         <!-- 累计运行公里数 -->
         <el-form-item
-          v-if="configDialog.current?.mileageRule === 0
-          && configDialog.current?.mileageAccumulatedAttrs?.length && !configDialog.current.totalMileage"
+          v-if="
+            configDialog.current?.mileageRule === 0 &&
+            configDialog.current?.mileageAccumulatedAttrs?.length &&
+            !configDialog.current.totalMileage
+          "
           :label="t('mainPlan.accumulatedMileage')"
           prop="accumulatedMileageOption"
-          :rules="[{
-            required: configDialog.current?.mileageRule === 0 && configDialog.current?.mileageAccumulatedAttrs?.length,
-            message: '请选择累计运行公里数',
-            trigger: 'change'
-          }]"
-        >
+          :rules="[
+            {
+              required:
+                configDialog.current?.mileageRule === 0 &&
+                configDialog.current?.mileageAccumulatedAttrs?.length,
+              message: '请选择累计运行公里数',
+              trigger: 'change'
+            }
+          ]">
           <el-select
             v-model="configDialog.form.accumulatedMileageOption"
             placeholder="请选择累计运行公里数"
             style="width: 80%"
             clearable
             @change="handleAccumulatedMileageChange"
-            disabled
-          >
+            disabled>
             <el-option
               v-for="(item, index) in configDialog.current.mileageAccumulatedAttrs"
               :key="`mileage-${item.pointName}-${index}`"
               :label="item.pointName"
-              :value="item.pointName"
-            />
+              :value="item.pointName" />
           </el-select>
         </el-form-item>
       </div>
@@ -344,20 +355,19 @@
       <el-button @click="configDialog.visible = false">{{ t('common.cancel') }}</el-button>
     </template>
   </el-dialog>
-
 </template>
 <script setup lang="ts">
-import { IotMaintainApi, IotMaintainVO } from '@/api/pms/maintain'
-import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { IotMaintainApi } from '@/api/pms/maintain'
+import { IotDeviceApi } from '@/api/pms/device'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
 import { ref } from 'vue'
 import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
-import { IotMaintenancePlanApi, IotMaintenancePlanVO } from '@/api/pms/maintenance'
+import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import MainPlanDeviceList from "@/views/pms/maintenance/MainPlanDeviceList.vue";
-import * as DeptApi from "@/api/system/dept";
-import {erpPriceTableColumnFormatter} from "@/utils";
+import MainPlanDeviceList from '@/views/pms/maintenance/maintenance-device-list.vue'
+import * as DeptApi from '@/api/system/dept'
+import { erpPriceTableColumnFormatter } from '@/utils'
 import dayjs from 'dayjs'
 
 /** 保养计划 表单 */
@@ -386,11 +396,11 @@ const formData = ref({
   responsiblePerson: undefined,
   remark: undefined,
   failureName: undefined,
-  status: undefined,
+  status: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '计划名称不能为空', trigger: 'blur' }],
-  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -411,8 +421,8 @@ const configDialog = reactive({
     timePeriodLead: 0,
     naturalDatePeriodLead: 0,
     // 多个累计时长 累计里程 匹配
-    accumulatedTimeOption: null,    // 累计运行时长选项
-    accumulatedMileageOption: null, // 累计运行公里数选项
+    accumulatedTimeOption: null, // 累计运行时长选项
+    accumulatedMileageOption: null // 累计运行公里数选项
   }
 })
 
@@ -444,15 +454,15 @@ const openConfigDialog = (row: IotMaintenanceBomVO) => {
     naturalDatePeriodLead: row.naturalDatePeriodLead || 0
   }
 
-  configDialog.form.accumulatedTimeOption = row.code || null;
-  configDialog.form.accumulatedMileageOption = row.type || null;
+  configDialog.form.accumulatedTimeOption = row.code || null
+  configDialog.form.accumulatedMileageOption = row.type || null
 
   configDialog.visible = true
 }
 
 // 保存配置
 const saveConfig = () => {
-  (configFormRef.value as any).validate((valid: boolean) => {
+  ;(configFormRef.value as any).validate((valid: boolean) => {
     if (!valid) return
     if (!configDialog.current) return
 
@@ -468,8 +478,8 @@ const saveConfig = () => {
       requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
     }
 
-    const missingFields = requiredFields.filter(field =>
-      !configDialog.form[field as keyof typeof configDialog.form]
+    const missingFields = requiredFields.filter(
+      (field) => !configDialog.form[field as keyof typeof configDialog.form]
     )
 
     if (missingFields.length > 0) {
@@ -510,8 +520,8 @@ const queryParams = reactive({
   planId: id
 })
 
-const deviceChoose = async(selectedDevices) => {
-  const newIds = selectedDevices.map(device => device.id)
+const deviceChoose = async (selectedDevices) => {
+  const newIds = selectedDevices.map((device) => device.id)
   deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
   const params = {
     deviceIds: deviceIds.value.join(',') // 明确传递数组参数
@@ -520,7 +530,7 @@ const deviceChoose = async(selectedDevices) => {
   // 根据选择的设备筛选出设备关系的分类BOM中与保养相关的节点项
   const res = await IotDeviceApi.deviceAssociateBomList(queryParams)
   const rawData = res || []
-  if(rawData.length === 0){
+  if (rawData.length === 0) {
     message.error('选择的设备不存在待保养BOM项')
   }
   if (!Array.isArray(rawData)) {
@@ -528,7 +538,7 @@ const deviceChoose = async(selectedDevices) => {
     return
   }
   // 转换数据结构(根据你的接口定义调整)
-  const newItems = rawData.map(device => ({
+  const newItems = rawData.map((device) => ({
     assetClass: device.assetClass,
     deviceCode: device.deviceCode,
     deviceName: device.deviceName,
@@ -537,7 +547,7 @@ const deviceChoose = async(selectedDevices) => {
     name: device.name,
     code: device.code,
     assetProperty: device.assetProperty,
-    remark: null,    // 初始化备注
+    remark: null, // 初始化备注
     deviceId: device.id, // 移除操作需要
     bomNodeId: device.bomNodeId,
     totalRunTime: device.totalRunTime,
@@ -552,13 +562,13 @@ const deviceChoose = async(selectedDevices) => {
     naturalDatePeriodLead: 0
   }))
   // 获取选择的设备相关的id数组
-  newItems.forEach(item => {
+  newItems.forEach((item) => {
     deviceIds.value.push(item.deviceId)
   })
   // 合并到现有列表(去重)
-  newItems.forEach(item => {
+  newItems.forEach((item) => {
     const exists = list.value.some(
-      existing => (existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId)
+      (existing) => existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId
     )
     if (!exists) {
       list.value.push(item)
@@ -568,21 +578,19 @@ const deviceChoose = async(selectedDevices) => {
 
 const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
 const openForm = () => {
-  deviceFormRef.value?.open();
+  deviceFormRef.value?.open()
 }
 
 const close = () => {
   delView(unref(currentRoute))
-  push({ name: 'IotMaintenancePlan', params:{}})
+  push({ name: 'IotMaintenancePlan', params: {} })
 }
 
 // 累计运行时长变更
-const handleAccumulatedTimeChange = (option) => {
-}
+const handleAccumulatedTimeChange = (option) => {}
 
 // 累计运行公里数变更
-const handleAccumulatedMileageChange = (option) => {
-}
+const handleAccumulatedMileageChange = (option) => {}
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
@@ -617,46 +625,58 @@ const submitForm = async () => {
 
 // 新增表单校验规则
 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'
-  }]
+  nextRunningKilometers: [
+    {
+      required: true,
+      message: '里程周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  kiloCycleLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ],
+  nextRunningTime: [
+    {
+      required: true,
+      message: '时间周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  timePeriodLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ],
+  nextNaturalDate: [
+    {
+      required: true,
+      message: '自然日周期必须填写',
+      trigger: 'blur'
+    }
+  ],
+  naturalDatePeriodLead: [
+    {
+      required: true,
+      message: '提前量必须填写',
+      trigger: 'blur'
+    }
+  ]
 })
 
 /** 校验表格数据 */
 const validateTableData = (): boolean => {
   let isValid = true
   const errorMessages: string[] = []
-  const noRulesErrorMessages: string[] = []  // 未设置任何保养项规则 的错误提示信息
-  const noRules: string[] = []  // 行记录中设置了保养规则的记录数量
-  const configErrors: string[] = []   // 保养规则配置弹出框
-  let shouldBreak = false;
+  const noRulesErrorMessages: string[] = [] // 未设置任何保养项规则 的错误提示信息
+  const noRules: string[] = [] // 行记录中设置了保养规则的记录数量
+  const configErrors: string[] = [] // 保养规则配置弹出框
+  let shouldBreak = false
 
   if (list.value.length === 0) {
     errorMessages.push('请至少添加一条设备保养明细')
@@ -667,20 +687,24 @@ const validateTableData = (): boolean => {
   }
 
   list.value.forEach((row, index) => {
-    if (shouldBreak) return;
+    if (shouldBreak) return
     const rowNumber = index + 1 // 用户可见的行号从1开始
     const deviceIdentifier = `${row.deviceCode}-${row.name}` // 设备标识
     // 校验逻辑
     const checkConfig = (ruleName: string, ruleValue: number, configField: keyof typeof row) => {
-      if (ruleValue === 0) { // 规则开启
+      if (ruleValue === 0) {
+        // 规则开启
         if (!row[configField] || row[configField] <= 0) {
-          configErrors.push(`第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`)
+          configErrors.push(
+            `第 ${rowNumber} 行(${deviceIdentifier}):请点击【配置】维护${ruleName}上次保养值`
+          )
           isValid = false
         }
       }
     }
     // 里程校验逻辑
-    if (row.mileageRule === 0) { // 假设 0 表示开启状态
+    if (row.mileageRule === 0) {
+      // 假设 0 表示开启状态
       if (!row.nextRunningKilometers || row.nextRunningKilometers <= 0) {
         errorMessages.push(`第 ${rowNumber} 行:开启里程规则必须填写有效的里程周期`)
         isValid = false
@@ -713,10 +737,10 @@ const validateTableData = (): boolean => {
     // 如果选中的一行记录未设置任何保养规则 提示 ‘保养项未设置任何保养规则’
     if (noRules.length === 3) {
       isValid = false
-      shouldBreak = true; // 设置标志变量为true,退出循环
+      shouldBreak = true // 设置标志变量为true,退出循环
       noRulesErrorMessages.push('保养项至少设置1个保养规则')
     }
-    noRules.length = 0;
+    noRules.length = 0
   })
   if (errorMessages.length > 0) {
     message.error('设置保养规则后,请维护对应的周期值')
@@ -761,29 +785,31 @@ onMounted(async () => {
   deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
   formData.value.deptId = deptId
   // if (id){
-    formType.value = 'update'
-    const plan = await IotMaintenancePlanApi.getIotMaintenancePlan(id);
-    deviceLabel.value = plan.deviceName
-    formData.value = plan
+  formType.value = 'update'
+  const plan = await IotMaintenancePlanApi.getIotMaintenancePlan(id)
+  deviceLabel.value = plan.deviceName
+  formData.value = plan
   // 查询保养责任人
-  const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0;
+  const personId = formData.value.responsiblePerson ? Number(formData.value.responsiblePerson) : 0
   UserApi.getUser(personId).then((res) => {
-    formData.value.responsiblePerson = res.nickname;
+    formData.value.responsiblePerson = res.nickname
   })
   // 查询保养计划明细
-  const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams);
+  const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
   list.value = []
   if (Array.isArray(data)) {
-    list.value = data.map(item => ({
+    list.value = data.map((item) => ({
       ...item,
       // 这里可以添加必要的字段转换(如果有日期等需要格式化的字段)
-      lastNaturalDate: item.lastNaturalDate ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD') : null
+      lastNaturalDate: item.lastNaturalDate
+        ? dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
+        : null
     }))
   }
 })
 const handleDelete = async (str: string) => {
   try {
-    const index = list.value.findIndex((item) => (item.id+'-'+item.bomNodeId) === str)
+    const index = list.value.findIndex((item) => item.id + '-' + item.bomNodeId === str)
     if (index !== -1) {
       // 通过 splice 删除元素
       list.value.splice(index, 1)
@@ -799,17 +825,17 @@ const handleDelete = async (str: string) => {
 }
 
 :deep(.el-input-number .el-input__inner) {
-  text-align: left !important;
   padding-left: 10px; /* 保持左侧间距 */
+  text-align: left !important;
 }
 
 /* 分组容器样式 */
 .form-group {
   position: relative;
-  border: 1px solid #dcdfe6;
-  border-radius: 4px;
   padding: 20px 15px 10px;
   margin-bottom: 18px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
   transition: border-color 0.2s;
 }
 
@@ -818,10 +844,10 @@ const handleDelete = async (str: string) => {
   position: absolute;
   top: -10px;
   left: 20px;
-  background: white;
   padding: 0 8px;
-  color: #606266;
   font-size: 12px;
   font-weight: 500;
+  color: #606266;
+  background: white;
 }
 </style>

+ 58 - 115
src/views/pms/maintenance/IotMaintenancePlanEdit.vue

@@ -6,8 +6,7 @@
       :rules="formRules"
       v-loading="formLoading"
       style="margin-top: 1em; margin-right: 4em; margin-left: 0.5em"
-      label-width="130px"
-    >
+      label-width="130px">
       <div class="base-expandable-content">
         <el-row>
           <el-col :span="12">
@@ -25,8 +24,7 @@
               <el-input
                 v-model="formData.remark"
                 type="textarea"
-                :placeholder="t('iotMaintain.remarkHolder')"
-              />
+                :placeholder="t('iotMaintain.remarkHolder')" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -41,8 +39,7 @@
         :model="queryParams"
         ref="queryFormRef"
         :inline="true"
-        label-width="68px"
-      >
+        label-width="68px">
         <el-form-item>
           <el-button @click="openForm" type="warning">
             <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}</el-button
@@ -66,8 +63,7 @@
         :cell-class-name="cellClassName"
         :max-height="420"
         scrollbar-always-on
-        :cell-style="cellStyle"
-      >
+        :cell-style="cellStyle">
         <!-- 添加序号列 -->
         <el-table-column
           type="index"
@@ -75,31 +71,27 @@
           :min-width="48"
           align="center"
           prop="serial"
-          fixed="left"
-        />
+          fixed="left" />
         <el-table-column label="设备id" align="center" prop="deviceId" v-if="false" />
         <el-table-column
           :label="t('iotMaintain.deviceCode')"
           align="center"
           prop="deviceCode"
           :min-width="columnWidths.deviceCode"
-          fixed="left"
-        />
+          fixed="left" />
         <el-table-column
           :label="t('iotMaintain.deviceName')"
           align="center"
           prop="deviceName"
           :min-width="columnWidths.deviceName"
-          fixed="left"
-        />
+          fixed="left" />
         <el-table-column
           :label="t('bomList.bomNode')"
           align="center"
           prop="name"
           :show-overflow-tooltip="false"
           :min-width="columnWidths.name"
-          fixed="left"
-        >
+          fixed="left">
           <template #default="{ row }">
             <div class="full-content-cell">
               {{ row.name }}
@@ -111,15 +103,13 @@
           align="center"
           key="runningTimeRule"
           prop="runningTimeRule"
-          :min-width="columnWidths.runningTimeRule"
-        >
+          :min-width="columnWidths.runningTimeRule">
           <template #default="scope">
             <el-switch
               v-model="scope.row.runningTimeRule"
               :active-value="0"
               :inactive-value="1"
-              @change="handleRuleChange(scope.row, 'runningTime')"
-            />
+              @change="handleRuleChange(scope.row, 'runningTime')" />
           </template>
         </el-table-column>
         <el-table-column
@@ -127,15 +117,13 @@
           align="center"
           key="mileageRule"
           prop="mileageRule"
-          :min-width="columnWidths.mileageRule"
-        >
+          :min-width="columnWidths.mileageRule">
           <template #default="scope">
             <el-switch
               v-model="scope.row.mileageRule"
               :active-value="0"
               :inactive-value="1"
-              @change="handleRuleChange(scope.row, 'mileage')"
-            />
+              @change="handleRuleChange(scope.row, 'mileage')" />
           </template>
         </el-table-column>
 
@@ -144,15 +132,13 @@
           align="center"
           key="naturalDateRule"
           prop="naturalDateRule"
-          :min-width="columnWidths.naturalDateRule"
-        >
+          :min-width="columnWidths.naturalDateRule">
           <template #default="scope">
             <el-switch
               v-model="scope.row.naturalDateRule"
               :active-value="0"
               :inactive-value="1"
-              @change="handleRuleChange(scope.row, 'date')"
-            />
+              @change="handleRuleChange(scope.row, 'date')" />
           </template>
         </el-table-column>
         <el-table-column
@@ -160,8 +146,7 @@
           align="center"
           prop="totalRunTime"
           :formatter="erpPriceTableColumnFormatter"
-          :min-width="columnWidths.totalRunTime"
-        >
+          :min-width="columnWidths.totalRunTime">
           <template #default="{ row }">
             {{ row.totalRunTime ?? row.tempTotalRunTime }}
           </template>
@@ -171,8 +156,7 @@
           align="center"
           prop="totalMileage"
           :formatter="erpPriceTableColumnFormatter"
-          :min-width="columnWidths.totalMileage"
-        >
+          :min-width="columnWidths.totalMileage">
           <template #default="{ row }">
             {{ row.totalMileage ?? row.tempTotalMileage }}
           </template>
@@ -182,21 +166,18 @@
           align="center"
           prop="tempTotalRunTime"
           :formatter="erpPriceTableColumnFormatter"
-          v-if="false"
-        />
+          v-if="false" />
         <el-table-column
           label="tempTotalMileage"
           align="center"
           prop="tempTotalMileage"
           :formatter="erpPriceTableColumnFormatter"
-          v-if="false"
-        />
+          v-if="false" />
         <el-table-column
           :label="t('mainPlan.lastMaintenanceDate')"
           prop="lastMaintenanceDate"
           align="center"
-          :min-width="columnWidths.lastMaintenanceDate"
-        >
+          :min-width="columnWidths.lastMaintenanceDate">
           <template #default="{ row }">
             <div class="full-content-cell">
               {{ row.lastMaintenanceDate }}
@@ -210,8 +191,7 @@
             align="center"
             prop="lastRunningTime"
             :formatter="erpPriceTableColumnFormatter"
-            :min-width="columnWidths.lastRunningTime"
-          >
+            :min-width="columnWidths.lastRunningTime">
             <template #default="{ row }">
               {{ row.lastRunningTime }}
             </template>
@@ -220,8 +200,7 @@
             :label="t('mainPlan.nextMaintenanceH')"
             align="center"
             prop="nextMaintenanceH"
-            :min-width="columnWidths.nextMaintenanceH"
-          >
+            :min-width="columnWidths.nextMaintenanceH">
             <template #default="{ row }">
               {{ row.nextMaintenanceH ?? '-' }}
             </template>
@@ -230,8 +209,7 @@
             :label="t('mainPlan.remainH')"
             align="center"
             prop="remainH"
-            :min-width="columnWidths.remainH"
-          >
+            :min-width="columnWidths.remainH">
             <template #default="{ row }">
               {{ row.remainH ?? '-' }}
             </template>
@@ -244,8 +222,7 @@
             align="center"
             prop="lastRunningKilometers"
             :formatter="erpPriceTableColumnFormatter"
-            :min-width="columnWidths.lastRunningKilometers"
-          >
+            :min-width="columnWidths.lastRunningKilometers">
             <template #default="{ row }">
               {{ row.lastRunningKilometers }}
             </template>
@@ -254,8 +231,7 @@
             :label="t('mainPlan.nextMaintenanceKm')"
             align="center"
             prop="nextMaintenanceKm"
-            :min-width="columnWidths.nextMaintenanceKm"
-          >
+            :min-width="columnWidths.nextMaintenanceKm">
             <template #default="{ row }">
               {{ row.nextMaintenanceKm ?? '-' }}
             </template>
@@ -264,8 +240,7 @@
             :label="t('mainPlan.remainKm')"
             align="center"
             prop="remainKm"
-            :min-width="columnWidths.remainKm"
-          >
+            :min-width="columnWidths.remainKm">
             <template #default="{ row }">
               {{ row.remainKm ?? '-' }}
             </template>
@@ -279,8 +254,7 @@
             align="center"
             prop="tempLastNaturalDate"
             :formatter="erpPriceTableColumnFormatter"
-            :min-width="columnWidths.tempLastNaturalDate"
-          >
+            :min-width="columnWidths.tempLastNaturalDate">
             <template #default="{ row }">
               {{ row.tempLastNaturalDate }}
             </template>
@@ -289,8 +263,7 @@
             :label="t('mainPlan.nextMaintDate')"
             align="center"
             prop="nextMaintenanceDate"
-            :min-width="columnWidths.nextMaintenanceDate"
-          >
+            :min-width="columnWidths.nextMaintenanceDate">
             <template #default="{ row }">
               {{ row.nextMaintenanceDate ?? '-' }}
             </template>
@@ -299,8 +272,7 @@
             :label="t('mainPlan.remainDay')"
             align="center"
             prop="remainDay"
-            :min-width="columnWidths.remainDay"
-          >
+            :min-width="columnWidths.remainDay">
             <template #default="{ row }">
               {{ row.remainDay ?? '-' }}
             </template>
@@ -312,8 +284,7 @@
           align="center"
           :min-width="columnWidths.operation"
           prop="operation"
-          fixed="right"
-        >
+          fixed="right">
           <template #default="scope">
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
               <div>
@@ -322,8 +293,7 @@
                   style="vertical-align: middle"
                   link
                   type="danger"
-                  @click="handleDelete(scope.row.deviceId + '-' + scope.row.bomNodeId)"
-                >
+                  @click="handleDelete(scope.row.deviceId + '-' + scope.row.bomNodeId)">
                   {{ t('modelTemplate.delete') }}
                 </el-button>
               </div>
@@ -367,8 +337,7 @@
     v-model="configDialog.visible"
     :title="`设备 ${configDialog.current?.deviceCode + '-' + configDialog.current?.name} 保养配置`"
     width="600px"
-    :close-on-click-modal="false"
-  >
+    :close-on-click-modal="false">
     <!-- 使用header插槽自定义标题 -->
     <template #header>
       <span
@@ -381,54 +350,47 @@
       :model="configDialog.form"
       label-width="200px"
       :rules="configFormRules"
-      ref="configFormRef"
-    >
+      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"
-        >
+          prop="lastRunningKilometers">
           <el-input-number
             v-model="configDialog.form.lastRunningKilometers"
             :precision="2"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
         <!-- 运行时间配置 -->
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.lastMaintenanceOperationTime')"
-          prop="lastRunningTime"
-        >
+          prop="lastRunningTime">
           <el-input-number
             v-model="configDialog.form.lastRunningTime"
             :precision="1"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
         <!-- 自然日期配置 -->
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
           :label="t('mainPlan.lastMaintenanceNaturalDate')"
-          prop="lastNaturalDate"
-        >
+          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%"
-          />
+            style="width: 60%" />
         </el-form-item>
       </div>
 
@@ -438,30 +400,26 @@
         <el-form-item
           v-if="configDialog.current?.mileageRule === 0"
           :label="t('mainPlan.operatingMileageCycle')"
-          prop="nextRunningKilometers"
-        >
+          prop="nextRunningKilometers">
           <el-input-number
             v-model="configDialog.form.nextRunningKilometers"
             :precision="2"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.mileageRule === 0"
           :label="t('mainPlan.OperatingMileageCycle_lead')"
-          prop="kiloCycleLead"
-        >
+          prop="kiloCycleLead">
           <el-input-number
             v-model="configDialog.form.kiloCycleLead"
             :precision="2"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
       </div>
 
@@ -470,30 +428,26 @@
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.RunTimeCycle')"
-          prop="nextRunningTime"
-        >
+          prop="nextRunningTime">
           <el-input-number
             v-model="configDialog.form.nextRunningTime"
             :precision="1"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.runningTimeRule === 0"
           :label="t('mainPlan.RunTimeCycle_Lead')"
-          prop="timePeriodLead"
-        >
+          prop="timePeriodLead">
           <el-input-number
             v-model="configDialog.form.timePeriodLead"
             :precision="1"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
       </div>
 
@@ -502,28 +456,24 @@
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
           :label="t('mainPlan.NaturalDailyCycle')"
-          prop="nextNaturalDate"
-        >
+          prop="nextNaturalDate">
           <el-input-number
             v-model="configDialog.form.nextNaturalDate"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
         <el-form-item
           v-if="configDialog.current?.naturalDateRule === 0"
           :label="t('mainPlan.NaturalDailyCycle_Lead')"
-          prop="naturalDatePeriodLead"
-        >
+          prop="naturalDatePeriodLead">
           <el-input-number
             v-model="configDialog.form.naturalDatePeriodLead"
             :min="0"
             controls-position="right"
             :controls="false"
-            style="width: 60%"
-          />
+            style="width: 60%" />
         </el-form-item>
       </div>
 
@@ -537,8 +487,7 @@
             configDialog.current?.mileageAccumulatedAttrs?.length) &&
           (configDialog.current.totalRunTime == null || isNaN(configDialog.current.totalRunTime)) &&
           (configDialog.current.totalMileage == null || isNaN(configDialog.current.totalMileage))
-        "
-      >
+        ">
         <div class="group-title">{{ t('mainPlan.accumulatedParams') }}</div>
         <!-- 累计运行时长 -->
         <el-form-item
@@ -559,21 +508,18 @@
               message: '请选择累计运行时长',
               trigger: 'change'
             }
-          ]"
-        >
+          ]">
           <el-select
             v-model="configDialog.form.accumulatedTimeOption"
             placeholder="请选择累计运行时长"
             style="width: 80%"
             clearable
-            @change="handleAccumulatedTimeChange"
-          >
+            @change="handleAccumulatedTimeChange">
             <el-option
               v-for="(item, index) in configDialog.current.timeAccumulatedAttrs"
               :key="`time-${item.pointName}-${index}`"
               :label="item.pointName"
-              :value="item.pointName"
-            />
+              :value="item.pointName" />
           </el-select>
         </el-form-item>
         <!-- 累计运行公里数 -->
@@ -595,21 +541,18 @@
               message: '请选择累计运行公里数',
               trigger: 'change'
             }
-          ]"
-        >
+          ]">
           <el-select
             v-model="configDialog.form.accumulatedMileageOption"
             placeholder="请选择累计运行公里数"
             style="width: 80%"
             clearable
-            @change="handleAccumulatedMileageChange"
-          >
+            @change="handleAccumulatedMileageChange">
             <el-option
               v-for="(item, index) in configDialog.current.mileageAccumulatedAttrs"
               :key="`mileage-${item.pointName}-${index}`"
               :label="item.pointName"
-              :value="item.pointName"
-            />
+              :value="item.pointName" />
           </el-select>
         </el-form-item>
       </div>
@@ -629,7 +572,7 @@ import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintena
 import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import MainPlanDeviceList from '@/views/pms/maintenance/MainPlanDeviceList.vue'
+import MainPlanDeviceList from '@/views/pms/maintenance/maintenance-device-list.vue'
 import * as DeptApi from '@/api/system/dept'
 import { erpPriceTableColumnFormatter } from '@/utils'
 import dayjs from 'dayjs'

+ 191 - 174
src/views/pms/maintenance/IotMaintenancePlanManage.vue

@@ -39,172 +39,185 @@
         </el-button>
       </div>
 
-      <ZmTable
-        ref="tableRef"
-        class="maintenance-plan-table"
-        :data="list"
-        :loading="loading"
-        :show-border="true"
-        :cell-class-name="cellClassName"
-        :cell-style="cellStyle"
-        :max-height="tableMaxHeight"
-        :column-max-width="420"
-        :highlight-current-row="false"
-        @header-dragend="handleHeaderDragEnd">
-        <ZmTableColumn
-          type="index"
-          :label="t('iotDevice.serial')"
-          :width="62"
-          fixed="left"
-          hide-in-column-settings />
-        <ZmTableColumn
-          prop="deviceCode"
-          :label="t('iotMaintain.deviceCode')"
-          :min-width="100"
-          fixed="left" />
-        <ZmTableColumn
-          prop="deviceName"
-          :label="t('iotMaintain.deviceName')"
-          :min-width="110"
-          fixed="left" />
-        <ZmTableColumn
-          prop="name"
-          :label="t('bomList.bomNode')"
-          :min-width="190"
-          fixed="left"
-          :show-overflow-tooltip="false">
-          <template #default="{ row }">
-            <div class="full-content-cell">{{ row.name }}</div>
+      <div class="table-resizer">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              ref="tableRef"
+              class="maintenance-plan-table"
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              :show-border="true"
+              :cell-class-name="cellClassName"
+              :cell-style="cellStyle"
+              :column-max-width="420"
+              :highlightCurrentRow="false"
+              :hover-highlight="false"
+              @header-dragend="handleHeaderDragEnd">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="62"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn
+                prop="deviceCode"
+                :label="t('iotMaintain.deviceCode')"
+                :min-width="100"
+                fixed="left" />
+              <ZmTableColumn
+                prop="deviceName"
+                :label="t('iotMaintain.deviceName')"
+                :min-width="110"
+                fixed="left" />
+              <ZmTableColumn
+                prop="name"
+                :label="t('bomList.bomNode')"
+                :min-width="190"
+                fixed="left"
+                :show-overflow-tooltip="false">
+                <template #default="{ row }">
+                  <div class="full-content-cell">{{ row.name }}</div>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="runningTimeRule" :label="t('main.runTime')" :width="92">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.runningTimeRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="handleRuleChange(row, 'runningTime')" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="mileageRule" :label="t('main.mileage')" :width="92">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.mileageRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="handleRuleChange(row, 'mileage')" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="naturalDateRule" :label="t('main.date')" :width="92">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.naturalDateRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="handleRuleChange(row, 'date')" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="totalRunTime"
+                :label="t('operationFillForm.sumTime')"
+                :min-width="130"
+                :real-value="(row) => row.totalRunTime ?? row.tempTotalRunTime">
+                <template #default="{ row }">
+                  {{ row.totalRunTime ?? row.tempTotalRunTime ?? '-' }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="totalMileage"
+                :label="t('operationFillForm.sumKil')"
+                :min-width="150"
+                :real-value="(row) => row.totalMileage ?? row.tempTotalMileage">
+                <template #default="{ row }">
+                  {{ row.totalMileage ?? row.tempTotalMileage ?? '-' }}
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="lastMaintenanceDate"
+                :label="t('mainPlan.lastMaintenanceDate')"
+                :min-width="126"
+                :real-value="(row) => formatDate(row.lastMaintenanceDate)">
+                <template #default="{ row }">
+                  {{ formatDate(row.lastMaintenanceDate) }}
+                </template>
+              </ZmTableColumn>
+
+              <ZmTableColumn v-if="hasTimeRule" column-key="time-group" label="保养时长" is-parent>
+                <ZmTableColumn
+                  prop="lastRunningTime"
+                  :label="t('mainPlan.lastMaintenanceOperationTime')"
+                  :min-width="150" />
+                <ZmTableColumn
+                  prop="nextMaintenanceH"
+                  :label="t('mainPlan.nextMaintenanceH')"
+                  :min-width="150">
+                  <template #default="{ row }">{{ row.nextMaintenanceH ?? '-' }}</template>
+                </ZmTableColumn>
+                <ZmTableColumn prop="remainH" :label="t('mainPlan.remainH')" :min-width="120">
+                  <template #default="{ row }">{{ row.remainH ?? '-' }}</template>
+                </ZmTableColumn>
+              </ZmTableColumn>
+
+              <ZmTableColumn
+                v-if="hasMileageRule"
+                column-key="mileage-group"
+                label="保养里程"
+                is-parent>
+                <ZmTableColumn
+                  prop="lastRunningKilometers"
+                  :label="t('mainPlan.lastMaintenanceMileage')"
+                  :min-width="150" />
+                <ZmTableColumn
+                  prop="nextMaintenanceKm"
+                  :label="t('mainPlan.nextMaintenanceKm')"
+                  :min-width="150">
+                  <template #default="{ row }">{{ row.nextMaintenanceKm ?? '-' }}</template>
+                </ZmTableColumn>
+                <ZmTableColumn prop="remainKm" :label="t('mainPlan.remainKm')" :min-width="120">
+                  <template #default="{ row }">{{ row.remainKm ?? '-' }}</template>
+                </ZmTableColumn>
+              </ZmTableColumn>
+
+              <ZmTableColumn v-if="hasDateRule" column-key="date-group" label="保养日期" is-parent>
+                <ZmTableColumn
+                  prop="lastNaturalDate"
+                  :label="t('mainPlan.lastMaintenanceNaturalDate')"
+                  :min-width="150"
+                  :real-value="(row) => formatDate(row.lastNaturalDate)">
+                  <template #default="{ row }">{{ formatDate(row.lastNaturalDate) }}</template>
+                </ZmTableColumn>
+                <ZmTableColumn
+                  prop="nextMaintenanceDate"
+                  :label="t('mainPlan.nextMaintDate')"
+                  :min-width="140">
+                  <template #default="{ row }">{{ row.nextMaintenanceDate ?? '-' }}</template>
+                </ZmTableColumn>
+                <ZmTableColumn prop="remainDay" :label="t('mainPlan.remainDay')" :min-width="120">
+                  <template #default="{ row }">{{ row.remainDay ?? '-' }}</template>
+                </ZmTableColumn>
+              </ZmTableColumn>
+
+              <ZmTableColumn
+                column-key="operation"
+                :label="t('operationFill.operation')"
+                :width="150"
+                fixed="right"
+                action>
+                <template #default="{ row }">
+                  <div class="table-actions">
+                    <el-button v-if="!isReadonly" link type="danger" @click="handleDelete(row)">
+                      <Icon icon="ep:zoom-out" class="mr-3px" />
+                      {{ t('modelTemplate.delete') }}
+                    </el-button>
+                    <el-button link type="primary" @click="openConfigDialog(row)">
+                      {{ isReadonly ? t('form.set') : t('modelTemplate.update') }}
+                    </el-button>
+                  </div>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
           </template>
-        </ZmTableColumn>
-        <ZmTableColumn prop="runningTimeRule" :label="t('main.runTime')" :width="92">
-          <template #default="{ row }">
-            <el-switch
-              v-model="row.runningTimeRule"
-              :active-value="0"
-              :inactive-value="1"
-              :disabled="isReadonly"
-              @change="handleRuleChange(row, 'runningTime')" />
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn prop="mileageRule" :label="t('main.mileage')" :width="92">
-          <template #default="{ row }">
-            <el-switch
-              v-model="row.mileageRule"
-              :active-value="0"
-              :inactive-value="1"
-              :disabled="isReadonly"
-              @change="handleRuleChange(row, 'mileage')" />
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn prop="naturalDateRule" :label="t('main.date')" :width="92">
-          <template #default="{ row }">
-            <el-switch
-              v-model="row.naturalDateRule"
-              :active-value="0"
-              :inactive-value="1"
-              :disabled="isReadonly"
-              @change="handleRuleChange(row, 'date')" />
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn
-          prop="totalRunTime"
-          :label="t('operationFillForm.sumTime')"
-          :min-width="130"
-          :real-value="(row) => row.totalRunTime ?? row.tempTotalRunTime">
-          <template #default="{ row }">
-            {{ row.totalRunTime ?? row.tempTotalRunTime ?? '-' }}
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn
-          prop="totalMileage"
-          :label="t('operationFillForm.sumKil')"
-          :min-width="150"
-          :real-value="(row) => row.totalMileage ?? row.tempTotalMileage">
-          <template #default="{ row }">
-            {{ row.totalMileage ?? row.tempTotalMileage ?? '-' }}
-          </template>
-        </ZmTableColumn>
-        <ZmTableColumn
-          prop="lastMaintenanceDate"
-          :label="t('mainPlan.lastMaintenanceDate')"
-          :min-width="126"
-          :real-value="(row) => formatDate(row.lastMaintenanceDate)">
-          <template #default="{ row }">
-            {{ formatDate(row.lastMaintenanceDate) }}
-          </template>
-        </ZmTableColumn>
-
-        <ZmTableColumn v-if="hasTimeRule" column-key="time-group" label="保养时长" is-parent>
-          <ZmTableColumn
-            prop="lastRunningTime"
-            :label="t('mainPlan.lastMaintenanceOperationTime')"
-            :min-width="150" />
-          <ZmTableColumn
-            prop="nextMaintenanceH"
-            :label="t('mainPlan.nextMaintenanceH')"
-            :min-width="150">
-            <template #default="{ row }">{{ row.nextMaintenanceH ?? '-' }}</template>
-          </ZmTableColumn>
-          <ZmTableColumn prop="remainH" :label="t('mainPlan.remainH')" :min-width="120">
-            <template #default="{ row }">{{ row.remainH ?? '-' }}</template>
-          </ZmTableColumn>
-        </ZmTableColumn>
-
-        <ZmTableColumn v-if="hasMileageRule" column-key="mileage-group" label="保养里程" is-parent>
-          <ZmTableColumn
-            prop="lastRunningKilometers"
-            :label="t('mainPlan.lastMaintenanceMileage')"
-            :min-width="150" />
-          <ZmTableColumn
-            prop="nextMaintenanceKm"
-            :label="t('mainPlan.nextMaintenanceKm')"
-            :min-width="150">
-            <template #default="{ row }">{{ row.nextMaintenanceKm ?? '-' }}</template>
-          </ZmTableColumn>
-          <ZmTableColumn prop="remainKm" :label="t('mainPlan.remainKm')" :min-width="120">
-            <template #default="{ row }">{{ row.remainKm ?? '-' }}</template>
-          </ZmTableColumn>
-        </ZmTableColumn>
-
-        <ZmTableColumn v-if="hasDateRule" column-key="date-group" label="保养日期" is-parent>
-          <ZmTableColumn
-            prop="lastNaturalDate"
-            :label="t('mainPlan.lastMaintenanceNaturalDate')"
-            :min-width="150"
-            :real-value="(row) => formatDate(row.lastNaturalDate)">
-            <template #default="{ row }">{{ formatDate(row.lastNaturalDate) }}</template>
-          </ZmTableColumn>
-          <ZmTableColumn
-            prop="nextMaintenanceDate"
-            :label="t('mainPlan.nextMaintDate')"
-            :min-width="140">
-            <template #default="{ row }">{{ row.nextMaintenanceDate ?? '-' }}</template>
-          </ZmTableColumn>
-          <ZmTableColumn prop="remainDay" :label="t('mainPlan.remainDay')" :min-width="120">
-            <template #default="{ row }">{{ row.remainDay ?? '-' }}</template>
-          </ZmTableColumn>
-        </ZmTableColumn>
-
-        <ZmTableColumn
-          column-key="operation"
-          :label="t('operationFill.operation')"
-          :width="150"
-          fixed="right"
-          action>
-          <template #default="{ row }">
-            <div class="table-actions">
-              <el-button v-if="!isReadonly" link type="danger" @click="handleDelete(row)">
-                <Icon icon="ep:zoom-out" class="mr-3px" />
-                {{ t('modelTemplate.delete') }}
-              </el-button>
-              <el-button link type="primary" @click="openConfigDialog(row)">
-                {{ isReadonly ? t('form.set') : t('modelTemplate.update') }}
-              </el-button>
-            </div>
-          </template>
-        </ZmTableColumn>
-      </ZmTable>
+        </el-auto-resizer>
+      </div>
     </section>
 
     <section class="plan-footer">
@@ -471,7 +484,7 @@ import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintena
 import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import MainPlanDeviceList from '@/views/pms/maintenance/MainPlanDeviceList.vue'
+import MainPlanDeviceList from '@/views/pms/maintenance/maintenance-device-list.vue'
 import * as DeptApi from '@/api/system/dept'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import dayjs from 'dayjs'
@@ -487,9 +500,6 @@ const mode = computed(() => {
   return 'create'
 })
 const isReadonly = computed(() => mode.value === 'detail')
-const tableMaxHeight = computed(() =>
-  isReadonly.value ? 'calc(100vh - 360px)' : 'calc(100vh - 390px)'
-)
 const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintenanceBomVO>()
 const dept = ref() // 当前登录人所属部门对象
 const configFormRef = ref() // 配置弹出框对象
@@ -1483,11 +1493,13 @@ const handleDelete = async (row: IotMaintenanceBomVO) => {
 <style scoped>
 .maintenance-plan-page {
   display: flex;
-  min-height: calc(100vh - 160px);
-  padding: 10px;
+  height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  min-height: 0;
   background: var(--el-bg-color-page);
   flex-direction: column;
-  gap: 10px;
+  gap: 12px;
 }
 
 .plan-section {
@@ -1518,17 +1530,22 @@ const handleDelete = async (row: IotMaintenanceBomVO) => {
 
 .table-toolbar {
   display: flex;
+  flex: 0 0 auto;
   justify-content: flex-start;
 }
 
+.table-resizer {
+  position: relative;
+  min-height: 0;
+  flex: 1 1 auto;
+}
+
 .maintenance-plan-table {
   --zm-table-radius: 2px;
   --zm-table-cell-height: 32px;
   --zm-table-header-cell-height: 38px;
   --zm-table-header-group-cell-height: 42px;
   --zm-table-font-size: 12px;
-
-  flex: 1 1 auto;
 }
 
 .full-content-cell {

+ 0 - 756
src/views/pms/maintenance/MainPlanDeviceList.vue

@@ -1,756 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="t('deviceList.selectDevice')"
-          :style="{ width: dialogWidth }"
-          :body-style="{ height: '100%' }"
-          class="device-select-dialog"  @close="handleClose" :close-on-click-modal="false">
-    <ContentWrap class="dialog-top">
-      <el-form
-        class="-mb-15px"
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-        @submit.prevent
-      >
-        <el-form-item  :label="t('deviceList.deviceName')" prop="deviceName">
-          <el-input
-            @keyup.enter="handleQuery"
-            v-model="queryParams.deviceName"
-            :placeholder="t('deviceList.nameHolder')"
-            clearable
-            class="!w-200px"
-          />
-        </el-form-item>
-        <el-form-item  :label="t('deviceList.deviceCode')" prop="deviceCode">
-          <el-input
-            @keyup.enter="handleQuery"
-            v-model="queryParams.deviceCode"
-            :placeholder="t('deviceList.codeHolder')"
-            clearable
-            class="!w-200px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />
-            {{ t('deviceList.search') }}</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('deviceList.reset') }}</el-button>
-          <el-button @click="handleConfirm" class="custom-green-button"><Icon icon="ep:check" class="mr-5px" /> {{ t('workOrderMaterial.confirm') }}</el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-    <ContentWrap class="table-container">
-      <div class="table-wrapper">
-        <el-table
-          v-loading="loading"
-          :data="list"
-          :stripe="true"
-          ref="tableRef"
-          :show-overflow-tooltip="false"
-          @row-click="handleRowClick"
-          :max-height="effectiveTableHeight"
-          style="width: 100%; min-width: 100%"
-          class="fixed-layout-table"
-        >
-          <el-table-column width="60" :label="t('workOrderMaterial.select')">
-            <template #default="{ row }">
-              <el-checkbox
-                :model-value="selectedRows.some(item => item.id === row.id)"
-                @click.stop="toggleRow(row)"
-                class="no-label-radio"
-              />
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('chooseMaintain.deviceCode')" align="center" prop="deviceCode" :min-width="flexColumnMinWidths.deviceCode"/>
-          <el-table-column :label="t('deviceList.deviceName')" align="center" prop="deviceName" :min-width="flexColumnMinWidths.deviceName">
-            <template #default="{ row }">
-              <div class="flex-cell">
-                {{ row.deviceName }}
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('faultForm.deptId')" align="center" prop="deptId" v-if="false"/>
-          <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" :min-width="flexColumnMinWidths.deptName"/>
-          <el-table-column :label="t('iotDevice.status')" align="center" prop="deviceStatus" :min-width="flexColumnMinWidths.deviceStatus">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('deviceInfo.deviceBOM')" align="center" prop="hasSetMaintenanceBom" :min-width="flexColumnMinWidths.createTime">
-            <template #header>
-              <div class="column-header">
-                {{ t('deviceInfo.deviceBOM') }}
-                <!-- 添加感叹号图标,并使用 el-tooltip 显示提示 -->
-                <el-tooltip
-                  effect="dark"
-                  content="请选择有保养项的设备"
-                  placement="top"
-                >
-                  <el-icon :size="12" color="#e6a23c" style="cursor: pointer; margin-left: 3px">
-                    <WarningFilled />
-                  </el-icon>
-                </el-tooltip>
-              </div>
-            </template>
-            <template #default="{ row }">
-              <div :class="{'no-maintenance': !row.hasSetMaintenanceBom, 'has-maintenance': row.hasSetMaintenanceBom}">
-                {{ row.hasSetMaintenanceBom ? t('mainPlan.haveMaintItems') : t('mainPlan.noMaintItems') }}
-              </div>
-            </template>
-          </el-table-column>
-          <!-- <el-table-column
-            :label="t('deviceList.createTime')"
-            align="center"
-            prop="createTime"
-            :min-width="flexColumnMinWidths.createTime"
-          >
-            <template #default="{ row }">
-              <div class="date-cell">
-                {{ formatDateTime(row.createTime) }}
-              </div>
-            </template>
-          </el-table-column> -->
-        </el-table>
-      </div>
-
-      <!-- 分页 -->
-      <Pagination
-        v-if="showPagination"
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
-  </Dialog>
-</template>
-
-<script setup lang="ts">
-import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import { WarningFilled } from '@element-plus/icons-vue'
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const loading = ref(true) // 列表的加载中
-const queryFormRef = ref() // 搜索的表单
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const tableRef = ref();
-const selectedRows = ref<IotDeviceVO[]>([]); // 多选数据(存储所有选中行的数组)
-const { t } = useI18n() // 国际化
-// 调整 emit 类型
-const emit = defineEmits<{
-  (e: 'choose', value: IotDeviceVO[]): void
-  (e: 'close'): void
-}>()
-
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  label: '',
-  status: undefined,
-  deptId: undefined,
-  assetClass: undefined,
-  deviceName: undefined,
-  deviceCode: undefined,
-  name: undefined,
-  code: undefined
-})
-
-// 响应式变量
-const dialogWidth = ref('1100px');
-const tableMinHeight = ref(400); // 初始最小高度
-const effectiveTableHeight = ref<number | null>(null); // 单一高度变量
-const dialogTopRef = ref<HTMLElement | null>(null);
-const showPagination = ref(false); // 控制分页显示
-const tableContainerWidth = ref(0) // 表格容器宽度
-
-// 弹性列最小宽度记录
-const flexColumnMinWidths = reactive({
-  deviceCode: 0,
-  deviceName: 0,
-  deptName: 0,
-  deviceStatus: 0,
-  createTime: 0
-})
-
-// 点击整行选中
-const handleRowClick = (row) => {
-  toggleRow(row)
-}
-const open = async () => {
-  dialogVisible.value = true
-  // 重置为初始参数(保留分页设置)
-  Object.assign(queryParams, initialQueryParams)
-  queryFormRef.value?.resetFields()
-
-  // 初始隐藏分页组件
-  showPagination.value = false;
-
-  await getList()
-
-  // 动态计算宽度
-  await nextTick();
-  const viewportWidth = window.innerWidth;
-  if (viewportWidth < 1200) {
-    // 小屏幕自适应
-    dialogWidth.value = Math.min(1100, viewportWidth - 40) + 'px';
-  } else {
-    // 大屏幕保持固定
-    dialogWidth.value = '1100px';
-  }
-  // 确保计算高度的DOM已渲染
-  await nextTick();
-  calculateTableHeight();
-}
-
-defineExpose({ open })
-const { wsCache } = useCache()
-
-// 文本测量工具函数
-const measureText = (text: string, fontSize: string = '14px',
-                     fontWeight: string = 'normal', extraStyles: Record<string, string> = {}): number => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.fontSize = fontSize
-  span.style.fontWeight = fontWeight
-  span.style.fontFamily = "'Microsoft YaHei', sans-serif"
-  span.style.whiteSpace = 'nowrap'
-  span.textContent = text
-
-  // 应用额外样式
-  Object.keys(extraStyles).forEach(key => {
-    span.style[key as any] = extraStyles[key];
-  });
-
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
-
-  return width
-};
-
-// 自定义日期时间格式化函数
-const formatDateTime = (dateString: string | Date) => {
-  if (!dateString) return '';
-
-  const date = new Date(dateString);
-
-  // 确保日期有效
-  if (isNaN(date.getTime())) {
-    return String(dateString).replace(/[\r\n]/g, '');;
-  }
-
-  const year = date.getFullYear();
-  const month = String(date.getMonth() + 1).padStart(2, '0');
-  const day = String(date.getDate()).padStart(2, '0');
-  const hours = String(date.getHours()).padStart(2, '0');
-  const minutes = String(date.getMinutes()).padStart(2, '0');
-  const seconds = String(date.getSeconds()).padStart(2, '0');
-
-  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`.replace(/[\r\n]/g, '');
-};
-
-// 列宽计算函数
-const calculateColumnWidths = async () => {
-  await nextTick()
-
-  if (!tableRef.value || !list.value.length) return
-
-  const table = tableRef.value
-  const columns = table.columns
-  const rows = list.value
-
-  // 获取表格容器宽度
-  const tableContainer = tableRef.value?.$el?.closest('.table-wrapper')
-  if (tableContainer) {
-    tableContainerWidth.value = tableContainer.clientWidth
-  }
-
-  // 计算各列所需最小宽度
-  const columnMinWidths = columns.map((col, colIndex) => {
-    if (colIndex === 0) return 60 // 选择列固定宽度
-
-    // 测量表头宽度
-    // 测量表头宽度 - 使用表头实际样式
-    const headerWidth = col.label
-      ? measureText(col.label, '14px', 'bold', {
-      padding: '0 12px', // 表头实际内边距
-      boxSizing: 'border-box'
-    }) + 0 // 安全边距
-      : 0;
-
-    // 测量单元格内容宽度
-    let maxCellWidth = 0
-    rows.forEach(row => {
-      let cellValue = ''
-      let cellWidth = 0
-      if (col.property === 'createTime') {
-        // 特殊处理:使用格式化后的日期文本
-        cellValue = formatDateTime(row[col.property])
-        // 使用完整时间样本确保宽度足够
-        const sampleText = '2024-12-31 23:59:59';
-        const sampleWidth = measureText(sampleText, '14px', 'normal', {
-          padding: '0 12px',
-          boxSizing: 'border-box'
-        });
-        // 时间列额外增加安全边距
-        cellWidth = Math.max(
-          measureText(cellValue, '14px', 'normal', {
-            padding: '0 12px',
-            boxSizing: 'border-box'
-          }),
-          sampleWidth
-        ) + 0; // 安全边距
-        // if (cellWidth > maxCellWidth) maxCellWidth = cellWidth
-      } else if (col.property === 'deviceStatus') {
-        // 特殊处理:字典标签文本
-        const statusText = row.deviceStatusLabel || row[col.property] || '';
-        // 模拟el-tag样式进行测量
-        cellWidth = measureText(statusText, '12px', 'normal', {
-          padding: '2px 7px',
-          border: '1px solid #dcdfe6',
-          borderRadius: '4px',
-          display: 'inline-block',
-          lineHeight: '1.5',
-          boxSizing: 'border-box'
-        }) + 0; // 20px安全边距
-      } else {
-        cellValue = row[col.property] || ''
-        cellWidth = measureText(cellValue, '14px', 'normal', {
-          padding: '0 12px',
-          boxSizing: 'border-box'
-        }) + 0; // 安全边距
-      }
-      if (cellWidth > maxCellWidth) maxCellWidth = cellWidth
-    })
-
-    // 取标题和内容的最大值,并设置最小宽度
-    let minWidth = 100; // 默认最小宽度
-    if (col.property === 'createTime') {
-      minWidth = 100; // 时间列最小宽度设为180px
-    }
-    // 取标题和内容的最大值
-    return Math.max(headerWidth, maxCellWidth, minWidth)
-  })
-
-  // 记录弹性列的最小宽度
-  flexColumnMinWidths.deviceCode = columnMinWidths[1]
-  flexColumnMinWidths.deviceName = columnMinWidths[2]
-  flexColumnMinWidths.deptName = columnMinWidths[4]
-  flexColumnMinWidths.deviceStatus = columnMinWidths[5]
-  flexColumnMinWidths.createTime = columnMinWidths[6]
-
-  // 触发表格重新布局
-  table.doLayout()
-};
-
-const getList = async () => {
-  loading.value = true
-  try {
-    // const user = wsCache.get(CACHE_KEY.USER)
-    // queryParams.deptId = user.user.deptId
-    const data = await IotDeviceApi.getIotDevicePage(queryParams)
-    list.value = data.list
-    total.value = data.total
-
-    // 只有在需要时才显示分页
-    showPagination.value = total.value > queryParams.pageSize;
-    // 数据加载完成后重新计算高度
-    await nextTick();
-    calculateTableHeight();
-    calculateColumnWidths()
-  } finally {
-    loading.value = false
-    // 数据加载完成后计算列宽
-    await nextTick();
-    calculateColumnWidths();
-  }
-}
-
-// 计算表格最大高度
-const calculateTableHeight = () => {
-  nextTick(() => {
-    try {
-      // 获取对话框实际可用空间
-      const dialogBody = document.querySelector('.el-dialog__body');
-      if (!dialogBody || !dialogTopRef.value) return;
-
-      const dialogBodyRect = dialogBody.getBoundingClientRect();
-
-      // 计算可用高度 = 对话框body高度 - 顶部表单高度 - 内边距
-      const dialogPadding = 20; // 上下内边距
-      const topHeight = dialogTopRef.value.offsetHeight;
-      const availableHeight = dialogBodyRect.height - topHeight - dialogPadding;
-
-      // 保证最小高度
-      let calculatedHeight = Math.max(availableHeight, tableMinHeight.value);
-
-      // 根据记录数量计算所需高度(考虑表头和行高)
-      const headerHeight = 40; // 表头高度估算
-      const rowHeight = 48; // 行高估算(增加8px用于行间分割线)
-      const totalRowsHeight = headerHeight + (list.value.length * rowHeight);
-
-      // 如果所需高度小于可用高度,则按实际高度显示
-      if (totalRowsHeight < availableHeight) {
-        calculatedHeight = totalRowsHeight;
-      }
-
-      // 确保高度合理
-      effectiveTableHeight.value = Math.max(calculatedHeight, 300);
-    } catch (e) {
-      console.error("高度计算错误:", e);
-      // 设置回退高度
-      effectiveTableHeight.value = 500;
-    }
-  });
-};
-
-const initialQueryParams = {
-  pageNo: 1,
-  pageSize: 10,
-  label: '',
-  status: undefined,
-  deptId: undefined,
-  assetClass: undefined,
-  deviceName: undefined,
-  deviceCode: undefined,
-  name: undefined,
-  code: undefined
-}
-
-// 多选 切换行选中状态
-const toggleRow = (row) => {
-  const index = selectedRows.value.findIndex(item => item.id === row.id);
-  if (index > -1) {
-    selectedRows.value.splice(index, 1); // 取消选中
-  } else {
-    selectedRows.value.push(row); // 选中
-  }
-};
-
-// 关闭时清空选择
-const handleClose = () => {
-  tableRef.value?.clearSelection();
-  selectedRows.value = []
-  emit('close')
-};
-
-// 确认选择
-const handleConfirm = () => {
-  if (selectedRows.value.length === 0) {
-    ElMessage.warning('请至少选择一个设备')
-    return
-  }
-
-  // 检查是否有设备没有保养项
-  const hasInvalidDevice = selectedRows.value.some(
-    row => row.hasSetMaintenanceBom === false
-  )
-
-  if (hasInvalidDevice) {
-    // 显示警告信息
-    ElMessage.warning({
-      message: '请选择有保养项的设备',
-      duration: 3000,
-      grouping: true
-    })
-    return // 不关闭弹窗
-  }
-
-  emit('choose', selectedRows.value.map(row => ({
-    ...row,
-    // 确保返回必要字段
-    id: row.id,
-    deviceCode: row.deviceCode,
-    deviceName: row.deviceName,
-    deviceStatus: row.deviceStatus,
-    deptName: row.deptName,
-    assetProperty: row.assetProperty,
-    name: row.name,
-    code: row.code
-  })))
-  dialogVisible.value = false;
-  handleClose()
-};
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-const choose = (row: DictDataVO) => {
-  emit('choose', row)
-  dialogVisible.value = false
-}
-/** 重置按钮操作 */
-const resetQuery = () => {
-  // 重置为初始参数
-  Object.assign(queryParams, initialQueryParams)
-  queryFormRef.value?.resetFields()
-  handleQuery()
-}
-
-// 监听列表变化动态调整高度
-watch(list, () => {
-  calculateTableHeight();
-  if (dialogVisible.value) {
-    calculateColumnWidths();
-  }
-});
-
-// 监听窗口大小变化
-onMounted(() => {
-  window.addEventListener('resize', calculateTableHeight);
-  window.addEventListener('resize', calculateColumnWidths);
-});
-
-onUnmounted(() => {
-  window.removeEventListener('resize', calculateTableHeight);
-  window.removeEventListener('resize', calculateColumnWidths);
-});
-
-</script>
-<style lang="scss" scoped>
-
-.device-select-dialog {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-
-  // 增强弹窗高度利用
-  ::v-deep(.el-dialog) {
-    max-height: 85vh;
-    display: flex;
-    flex-direction: column;
-
-    .el-dialog__header {
-      padding: 15px 20px;
-    }
-
-    .el-dialog__body {
-      display: flex;
-      flex-direction: column;
-      flex: 1;
-      padding: 15px;
-      overflow: hidden;
-    }
-  }
-}
-
-/* 列宽度保证 - 防止挤压 */
-.el-table {
-  /* 确保所有列头文字居中 */
-  ::v-deep(.el-table__header) th > .cell {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    text-align: center;
-  }
-
-  /* 所有内容列居中对齐 */
-  ::v-deep(.el-table__body) td {
-    text-align: center !important;
-
-    .cell {
-      display: flex;
-      justify-content: center;
-      align-items: center;
-    }
-  }
-
-  // 行高增加,提升可读性
-  ::v-deep(.el-table__row) {
-    height: 40px;
-  }
-}
-
-/* 列宽自适应样式 */
-::v-deep(.el-table) {
-  .el-table__header th {
-    padding: 0 !important;
-    .cell {
-      padding: 0 0px !important;
-      box-sizing: border-box !important;
-      font-weight: bold !important;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      height: 100%;
-    }
-  }
-
-  .el-table__body td {
-    padding: 0 !important;
-    .cell {
-      padding: 0 0px !important;
-      box-sizing: border-box !important;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      height: 100%;
-    }
-  }
-
-  /* 特定列额外修正 */
-  .el-table__cell:nth-child(4), /* 部门列 */
-  .el-table__cell:nth-child(5), /* 状态列 */
-  .el-table__cell:nth-child(6)  /* 时间列 */ {
-    .date-cell {
-      white-space: nowrap !important;
-      overflow: visible !important;
-      text-overflow: clip !important;
-      justify-content: center;
-    }
-  }
-}
-
-/* 固定表格布局 */
-.fixed-layout-table {
-  ::v-deep(table) {
-    table-layout: fixed !important; /* 固定列宽 */
-    width: auto !important;
-    min-width: 100%;
-  }
-
-  ::v-deep(.el-table__header),
-  ::v-deep(.el-table__body) {
-    width: auto !important; /* 允许宽度扩展 */
-  }
-
-  ::v-deep(.el-table__cell) {
-    overflow: visible !important;   /* 禁用裁剪 */
-    text-overflow: unset !important; /* 移除省略号 */
-    white-space: nowrap !important;  /* 禁止换行 */
-
-    &:nth-child(4) { /* 部门 */
-      min-width: v-bind('flexColumnMinWidths.deptName + "px"');
-      text-align: center;
-    }
-    &:nth-child(5) { /* 状态 */
-      min-width: v-bind('flexColumnMinWidths.deviceStatus + "px"');
-      text-align: center;
-    }
-    &:nth-child(6) { /* 创建时间 */
-      min-width: v-bind('(flexColumnMinWidths.createTime) + "px"');
-      overflow: visible !important;
-      text-align: center;
-    }
-  }
-}
-
-.dialog-top {
-  flex-shrink: 0;
-  margin-bottom: 15px;
-
-  .el-form {
-    padding: 5px 0; // 减少表单内边距
-  }
-}
-
-.table-container {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  min-height: 0;
-  max-height: 100%;
-
-  .table-wrapper {
-    overflow: auto !important;
-    .el-table {
-      // 表格自带的滚动机制
-      ::v-deep(.el-table__body-wrapper) {
-        overflow: auto !important;
-      }
-    }
-  }
-}
-
-.no-maintenance {
-  color: red; /* 无保养项文本为红色 */
-  font-weight: bold;
-}
-
-.has-maintenance {
-  color: #67c23a; /* 有保养项文本为淡绿色 */
-  font-weight: bold;
-}
-
-.table-wrapper {
-  width: 100%;
-  overflow-y: hidden;
-  overflow-x: auto !important;
-}
-
-.no-label-radio .el-radio__label {
-  display: none;
-}
-.no-label-radio .el-radio__inner {
-  margin-right: 0;
-}
-
-/* 自定义淡绿色按钮 */
-:deep(.custom-green-button) {
-  background-color: #e1f3d8;
-  border-color: #e1f3d8;
-  color: #67c23a;
-}
-
-/* 悬停效果 */
-:deep(.custom-green-button:hover) {
-  background-color: #d1e8c0;
-  border-color: #d1e8c0;
-  color: #5daf34;
-}
-
-/* 点击效果 */
-:deep(.custom-green-button:active) {
-  background-color: #c2dca8;
-  border-color: #c2dca8;
-}
-
-// 分页样式优化 - 弹性位置
-.pagination-container {
-  margin-top: auto; // 将分页推到底部
-  padding: 15px 0 5px;
-  background: white; // 防止内容重叠
-  z-index: 10;
-}
-
-/* 使感叹号图标与标题对齐 */
-.column-header {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  .el-tooltip {
-    margin-left: 3px;
-    display: inline-flex;
-    align-items: center;
-
-    .el-icon {
-      font-size: 12px;
-      color: #e6a23c;
-      cursor: pointer;
-      transition: color 0.3s;
-
-      &:hover {
-        color: #d48816;
-      }
-    }
-  }
-}
-
-.el-tooltip {
-  cursor: pointer;
-}
-
-/* 优化图标样式 */
-.el-tooltip .el-icon-warning {
-  font-size: 18px;
-  color: #e6a23c; /* 使感叹号图标有明显的颜色 */
-}
-
-</style>

+ 214 - 0
src/views/pms/maintenance/maintenance-device-list.vue

@@ -0,0 +1,214 @@
+<script setup lang="ts">
+import { IotDeviceApi } from '@/api/pms/device'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { DICT_TYPE, realValue } from '@/utils/dict'
+import { WarningFilled } from '@element-plus/icons-vue'
+import { DeviceList, DeviceQuery } from './types'
+
+const { t } = useI18n()
+
+const emits = defineEmits<{
+  choose: [value: DeviceList[]]
+  close: []
+}>()
+
+const visible = ref(false)
+
+const open = () => {
+  visible.value = true
+  query.value = initQuery()
+  selectedRows.value = []
+  tableRef.value?.elTableRef.clearSelection()
+  getList()
+}
+
+defineExpose({ open })
+
+const initQuery = (): DeviceQuery => ({
+  pageNo: 1,
+  pageSize: 10
+})
+
+const loading = ref(false)
+const query = ref(initQuery())
+const total = ref(0)
+
+const list = ref<DeviceList[]>([])
+const selectedRows = ref<DeviceList[]>([])
+const tableRef = ref()
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const res = await IotDeviceApi.getIotDevicePage(query.value)
+    total.value = res.total
+    list.value = res.list
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSizeChange = (val: number) => {
+  query.value.pageSize = val
+  getList()
+}
+
+const handleCurrentChange = (val: number) => {
+  query.value.pageNo = val
+  getList()
+}
+
+const handleQuery = () => {
+  query.value.pageNo = 1
+  getList()
+}
+
+const reset = () => {
+  query.value = initQuery()
+  handleQuery()
+}
+
+const handleClose = () => {
+  tableRef.value?.elTableRef.clearSelection()
+  selectedRows.value = []
+  emits('close')
+}
+
+const handleConfirm = () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+
+  emits('choose', selectedRows.value)
+  visible.value = false
+}
+
+const handleSelectionChange = (val: DeviceList[]) => {
+  selectedRows.value = val
+}
+
+const handleRowClick = (row: DeviceList) => {
+  if (!row.hasSetMaintenanceBom) {
+    ElMessage.warning({
+      message: '请选择有保养项的设备',
+      grouping: true
+    })
+    return
+  }
+
+  tableRef.value?.elTableRef.toggleRowSelection(row)
+}
+
+const { ZmTable, ZmTableColumn } = useTableComponents<DeviceList>()
+</script>
+
+<template>
+  <Dialog
+    v-model="visible"
+    :title="t('deviceList.selectDevice')"
+    :style="{ width: '1200px' }"
+    :body-style="{ height: '100%' }"
+    :close-on-click-modal="false"
+    @close="handleClose">
+    <el-form
+      :model="query"
+      class="flex border-1 border-solid border-gray-300 p-4 rounded"
+      size="default"
+      label-width="86px">
+      <el-form-item class="mb-0!" :label="t('deviceList.deviceName')" prop="deviceName">
+        <el-input
+          v-model="query.deviceName"
+          @keyup.enter="handleQuery"
+          :placeholder="t('deviceList.nameHolder')"
+          clearable
+          class="!w-200px" />
+      </el-form-item>
+      <el-form-item class="mb-0!" :label="t('deviceList.deviceCode')" prop="deviceCode">
+        <el-input
+          v-model="query.deviceCode"
+          @keyup.enter="handleQuery"
+          :placeholder="t('deviceList.codeHolder')"
+          clearable
+          class="!w-200px" />
+      </el-form-item>
+      <el-form-item class="mb-0!">
+        <el-button @click="handleQuery" type="primary">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('deviceList.search') }}
+        </el-button>
+        <el-button @click="reset">
+          <Icon icon="ep:refresh" class="mr-5px" /> {{ t('deviceList.reset') }}
+        </el-button>
+        <el-button @click="handleConfirm" type="success">
+          <Icon icon="ep:check" class="mr-5px" /> {{ t('workOrderMaterial.confirm') }}
+        </el-button>
+      </el-form-item>
+    </el-form>
+    <section class="mt-4 p-4 pb-1 border-1 border-solid border-gray-300 rounded">
+      <ZmTable
+        ref="tableRef"
+        :data="list"
+        :loading="loading"
+        :height="420"
+        row-key="id"
+        :column-max-width="420"
+        @row-click="handleRowClick"
+        @selection-change="handleSelectionChange">
+        <ZmTableColumn
+          column-key="selection"
+          type="selection"
+          width="55"
+          reserve-selection
+          :selectable="(row) => row.hasSetMaintenanceBom" />
+        <ZmTableColumn :label="t('chooseMaintain.deviceCode')" prop="deviceCode" />
+        <ZmTableColumn :label="t('deviceList.deviceName')" prop="deviceName" />
+        <ZmTableColumn :label="t('iotDevice.dept')" prop="deptName" />
+        <ZmTableColumn
+          :label="t('iotDevice.status')"
+          prop="deviceStatus"
+          :real-value="(row) => realValue(DICT_TYPE.PMS_DEVICE_STATUS, row.deviceStatus ?? '')">
+          <template #default="{ row }">
+            <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus ?? ''" />
+          </template>
+        </ZmTableColumn>
+        <ZmTableColumn
+          :label="t('deviceInfo.deviceBOM')"
+          prop="hasSetMaintenanceBom"
+          :real-value="
+            (row) =>
+              row.hasSetMaintenanceBom ? t('mainPlan.haveMaintItems') : t('mainPlan.noMaintItems')
+          ">
+          <template #header>
+            {{ t('deviceInfo.deviceBOM') }}
+            <el-tooltip effect="dark" content="请选择有保养项的设备" placement="top">
+              <el-icon :size="12" color="#e6a23c" style="margin-left: 3px; cursor: pointer">
+                <WarningFilled />
+              </el-icon>
+            </el-tooltip>
+          </template>
+          <template #default="{ row }">
+            <el-tag :type="row.hasSetMaintenanceBom ? 'success' : 'danger'">
+              {{
+                row.hasSetMaintenanceBom ? t('mainPlan.haveMaintItems') : t('mainPlan.noMaintItems')
+              }}
+            </el-tag>
+          </template>
+        </ZmTableColumn>
+      </ZmTable>
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          :current-page="query.pageNo"
+          :page-size="query.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </section>
+  </Dialog>
+</template>
+
+<style scoped lang="scss"></style>

+ 526 - 0
src/views/pms/maintenance/maintenance-plan-form.vue

@@ -0,0 +1,526 @@
+<script setup lang="ts">
+import type { FormInstance, FormRules } from 'element-plus'
+import { List } from './types'
+
+const { t } = useI18n()
+
+const visible = ref(false)
+
+type AccumulatedAttr = {
+  pointName: string
+  totalRunTime?: number | null
+}
+
+interface Props {
+  row: List
+  readonly: boolean
+  timeAccumulatedAttrs: AccumulatedAttr[]
+  mileageAccumulatedAttrs: AccumulatedAttr[]
+}
+
+const form = ref<List>({
+  deviceId: 0,
+  bomNodeId: '',
+  deviceCode: '',
+  deviceName: '',
+  name: '',
+  runningTimeRule: 0,
+  mileageRule: 0,
+  naturalDateRule: 0,
+  totalRunTime: null,
+  tempTotalRunTime: null,
+  totalMileage: null,
+  tempTotalMileage: null,
+  lastMaintenanceDate: null,
+  lastRunningTime: null,
+  nextRunningTime: null,
+  lastRunningKilometers: null,
+  nextRunningKilometers: null,
+  lastNaturalDate: null,
+  nextNaturalDate: null,
+  kiloCycleLead: null,
+  timePeriodLead: null,
+  naturalDatePeriodLead: null,
+  code: null,
+  type: null
+})
+const isReadonly = ref<boolean>(false)
+const sourceRow = ref<List>()
+const formRef = ref<FormInstance>()
+const timeAccumulatedAttrs = ref<AccumulatedAttr[]>([])
+const mileageAccumulatedAttrs = ref<AccumulatedAttr[]>([])
+
+const timeEnable = computed(() => form.value.runningTimeRule === 0)
+const mileageEnable = computed(() => form.value.mileageRule === 0)
+const naturalDateEnable = computed(() => form.value.naturalDateRule === 0)
+const timeAccumulatedVisible = computed(
+  () =>
+    timeEnable.value &&
+    timeAccumulatedAttrs.value.length > 0 &&
+    (form.value.totalRunTime == null || isNaN(form.value.totalRunTime))
+)
+const mileageAccumulatedVisible = computed(
+  () =>
+    mileageEnable.value &&
+    mileageAccumulatedAttrs.value.length > 0 &&
+    (form.value.totalMileage == null || isNaN(form.value.totalMileage))
+)
+const accumulatedVisible = computed(
+  () => timeAccumulatedVisible.value || mileageAccumulatedVisible.value
+)
+
+type NumberFieldProp =
+  | 'lastRunningTime'
+  | 'nextRunningTime'
+  | 'timePeriodLead'
+  | 'lastRunningKilometers'
+  | 'nextRunningKilometers'
+  | 'kiloCycleLead'
+  | 'nextNaturalDate'
+  | 'naturalDatePeriodLead'
+type DateFieldProp = 'lastNaturalDate'
+type NumberFieldConfig = {
+  label: string
+  prop: NumberFieldProp
+  type: 'number'
+  precision?: number
+}
+type DateFieldConfig = {
+  label: string
+  prop: DateFieldProp
+  type: 'date'
+  placeholder?: string
+}
+type FieldConfig = NumberFieldConfig | DateFieldConfig
+type ConditionalFieldConfig = FieldConfig & { visible?: boolean }
+
+type SectionConfig = {
+  key: string
+  title: string
+  visible: boolean
+  fields: FieldConfig[]
+}
+
+const enabledRuleCount = computed(
+  () => [timeEnable.value, mileageEnable.value, naturalDateEnable.value].filter(Boolean).length
+)
+
+const enabledFields = (fields: ConditionalFieldConfig[]): FieldConfig[] =>
+  fields.filter(({ visible = true }) => visible)
+
+const ruleSections = computed<SectionConfig[]>(() => [
+  {
+    key: 'basic',
+    title: t('mainPlan.basicMaintenanceRecords'),
+    visible: enabledRuleCount.value > 0,
+    fields: enabledFields([
+      {
+        label: t('mainPlan.lastMaintenanceOperationTime'),
+        prop: 'lastRunningTime',
+        type: 'number',
+        precision: 1,
+        visible: timeEnable.value
+      },
+      {
+        label: t('mainPlan.lastMaintenanceMileage'),
+        prop: 'lastRunningKilometers',
+        type: 'number',
+        precision: 2,
+        visible: mileageEnable.value
+      },
+      {
+        label: t('mainPlan.lastMaintenanceNaturalDate'),
+        prop: 'lastNaturalDate',
+        type: 'date',
+        placeholder: '选择日期',
+        visible: naturalDateEnable.value
+      }
+    ])
+  },
+  {
+    key: 'time',
+    title: t('mainPlan.RunTimeRuleConfiguration'),
+    visible: timeEnable.value,
+    fields: [
+      {
+        label: t('mainPlan.RunTimeCycle'),
+        prop: 'nextRunningTime',
+        type: 'number',
+        precision: 1
+      },
+      {
+        label: t('mainPlan.RunTimeCycle_Lead'),
+        prop: 'timePeriodLead',
+        type: 'number',
+        precision: 1
+      }
+    ]
+  },
+  {
+    key: 'mileage',
+    title: t('mainPlan.operatingMileageRuleConfiguration'),
+    visible: mileageEnable.value,
+    fields: [
+      {
+        label: t('mainPlan.operatingMileageCycle'),
+        prop: 'nextRunningKilometers',
+        type: 'number',
+        precision: 2
+      },
+      {
+        label: t('mainPlan.OperatingMileageCycle_lead'),
+        prop: 'kiloCycleLead',
+        type: 'number',
+        precision: 2
+      }
+    ]
+  },
+  {
+    key: 'natural-date',
+    title: t('mainPlan.NaturalDayRuleConfig'),
+    visible: naturalDateEnable.value,
+    fields: [
+      {
+        label: t('mainPlan.NaturalDailyCycle'),
+        prop: 'nextNaturalDate',
+        type: 'number'
+      },
+      {
+        label: t('mainPlan.NaturalDailyCycle_Lead'),
+        prop: 'naturalDatePeriodLead',
+        type: 'number'
+      }
+    ]
+  }
+])
+
+const cloneRow = (row: List): List => ({
+  ...row
+})
+
+const isPositiveRequiredValue = (value: unknown) => typeof value === 'number' && value > 0
+
+const positiveNumberRule = (label: string) => ({
+  required: true,
+  validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => {
+    if (isPositiveRequiredValue(value)) {
+      callback()
+      return
+    }
+
+    callback(new Error(`请填写${label}`))
+  },
+  trigger: ['blur', 'change']
+})
+
+const requiredRule = (message: string) => ({
+  required: true,
+  message,
+  trigger: ['blur', 'change']
+})
+
+const formRules = computed<FormRules>(() => {
+  const rules: FormRules = {}
+
+  for (const section of ruleSections.value) {
+    if (!section.visible) continue
+
+    for (const field of section.fields) {
+      rules[field.prop] =
+        field.type === 'number'
+          ? [positiveNumberRule(field.label)]
+          : [requiredRule(`请选择${field.label}`)]
+    }
+  }
+
+  if (timeAccumulatedVisible.value) {
+    rules.code = [requiredRule('请选择累计运行时长')]
+  }
+
+  if (mileageAccumulatedVisible.value) {
+    rules.type = [requiredRule('请选择累计运行公里数')]
+  }
+
+  return rules
+})
+
+const emit = defineEmits<{
+  saved: []
+}>()
+
+const close = () => {
+  visible.value = false
+}
+
+const save = async () => {
+  if (isReadonly.value) {
+    close()
+    return
+  }
+
+  if (!sourceRow.value) return
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+  } catch {
+    return
+  }
+
+  const data = cloneRow(form.value)
+
+  if (timeAccumulatedVisible.value) {
+    const selectedTimeAttr = timeAccumulatedAttrs.value.find((item) => item.pointName === data.code)
+    data.tempTotalRunTime = selectedTimeAttr?.totalRunTime ?? null
+  }
+
+  if (mileageAccumulatedVisible.value) {
+    const selectedMileageAttr = mileageAccumulatedAttrs.value.find(
+      (item) => item.pointName === data.type
+    )
+    data.tempTotalMileage = selectedMileageAttr?.totalRunTime ?? null
+  }
+
+  Object.assign(sourceRow.value, data)
+  emit('saved')
+  close()
+}
+
+const open = ({
+  row,
+  readonly,
+  timeAccumulatedAttrs: timeAttrs,
+  mileageAccumulatedAttrs: mileageAttrs
+}: Props) => {
+  sourceRow.value = row
+  form.value = cloneRow(row)
+  isReadonly.value = readonly
+  timeAccumulatedAttrs.value = timeAttrs || []
+  mileageAccumulatedAttrs.value = mileageAttrs || []
+  visible.value = true
+  nextTick(() => {
+    formRef.value?.clearValidate()
+  })
+}
+
+defineExpose({
+  open
+})
+</script>
+
+<template>
+  <Dialog v-model="visible" title="保养项配置" width="720px" :close-on-click-modal="false">
+    <el-form
+      ref="formRef"
+      size="default"
+      :model="form"
+      :rules="formRules"
+      label-position="top"
+      class="maintenance-config-form">
+      <div class="config-summary">
+        <div class="summary-item">
+          <span class="summary-label">{{ t('iotMaintain.deviceCode') }}</span>
+          <span class="summary-value">{{ form.deviceCode || '-' }}</span>
+        </div>
+        <div class="summary-item">
+          <span class="summary-label">{{ t('iotMaintain.deviceName') }}</span>
+          <span class="summary-value">{{ form.deviceName || '-' }}</span>
+        </div>
+        <div class="summary-item summary-item-full">
+          <span class="summary-label">{{ t('bomList.bomNode') }}</span>
+          <span class="summary-value">{{ form.name || '-' }}</span>
+        </div>
+      </div>
+
+      <el-empty v-if="enabledRuleCount === 0" :image-size="88" description="当前保养项未启用规则" />
+
+      <section
+        v-for="section in ruleSections"
+        v-show="section.visible"
+        :key="section.key"
+        class="config-section">
+        <div class="section-header">
+          <div class="section-title">{{ section.title }}</div>
+        </div>
+        <el-row :gutter="16">
+          <el-col v-for="field in section.fields" :key="field.prop" :xs="24" :sm="12">
+            <el-form-item :label="field.label" :prop="field.prop">
+              <el-input-number
+                v-if="field.type === 'number'"
+                v-model="form[field.prop]"
+                :precision="field.precision"
+                :min="0"
+                :controls="false"
+                :disabled="isReadonly"
+                class="w-full!" />
+              <el-date-picker
+                v-else
+                v-model="form[field.prop]"
+                type="date"
+                :placeholder="field.placeholder"
+                value-format="x"
+                :disabled="isReadonly"
+                class="w-full!" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </section>
+      <section v-if="accumulatedVisible" class="config-section">
+        <div class="section-header">
+          <div class="section-title">{{ t('mainPlan.accumulatedParams') }}</div>
+        </div>
+        <el-row :gutter="16">
+          <el-col v-if="timeAccumulatedVisible" :xs="24" :sm="12">
+            <el-form-item :label="t('mainPlan.accumulatedRunTime')" prop="code">
+              <el-select
+                v-model="form.code"
+                placeholder="请选择累计运行时长"
+                clearable
+                :disabled="isReadonly"
+                class="w-full!">
+                <el-option
+                  v-for="(item, index) in timeAccumulatedAttrs"
+                  :key="`time-${item.pointName}-${index}`"
+                  :label="item.pointName"
+                  :value="item.pointName" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col v-if="mileageAccumulatedVisible" :xs="24" :sm="12">
+            <el-form-item :label="t('mainPlan.accumulatedMileage')" prop="type">
+              <el-select
+                v-model="form.type"
+                placeholder="请选择累计运行公里数"
+                clearable
+                :disabled="isReadonly"
+                class="w-full!">
+                <el-option
+                  v-for="(item, index) in mileageAccumulatedAttrs"
+                  :key="`mileage-${item.pointName}-${index}`"
+                  :label="item.pointName"
+                  :value="item.pointName" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </section>
+    </el-form>
+    <template #footer>
+      <el-button @click="close">{{ t('common.cancel') }}</el-button>
+      <el-button v-if="!isReadonly" type="primary" @click="save">{{ t('common.save') }}</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<style scoped lang="scss">
+.maintenance-config-form {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.config-summary {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px 14px;
+  padding: 12px 14px;
+  background: var(--el-fill-color-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.summary-item {
+  min-width: 0;
+}
+
+.summary-item-full {
+  grid-column: 1 / -1;
+}
+
+.summary-label {
+  display: block;
+  margin-bottom: 4px;
+  font-size: 12px;
+  line-height: 1.2;
+  color: var(--el-text-color-secondary);
+}
+
+.summary-value {
+  display: block;
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 1.4;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.config-section {
+  padding: 14px 14px 2px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.section-title {
+  position: relative;
+  padding-left: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 1.4;
+  color: var(--el-text-color-primary);
+}
+
+.section-title::before {
+  position: absolute;
+  top: 3px;
+  bottom: 3px;
+  left: 0;
+  width: 3px;
+  background: var(--el-color-primary);
+  border-radius: 3px;
+  content: '';
+}
+
+.form-control {
+  width: 100% !important;
+}
+
+.maintenance-config-form :deep(.el-form-item) {
+  margin-bottom: 14px;
+}
+
+.maintenance-config-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  font-size: 13px;
+  line-height: 1.4;
+  color: var(--el-text-color-regular);
+}
+
+.maintenance-config-form :deep(.el-input__wrapper) {
+  border-radius: 6px;
+  box-shadow: 0 0 0 1px var(--el-border-color-light) inset;
+}
+
+.maintenance-config-form :deep(.el-input__wrapper:hover) {
+  box-shadow: 0 0 0 1px var(--el-border-color) inset;
+}
+
+.maintenance-config-form :deep(.el-input.is-disabled .el-input__wrapper) {
+  background-color: var(--el-fill-color-lighter);
+}
+
+@media (width <= 640px) {
+  .config-summary {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 873 - 0
src/views/pms/maintenance/maintenance-plan-manage.vue

@@ -0,0 +1,873 @@
+<script lang="ts" setup>
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { useUserStore } from '@/store/modules/user'
+import * as DeptApi from '@/api/system/dept'
+import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
+import { dayjs, FormInstance, FormRules } from 'element-plus'
+import { IotMaintenanceBomApi } from '@/api/pms/iotmaintenancebom'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { DeviceList, List, Plan, Query } from './types'
+import { IotDeviceApi } from '@/api/pms/device'
+import MaintenancePlanForm from './maintenance-plan-form.vue'
+import maintenanceDeviceList from './maintenance-device-list.vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const route = useRoute()
+const router = useRouter()
+const { delView } = useTagsViewStore()
+const message = useMessage()
+const { t } = useI18n()
+
+const mode = computed(() => {
+  if (route.name === 'IotMaintenancePlanDetail') return 'detail'
+  if (route.name === 'IotMainPlanEdit') return 'edit'
+  return 'create'
+})
+
+const isReadonly = computed(() => mode.value === 'detail')
+
+const id = computed(() => route.params.id as string | undefined)
+
+const initQuery = (): Query => ({
+  deviceIds: undefined,
+  planId: undefined,
+  bomFlag: 'b'
+})
+
+const queryParams = ref<Query>(initQuery())
+
+const initPlan = (): Plan => ({
+  name: '',
+  remark: '',
+  responsiblePerson: '',
+  serialNumber: ''
+})
+
+const list = ref<List[]>([])
+
+const { ZmTable, ZmTableColumn } = useTableComponents<List>()
+
+const loading = ref(false)
+const panelLoading = ref(false)
+const saving = ref(false)
+const plan = ref<Plan>(initPlan())
+const deviceIds = ref<Set<number>>(new Set())
+const originalRowMap = ref(new Map<string, List>())
+const attrs = ref(
+  new Map<string, { timeAccumulatedAttrs: any[]; mileageAccumulatedAttrs: any[] }>()
+)
+const planRef = ref<FormInstance>()
+const planRules = reactive<FormRules>({
+  name: [{ required: true, message: '计划名称不能为空', trigger: ['blur', 'change'] }]
+})
+
+const getRowKey = (row: Pick<List, 'deviceId' | 'bomNodeId'>) => `${row.deviceId}-${row.bomNodeId}`
+
+const cloneRow = (row: List): List => ({
+  ...row
+})
+
+const refreshOriginalRows = () => {
+  const currentKeys = new Set(list.value.map(getRowKey))
+
+  list.value.forEach((row) => {
+    const key = getRowKey(row)
+    originalRowMap.value.set(key, cloneRow(row))
+  })
+
+  Array.from(originalRowMap.value.keys()).forEach((key) => {
+    if (!currentKeys.has(key)) {
+      originalRowMap.value.delete(key)
+    }
+  })
+}
+
+const resetAttrs = (rows: List[]) => {
+  attrs.value.clear()
+  rows.forEach((row: any) => {
+    attrs.value.set(getRowKey(row), {
+      timeAccumulatedAttrs: row.timeAccumulatedAttrs || [],
+      mileageAccumulatedAttrs: row.mileageAccumulatedAttrs || []
+    })
+  })
+}
+
+const syncListMeta = () => {
+  deviceIds.value = new Set(list.value.map((item) => item.deviceId))
+  sortList()
+}
+
+const resetPageRows = (rows: List[]) => {
+  list.value = rows
+  resetAttrs(rows)
+  refreshOriginalRows()
+  syncListMeta()
+}
+
+const initPage = async () => {
+  panelLoading.value = true
+
+  plan.value = initPlan()
+  queryParams.value = initQuery()
+
+  try {
+    if (mode.value === 'create') {
+      const deptId = useUserStore().getUser.deptId
+      const dept = await DeptApi.getDept(deptId)
+      plan.value.name = `${dept?.name || ''} - 保养计划`
+      plan.value.deptId = deptId
+
+      const { wsCache } = useCache()
+      const userInfo = wsCache.get(CACHE_KEY.USER)
+      plan.value.responsiblePerson = userInfo.user.id
+    } else if (id.value) {
+      const res = await IotMaintenancePlanApi.getIotMaintenancePlan(Number(id.value))
+      plan.value = res
+
+      queryParams.value.planId = Number(id.value)
+      const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams.value)
+      resetPageRows(data || [])
+    }
+  } finally {
+    panelLoading.value = false
+  }
+}
+
+const sortList = () => {
+  list.value = list.value.sort((a, b) => {
+    const aCode = a.deviceCode || ''
+    const bCode = b.deviceCode || ''
+    const aName = a.name || ''
+    const bName = b.name || ''
+
+    if (aCode !== bCode) {
+      return aCode.localeCompare(bCode)
+    }
+
+    return aName.localeCompare(bName)
+  })
+}
+
+watch(
+  () => route.fullPath,
+  () => {
+    initPage()
+  },
+  { immediate: true }
+)
+
+watch(
+  () => list.value.length,
+  () => {
+    syncListMeta()
+  }
+)
+
+const runningTimeVisible = computed(() => list.value.some((item) => item.runningTimeRule === 0))
+const mileageVisible = computed(() => list.value.some((item) => item.mileageRule === 0))
+const naturalDateVisible = computed(() => list.value.some((item) => item.naturalDateRule === 0))
+
+const EMPTY_TEXT = '——'
+type DisplayValue = number | string
+
+const isPositiveNumber = (value: unknown): value is number => typeof value === 'number' && value > 0
+
+const toFixedNumber = (value: number, fractionDigits = 2) =>
+  parseFloat(value.toFixed(fractionDigits))
+
+const emptyFormatter = (row: List, keys: (keyof List)[], isTime: boolean = false) => {
+  const data = [...keys].reverse().find((key) => row[key] !== null && row[key] !== undefined)
+  const value = data ? row[data] : EMPTY_TEXT
+
+  return value !== EMPTY_TEXT && isTime ? dayjs(value as number).format('YYYY-MM-DD') : value
+}
+
+const positiveField = (row: List, key: keyof List) => {
+  const value = row[key]
+  return isPositiveNumber(value) ? value : null
+}
+
+const positiveDisplayValue = (row: List, keys: (keyof List)[]) => {
+  const value = emptyFormatter(row, keys)
+  return isPositiveNumber(value) ? value : null
+}
+
+const ruleValue = (
+  rule: List['runningTimeRule'],
+  values: Array<number | null>,
+  calculator: (...values: number[]) => DisplayValue
+) => {
+  const numericValues = values.filter(isPositiveNumber)
+  return rule === 0 && numericValues.length === values.length
+    ? calculator(...numericValues)
+    : EMPTY_TEXT
+}
+
+const nextMaintenanceH = (row: List) =>
+  ruleValue(
+    row.runningTimeRule,
+    [positiveField(row, 'lastRunningTime'), positiveField(row, 'nextRunningTime')],
+    (last, next) => last + next
+  )
+
+const remainH = (row: List) =>
+  ruleValue(
+    row.runningTimeRule,
+    [
+      positiveField(row, 'lastRunningTime'),
+      positiveField(row, 'nextRunningTime'),
+      positiveDisplayValue(row, ['totalRunTime', 'tempTotalRunTime'])
+    ],
+    (last, next, current) => toFixedNumber(next - (current - last))
+  )
+
+const nextMaintenanceKm = (row: List) =>
+  ruleValue(
+    row.mileageRule,
+    [positiveField(row, 'lastRunningKilometers'), positiveField(row, 'nextRunningKilometers')],
+    (last, next) => last + next
+  )
+
+const remainKm = (row: List) =>
+  ruleValue(
+    row.mileageRule,
+    [
+      positiveField(row, 'lastRunningKilometers'),
+      positiveField(row, 'nextRunningKilometers'),
+      positiveDisplayValue(row, ['totalMileage', 'tempTotalMileage'])
+    ],
+    (last, next, current) => toFixedNumber(next - (current - last))
+  )
+
+const nextNaturalDateValue = (row: List) => {
+  if (row.naturalDateRule !== 0 || !row.lastNaturalDate || !row.nextNaturalDate) return null
+  return dayjs(row.lastNaturalDate).add(row.nextNaturalDate, 'day')
+}
+
+const nextMaintenanceDate = (row: List) =>
+  nextNaturalDateValue(row)?.format('YYYY-MM-DD') ?? EMPTY_TEXT
+
+const remainDay = (row: List) =>
+  isPositiveNumber(row.nextNaturalDate)
+    ? (nextNaturalDateValue(row)?.diff(dayjs(), 'day') ?? EMPTY_TEXT)
+    : EMPTY_TEXT
+
+const hasEnabledRule = (row: List) =>
+  row.runningTimeRule === 0 || row.mileageRule === 0 || row.naturalDateRule === 0
+
+const requiresTimeAccumulatedAttr = (row: List) => {
+  const rowAttrs = attrs.value.get(getRowKey(row))
+  return (
+    row.runningTimeRule === 0 &&
+    (rowAttrs?.timeAccumulatedAttrs.length ?? 0) > 0 &&
+    (row.totalRunTime == null || isNaN(row.totalRunTime))
+  )
+}
+
+const requiresMileageAccumulatedAttr = (row: List) => {
+  const rowAttrs = attrs.value.get(getRowKey(row))
+  return (
+    row.mileageRule === 0 &&
+    (rowAttrs?.mileageAccumulatedAttrs.length ?? 0) > 0 &&
+    (row.totalMileage == null || isNaN(row.totalMileage))
+  )
+}
+
+const checkRowFilled = (row: List) => {
+  if (!hasEnabledRule(row)) return false
+
+  const runningTimeFilled =
+    row.runningTimeRule !== 0 ||
+    (isPositiveNumber(row.lastRunningTime) &&
+      isPositiveNumber(row.nextRunningTime) &&
+      isPositiveNumber(row.timePeriodLead) &&
+      (!requiresTimeAccumulatedAttr(row) || !!row.code))
+
+  const mileageFilled =
+    row.mileageRule !== 0 ||
+    (isPositiveNumber(row.lastRunningKilometers) &&
+      isPositiveNumber(row.nextRunningKilometers) &&
+      isPositiveNumber(row.kiloCycleLead) &&
+      (!requiresMileageAccumulatedAttr(row) || !!row.type))
+
+  const naturalDateFilled =
+    row.naturalDateRule !== 0 ||
+    (!!row.lastNaturalDate &&
+      isPositiveNumber(row.nextNaturalDate) &&
+      isPositiveNumber(row.naturalDatePeriodLead))
+
+  return runningTimeFilled && mileageFilled && naturalDateFilled
+}
+
+const cellClassName = ({ row, column }: { row: List; column: { type?: string } }) =>
+  column.type === 'index' && checkRowFilled(row) ? 'all-filled' : ''
+
+const getRowDisplayName = (row: List) => `${row.deviceCode || '-'}-${row.name || '-'}`
+
+const validateTableData = () => {
+  if (list.value.length === 0) {
+    message.error('请至少添加一条设备保养明细')
+    return false
+  }
+
+  for (const [index, row] of list.value.entries()) {
+    const rowNumber = index + 1
+    const rowName = getRowDisplayName(row)
+
+    if (!hasEnabledRule(row)) {
+      message.error(`第 ${rowNumber} 行(${rowName}):保养项至少设置1个保养规则`)
+      return false
+    }
+
+    if (row.runningTimeRule === 0) {
+      if (
+        !isPositiveNumber(row.lastRunningTime) ||
+        !isPositiveNumber(row.nextRunningTime) ||
+        !isPositiveNumber(row.timePeriodLead)
+      ) {
+        message.error(`第 ${rowNumber} 行(${rowName}):请完整配置运行时长规则`)
+        return false
+      }
+
+      if (requiresTimeAccumulatedAttr(row) && !row.code) {
+        message.error(`第 ${rowNumber} 行(${rowName}):请选择累计运行时长参数`)
+        return false
+      }
+    }
+
+    if (row.mileageRule === 0) {
+      if (
+        !isPositiveNumber(row.lastRunningKilometers) ||
+        !isPositiveNumber(row.nextRunningKilometers) ||
+        !isPositiveNumber(row.kiloCycleLead)
+      ) {
+        message.error(`第 ${rowNumber} 行(${rowName}):请完整配置运行里程规则`)
+        return false
+      }
+
+      if (requiresMileageAccumulatedAttr(row) && !row.type) {
+        message.error(`第 ${rowNumber} 行(${rowName}):请选择累计运行公里数参数`)
+        return false
+      }
+    }
+
+    if (
+      row.naturalDateRule === 0 &&
+      (!row.lastNaturalDate ||
+        !isPositiveNumber(row.nextNaturalDate) ||
+        !isPositiveNumber(row.naturalDatePeriodLead))
+    ) {
+      message.error(`第 ${rowNumber} 行(${rowName}):请完整配置自然日期规则`)
+      return false
+    }
+  }
+
+  return true
+}
+
+const close = () => {
+  delView(unref(router.currentRoute))
+  router.push({ name: 'IotMaintenancePlan', params: {} })
+}
+
+const savePlan = async () => {
+  if (isReadonly.value) return
+  if (!planRef.value) return
+
+  try {
+    await planRef.value.validate()
+  } catch {
+    return
+  }
+
+  if (!validateTableData()) return
+
+  saving.value = true
+  try {
+    const data = {
+      mainPlan: plan.value,
+      mainPlanBom: list.value
+    }
+
+    if (mode.value === 'create') {
+      await IotMaintenancePlanApi.createIotMaintenancePlan(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotMaintenancePlanApi.updatePlan(data)
+      message.success(t('common.updateSuccess'))
+    }
+
+    close()
+  } finally {
+    saving.value = false
+  }
+}
+
+type RuleType = 'runningTime' | 'mileage' | 'naturalDate'
+type RuleField =
+  | 'tempTotalRunTime'
+  | 'lastRunningTime'
+  | 'nextRunningTime'
+  | 'timePeriodLead'
+  | 'tempTotalMileage'
+  | 'lastRunningKilometers'
+  | 'nextRunningKilometers'
+  | 'kiloCycleLead'
+  | 'lastNaturalDate'
+  | 'nextNaturalDate'
+  | 'naturalDatePeriodLead'
+  | 'code'
+  | 'type'
+
+const ruleFieldMap: Record<RuleType, RuleField[]> = {
+  runningTime: ['lastRunningTime', 'nextRunningTime', 'timePeriodLead', 'code', 'tempTotalRunTime'],
+  mileage: [
+    'lastRunningKilometers',
+    'nextRunningKilometers',
+    'kiloCycleLead',
+    'type',
+    'tempTotalMileage'
+  ],
+  naturalDate: ['lastNaturalDate', 'nextNaturalDate', 'naturalDatePeriodLead']
+}
+
+const clearRuleFields = (row: List, fields: RuleField[]) => {
+  fields.forEach((field) => {
+    row[field] = null
+  })
+}
+
+const restoreRuleFields = (row: List, fields: RuleField[]) => {
+  const originalRow = originalRowMap.value.get(getRowKey(row))
+  if (!originalRow) return
+
+  fields.forEach((field) => {
+    row[field] = originalRow[field] as any
+  })
+}
+
+const handleRuleChange = (row: List, ruleType: RuleType, value: unknown) => {
+  const fields = ruleFieldMap[ruleType]
+
+  if (value === 1) {
+    clearRuleFields(row, fields)
+    return
+  }
+
+  restoreRuleFields(row, fields)
+}
+
+const deviceList = ref()
+
+const onDeviceSelect = () => {
+  if (isReadonly.value) return
+  deviceList.value.open()
+}
+
+const deviceChoose = async (rows: DeviceList[]) => {
+  const selectIds = rows.map((item) => item.id)
+  const params = {
+    deviceIds: selectIds.join(','),
+    bomFlag: 'b'
+  }
+  try {
+    const res = await IotDeviceApi.deviceAssociateBomList(params)
+
+    if (res.length === 0) {
+      message.error('选择的设备不存在待保养BOM项')
+    }
+
+    if (!Array.isArray(res)) return
+
+    const existingKeys = new Set(list.value.map((item) => `${item.deviceId}-${item.bomNodeId}`))
+
+    const items = res
+      .filter((item) => !existingKeys.has(`${item.id}-${item.bomNodeId}`))
+      .map<List>((item) => {
+        const row: List = {
+          deviceId: item.id,
+          bomNodeId: item.bomNodeId,
+          deviceCode: item.deviceCode,
+          deviceName: item.deviceName,
+          name: item.name,
+          runningTimeRule: 1,
+          mileageRule: 1,
+          naturalDateRule: 1,
+          totalRunTime: item.totalRunTime,
+          tempTotalRunTime: null,
+          totalMileage: item.totalMileage,
+          tempTotalMileage: null,
+          lastMaintenanceDate: null,
+          lastRunningTime: null,
+          nextRunningTime: null,
+          lastRunningKilometers: null,
+          nextRunningKilometers: null,
+          lastNaturalDate: null,
+          nextNaturalDate: null,
+          kiloCycleLead: null,
+          timePeriodLead: null,
+          naturalDatePeriodLead: null,
+          code: item.code ?? null,
+          type: item.type ?? null
+        }
+
+        attrs.value.set(getRowKey(row), {
+          timeAccumulatedAttrs: item.timeAccumulatedAttrs || [],
+          mileageAccumulatedAttrs: item.mileageAccumulatedAttrs || []
+        })
+
+        return row
+      })
+
+    list.value.push(...items)
+    refreshOriginalRows()
+    syncListMeta()
+  } catch (error) {
+    message.error('获取设备关联保养BOM项失败')
+  }
+}
+
+const handleDelete = (str: string) => {
+  list.value = list.value.filter((item) => `${item.deviceId}-${item.bomNodeId}` !== str)
+}
+
+const planForm = ref()
+
+const editPlan = (row: List) => {
+  if (row.runningTimeRule !== 0 && row.mileageRule !== 0 && row.naturalDateRule !== 0) {
+    message.error('请先设置保养规则')
+    return
+  }
+
+  const rowAttrs = attrs.value.get(getRowKey(row))
+  planForm.value?.open({
+    row,
+    readonly: isReadonly.value,
+    timeAccumulatedAttrs: rowAttrs?.timeAccumulatedAttrs || [],
+    mileageAccumulatedAttrs: rowAttrs?.mileageAccumulatedAttrs || []
+  })
+}
+</script>
+
+<template>
+  <div
+    v-loading="panelLoading"
+    class="flex flex-col gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <section class="panel">
+      <div class="plan-info-accent"></div>
+      <el-form
+        :model="plan"
+        size="default"
+        :rules="planRules"
+        label-width="88px"
+        class="plan-info-form"
+        ref="planRef">
+        <el-row :gutter="24">
+          <el-col :xs="24" :md="13">
+            <el-form-item :label="t('main.planName')" prop="name">
+              <el-input v-model="plan.name" :disabled="isReadonly" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :md="11">
+            <el-form-item :label="t('main.planCode')" prop="serialNumber">
+              <el-input v-model="plan.serialNumber" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" class="mt-2">
+            <el-form-item :label="t('iotMaintain.remark')" prop="remark">
+              <el-input
+                v-model="plan.remark"
+                type="textarea"
+                :rows="3"
+                :placeholder="t('iotMaintain.remarkHolder')"
+                :disabled="isReadonly"
+                resize="none"
+                show-word-limit
+                maxlength="300" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </section>
+    <section class="panel content-panel flex-1!">
+      <div v-if="!isReadonly" class="operation">
+        <el-button size="default" type="success" @click="onDeviceSelect">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+      </div>
+      <div class="table">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :highlight-current-row="false"
+              :cell-class-name="cellClassName">
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn
+                :min-width="120"
+                prop="deviceCode"
+                :label="t('iotMaintain.deviceCode')" />
+              <ZmTableColumn
+                :min-width="240"
+                prop="deviceName"
+                :label="t('iotMaintain.deviceName')" />
+              <ZmTableColumn :min-width="240" prop="name" :label="t('bomList.bomNode')" />
+              <ZmTableColumn prop="runningTimeRule" width="66" :label="t('main.runTime')">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.runningTimeRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="(value) => handleRuleChange(row, 'runningTime', value)" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="mileageRule" width="66" :label="t('main.mileage')">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.mileageRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="(value) => handleRuleChange(row, 'mileage', value)" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn prop="naturalDateRule" width="66" :label="t('main.date')">
+                <template #default="{ row }">
+                  <el-switch
+                    v-model="row.naturalDateRule"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="isReadonly"
+                    @change="(value) => handleRuleChange(row, 'naturalDate', value)" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="totalRunTime"
+                :label="t('operationFillForm.sumTime')"
+                width="108"
+                :real-value="(row) => emptyFormatter(row, ['totalRunTime', 'tempTotalRunTime'])"
+                cover-formatter />
+              <ZmTableColumn
+                prop="totalMileage"
+                :label="t('operationFillForm.sumKil')"
+                width="128"
+                :real-value="(row) => emptyFormatter(row, ['totalMileage', 'tempTotalMileage'])"
+                cover-formatter />
+              <ZmTableColumn
+                prop="lastMaintenanceDate"
+                :label="t('mainPlan.lastMaintenanceDate')"
+                width="90"
+                :real-value="(row) => emptyFormatter(row, ['lastMaintenanceDate'], true)"
+                cover-formatter />
+
+              <ZmTableColumn
+                :visible="runningTimeVisible"
+                column-key="time-group"
+                label="保养时长"
+                is-parent>
+                <ZmTableColumn
+                  prop="lastRunningTime"
+                  :label="t('mainPlan.lastMaintenanceOperationTime')"
+                  :real-value="(row) => emptyFormatter(row, ['lastRunningTime'])"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="nextMaintenanceH"
+                  :label="t('mainPlan.nextMaintenanceH')"
+                  :real-value="nextMaintenanceH"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="remainH"
+                  :label="t('mainPlan.remainH')"
+                  :real-value="remainH"
+                  cover-formatter />
+              </ZmTableColumn>
+              <ZmTableColumn
+                :visible="mileageVisible"
+                column-key="mileage-group"
+                label="保养里程"
+                is-parent>
+                <ZmTableColumn
+                  prop="lastRunningKilometers"
+                  :label="t('mainPlan.lastMaintenanceMileage')"
+                  :real-value="(row) => emptyFormatter(row, ['lastRunningKilometers'])"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="nextRunningKilometers"
+                  :label="t('mainPlan.nextMaintenanceKm')"
+                  :real-value="nextMaintenanceKm"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="remainKmometers"
+                  :label="t('mainPlan.remainKm')"
+                  :real-value="remainKm"
+                  cover-formatter />
+              </ZmTableColumn>
+              <ZmTableColumn
+                :visible="naturalDateVisible"
+                column-key="date-group"
+                label="保养日期"
+                is-parent>
+                <ZmTableColumn
+                  prop="lastNaturalDate"
+                  :label="t('mainPlan.lastMaintenanceNaturalDate')"
+                  :real-value="(row) => emptyFormatter(row, ['lastNaturalDate'], true)"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="nextMaintenanceDate"
+                  :label="t('mainPlan.nextMaintDate')"
+                  :real-value="nextMaintenanceDate"
+                  cover-formatter />
+                <ZmTableColumn
+                  prop="remainDay"
+                  :label="t('mainPlan.remainDay')"
+                  :real-value="remainDay"
+                  cover-formatter />
+              </ZmTableColumn>
+              <ZmTableColumn
+                column-key="operation"
+                :label="t('operationFill.operation')"
+                :width="140"
+                fixed="right">
+                <template #default="{ row }">
+                  <el-button size="default" link type="primary" @click="editPlan(row)">
+                    <div class="i-lucide:edit-3 mr-1 translate-y-1px"></div>
+                    {{ isReadonly ? t('form.set') : t('modelTemplate.update') }}
+                  </el-button>
+                  <el-button
+                    size="default"
+                    v-if="!isReadonly"
+                    link
+                    type="danger"
+                    @click="handleDelete(`${row.deviceId}-${row.bomNodeId}`)">
+                    <div class="i-lucide:x mr-1 translate-y-1px"></div>
+                    {{ t('modelTemplate.delete') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </section>
+    <section class="panel footer-panel">
+      <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+      <el-button v-if="!isReadonly" type="primary" :loading="saving" @click="savePlan">{{
+        t('iotMaintain.save')
+      }}</el-button>
+    </section>
+  </div>
+  <maintenance-device-list ref="deviceList" @choose="deviceChoose" />
+  <maintenance-plan-form ref="planForm" @saved="refreshOriginalRows" />
+</template>
+
+<style scoped>
+.panel {
+  position: relative;
+  flex: 0 0 auto;
+  padding: 18px 20px 8px;
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
+}
+
+.plan-info-accent {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  height: 3px;
+  background: linear-gradient(
+    90deg,
+    var(--el-color-primary),
+    var(--el-color-success),
+    var(--el-color-warning)
+  );
+}
+
+.plan-info-form {
+  width: 100%;
+}
+
+.plan-info-form :deep(.el-form-item) {
+  margin-bottom: 14px;
+}
+
+.plan-info-form :deep(.el-form-item__label) {
+  height: 32px;
+  font-size: 13px;
+  line-height: 32px;
+  color: var(--el-text-color-regular);
+}
+
+.plan-info-form :deep(.el-input__wrapper),
+.plan-info-form :deep(.el-textarea__inner) {
+  border-radius: 6px;
+  box-shadow: 0 0 0 1px var(--el-border-color-light) inset;
+}
+
+.plan-info-form :deep(.el-input__wrapper:hover),
+.plan-info-form :deep(.el-textarea__inner:hover) {
+  box-shadow: 0 0 0 1px var(--el-border-color) inset;
+}
+
+.plan-info-form :deep(.el-input.is-disabled .el-input__wrapper) {
+  background-color: var(--el-fill-color-lighter);
+}
+
+.plan-info-form :deep(.el-textarea__inner) {
+  min-height: 76px !important;
+  padding-top: 8px;
+  line-height: 1.6;
+}
+
+.content-panel {
+  display: flex;
+  min-height: 0;
+  flex-direction: column;
+}
+
+.operation {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  justify-content: flex-end;
+  padding-bottom: 12px;
+}
+
+.table {
+  position: relative;
+  min-height: 0;
+  flex: 1 1 auto;
+}
+
+.footer-panel {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 12px 20px;
+}
+
+:deep(.zm-table .all-filled) {
+  background-color: #67c23a !important;
+}
+
+:deep(.zm-table .all-filled .cell) {
+  color: #fff;
+}
+
+@media (width <= 768px) {
+  .panel {
+    padding: 16px 14px 4px;
+  }
+}
+</style>

+ 59 - 0
src/views/pms/maintenance/types.ts

@@ -0,0 +1,59 @@
+export interface DeviceQuery {
+  pageNo: number
+  pageSize: number
+  deviceName?: string
+  deviceCode?: string
+}
+
+export interface DeviceList {
+  id: number
+  deviceCode: string
+  deviceName: string
+  deptName: string
+  deviceStatus: string
+  hasSetMaintenanceBom: boolean
+}
+
+export interface Query {
+  deviceIds?: string
+  planId?: number
+  bomFlag?: string
+}
+
+export interface Plan {
+  createtime?: number
+  id?: number
+  deptId?: number
+  name: string
+  remark: string
+  responsiblePerson: string
+  serialNumber: string
+  status?: number
+}
+
+export interface List {
+  deviceId: number
+  bomNodeId: string
+  deviceCode: string
+  deviceName: string
+  name: string
+  runningTimeRule: 0 | 1
+  mileageRule: 0 | 1
+  naturalDateRule: 0 | 1
+  totalRunTime: number | null
+  tempTotalRunTime: number | null
+  totalMileage: number | null
+  tempTotalMileage: number | null
+  lastMaintenanceDate: number | null
+  lastRunningTime: number | null
+  nextRunningTime: number | null
+  lastRunningKilometers: number | null
+  nextRunningKilometers: number | null
+  lastNaturalDate: number | null
+  nextNaturalDate: number | null
+  kiloCycleLead: number | null
+  timePeriodLead: number | null
+  naturalDatePeriodLead: number | null
+  code: string | null
+  type: string | null
+}

+ 1 - 1
stylelint.config.js

@@ -13,7 +13,7 @@ module.exports = {
     'at-rule-no-unknown': [
       true,
       {
-        ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend']
+        ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend', 'apply']
       }
     ],
     'media-query-no-invalid': null,

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