yanghao vor 6 Stunden
Ursprung
Commit
91b55332f5

+ 1 - 1
.env.dev

@@ -4,7 +4,7 @@ NODE_ENV=production
 VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='https://iot.deepoil.cc:5443'
 
 # MQTT服务地址
 VITE_MQTT_SERVER_URL = ''

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.200:48080  https://iot.deepoil.cc  http://172.26.0.56:48080
-VITE_BASE_URL='http://172.21.0.198:58080'
+VITE_BASE_URL='https://iot.deepoil.cc:5443'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

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

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

+ 4 - 6
src/components/ZmTable/README.md

@@ -73,6 +73,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
 | `columnMaxWidth` | `number` | `360` | 自动计算列宽时的最大宽度。 |
 | `customClass` | `boolean` | `false` | 为 `true` 时不挂载默认 `.zm-table` class,也就不会使用组件默认样式变量。 |
 | `showBorder` | `boolean` | `false` | 控制组件自己的边框显示样式。 |
+| `hoverHighlight` | `boolean` | `true` | 控制鼠标悬浮行时是否显示 hover 背景。 |
 | `showOverflowTooltip` | `boolean` | `true` | 继承自 Element Plus。想让文字换行时传 `false`,再配合页面 CSS 覆盖 `.cell` 的换行样式。 |
 
 组件内部默认还会给 Element Plus 表格设置这些值:
@@ -147,8 +148,7 @@ function handleSort(prop: string, order: 'asc' | 'desc' | null) {
   label="会议日期"
   zm-sortable
   v-model:sort-order="meetingDateOrder"
-  @sort-change="handleSortChange"
-/>
+  @sort-change="handleSortChange" />
 ```
 
 ```ts
@@ -171,8 +171,7 @@ function handleSortChange(payload: { prop: string; order: 'asc' | 'desc' | null
   label="状态"
   zm-filterable
   v-model:filter-model-value="query.status"
-  @filter-visible-change="handleFilterVisibleChange"
->
+  @filter-visible-change="handleFilterVisibleChange">
   <template #filter="{ filterModelValue, updateFilterModelValue, close }">
     <div class="p-2">
       <el-select
@@ -329,8 +328,7 @@ Element Plus 原生多级表头可以直接写:
   prop="meetingDate"
   label="会议日期"
   cover-formatter
-  :real-value="(row) => dayjs(row.meetingDate).format('YYYY-MM-DD')"
-/>
+  :real-value="(row) => dayjs(row.meetingDate).format('YYYY-MM-DD')" />
 ```
 
 ## 插槽

+ 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>

+ 42 - 17
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
@@ -34,6 +35,7 @@ interface Props
   loading: boolean
   customClass?: boolean
   showBorder?: boolean
+  hoverHighlight?: boolean
   align?: ColumnAlign
   columnMaxWidth?: number
 }
@@ -54,6 +56,7 @@ const defaultOptions: Partial<Props> = {
   showOverflowTooltip: true,
   scrollbarAlwaysOn: true,
   showBorder: false,
+  hoverHighlight: true,
   customClass: false,
   tooltipOptions: {
     popperClass: 'max-w-120'
@@ -65,6 +68,7 @@ const bindProps = computed(() => {
     data,
     customClass: _customClass,
     showBorder: _showBorder,
+    hoverHighlight: _hoverHighlight,
     align: _align,
     columnMaxWidth,
     ...otherProps
@@ -163,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')),
@@ -315,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) => {
@@ -328,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
@@ -339,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]
   })
 }
 
@@ -378,10 +396,13 @@ defineExpose({
   <el-table
     ref="tableRef"
     v-loading="loading"
-    :class="{ 'zm-table': !customClass, 'show-border': showBorder }"
+    :class="{
+      'zm-table': !customClass,
+      'show-border': showBorder,
+      'is-hover-highlight-disabled': hoverHighlight === false
+    }"
     v-bind="bindProps"
-    :data="data"
-  >
+    :data="data">
     <template v-for="(_, name) in forwardedSlots" #[name]="slotData">
       <slot :name="name" v-bind="slotData || {}"></slot>
     </template>
@@ -549,13 +570,6 @@ defineExpose({
       }
     }
 
-    tr:hover,
-    tr.hover-row {
-      .el-table__cell {
-        background: var(--zm-table-hover-bg) !important;
-      }
-    }
-
     tr.current-row {
       .el-table__cell {
         // color: var(--el-color-primary);
@@ -564,6 +578,17 @@ defineExpose({
     }
   }
 
+  &:not(.is-hover-highlight-disabled) {
+    .el-table__body {
+      tr:hover,
+      tr.hover-row {
+        .el-table__cell {
+          background: var(--zm-table-hover-bg) !important;
+        }
+      }
+    }
+  }
+
   .el-table__row {
     .el-table__cell {
       font-weight: var(--zm-table-row-font-weight);

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

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

Datei-Diff unterdrückt, da er zu groß ist
+ 285 - 273
src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue


+ 1 - 1
src/views/pms/iotrhdailyreport/components/DailyStatistics.vue

@@ -49,7 +49,7 @@ const totalWorkKeys: [string, string, string, string, number][] = [
     '%',
     '设备利用率',
     'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
+    2
   ],
   [
     'totalPowerConsumption',

+ 43 - 13
src/views/pms/iotrhdailyreport/index.vue

@@ -259,6 +259,8 @@ async function handleExport() {
 
 const unfilledDialogRef = ref()
 
+const alarmCollapse = ref<string[]>([])
+
 const openUnfilledDialog = () => {
   if (!query.value.createTime || query.value.createTime.length === 0) {
     message.warning('请先选择创建时间范围')
@@ -399,19 +401,25 @@ const openUnfilledDialog = () => {
       is-index
       @current-change="handleCurrentChange"
       @size-change="handleSizeChange" />
-    <div class="p-2 bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col gap-2">
-      <el-alert
-        class="h-8!"
-        title="运行时效=当日注气量/产能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;超过120%红色预警"
-        type="error"
-        show-icon
-        :closable="false" />
-      <el-alert
-        class="h-8!"
-        title="气电比计算结果大于15橙色预警"
-        type="warning"
-        show-icon
-        :closable="false" />
+    <div class="p-1 bg-white dark:bg-[#1d1e1f] rounded-lg shadow">
+      <el-collapse v-model="alarmCollapse" class="alarm-collapse">
+        <el-collapse-item title="告警提示" name="alarm">
+          <div class="flex flex-col gap-2">
+            <el-alert
+              class="h-8!"
+              title="运行时效=当日注气量/产能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;超过120%红色预警"
+              type="error"
+              show-icon
+              :closable="false" />
+            <el-alert
+              class="h-8!"
+              title="气电比计算结果大于15橙色预警"
+              type="warning"
+              show-icon
+              :closable="false" />
+          </div>
+        </el-collapse-item>
+      </el-collapse>
     </div>
   </div>
 
@@ -428,4 +436,26 @@ const openUnfilledDialog = () => {
     height: 42px;
   }
 }
+
+:deep(.alarm-collapse) {
+  border-top: 0;
+  border-bottom: 0;
+
+  .el-collapse-item__header {
+    height: 32px;
+    padding-left: 12px;
+    font-size: 15px;
+    font-weight: 700;
+    line-height: 32px;
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__wrap {
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__content {
+    padding-bottom: 4px;
+  }
+}
 </style>

+ 43 - 13
src/views/pms/iotrydailyreport/index.vue

@@ -152,6 +152,8 @@ const visible = ref(false)
 
 const formRef = ref()
 
+const alarmCollapse = ref<string[]>([])
+
 function handleOpenForm(id: number, type: 'edit' | 'readonly') {
   if (formRef.value) {
     formRef.value.handleOpenForm(id, type)
@@ -255,19 +257,25 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
       </template>
     </ry-table>
 
-    <div class="p-2 bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col gap-2">
-      <el-alert
-        class="h-8!"
-        title="当日油耗大于9000升&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;红色预警"
-        type="error"
-        show-icon
-        :closable="false" />
-      <el-alert
-        class="h-8!"
-        title="进尺工作时间+其它生产时间+非生产时间=24H&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;否则橙色预警"
-        type="warning"
-        show-icon
-        :closable="false" />
+    <div class="p-1 bg-white dark:bg-[#1d1e1f] rounded-lg shadow">
+      <el-collapse v-model="alarmCollapse" class="alarm-collapse">
+        <el-collapse-item title="告警提示" name="alarm">
+          <div class="flex flex-col gap-2">
+            <el-alert
+              class="h-8!"
+              title="当日油耗大于9000升&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;红色预警"
+              type="error"
+              show-icon
+              :closable="false" />
+            <el-alert
+              class="h-8!"
+              title="进尺工作时间+其它生产时间+非生产时间=24H&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;否则橙色预警"
+              type="warning"
+              show-icon
+              :closable="false" />
+          </div>
+        </el-collapse-item>
+      </el-collapse>
     </div>
   </div>
 
@@ -283,4 +291,26 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
 :deep(.el-form-item) {
   margin-bottom: 0;
 }
+
+:deep(.alarm-collapse) {
+  border-top: 0;
+  border-bottom: 0;
+
+  .el-collapse-item__header {
+    height: 32px;
+    padding-left: 12px;
+    font-size: 15px;
+    font-weight: 700;
+    line-height: 32px;
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__wrap {
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__content {
+    padding-bottom: 4px;
+  }
+}
 </style>

+ 49 - 19
src/views/pms/iotrydailyreport/xjindex.vue

@@ -151,6 +151,8 @@ const visible = ref(false)
 
 const formRef = ref()
 
+const alarmCollapse = ref<string[]>([])
+
 function handleOpenForm(id: number, type: 'edit' | 'readonly') {
   if (formRef.value) {
     formRef.value.handleOpenForm(id, type)
@@ -255,25 +257,31 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
       </template>
     </ry-xj-table>
 
-    <div class="p-2 bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col gap-2">
-      <el-alert
-        class="h-8!"
-        title="运行时效=生产时间/额定生产时间&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;超过100%红色预警"
-        type="error"
-        show-icon
-        :closable="false" />
-      <el-alert
-        class="h-8!"
-        title="生产时间+非生产时间=额定生产时间&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;否则橙色预警"
-        type="warning"
-        show-icon
-        :closable="false" />
-      <el-alert
-        class="h-8!"
-        title="当日油耗大于3500升&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;蓝色预警"
-        type="primary"
-        show-icon
-        :closable="false" />
+    <div class="p-1 bg-white dark:bg-[#1d1e1f] rounded-lg shadow">
+      <el-collapse v-model="alarmCollapse" class="alarm-collapse">
+        <el-collapse-item title="告警提示" name="alarm">
+          <div class="flex flex-col gap-2">
+            <el-alert
+              class="h-8!"
+              title="运行时效=生产时间/额定生产时间&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;超过100%红色预警"
+              type="error"
+              show-icon
+              :closable="false" />
+            <el-alert
+              class="h-8!"
+              title="生产时间+非生产时间=额定生产时间&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;否则橙色预警"
+              type="warning"
+              show-icon
+              :closable="false" />
+            <el-alert
+              class="h-8!"
+              title="当日油耗大于3500升&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;蓝色预警"
+              type="primary"
+              show-icon
+              :closable="false" />
+          </div>
+        </el-collapse-item>
+      </el-collapse>
     </div>
   </div>
 
@@ -289,4 +297,26 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
 :deep(.el-form-item) {
   margin-bottom: 0;
 }
+
+:deep(.alarm-collapse) {
+  border-top: 0;
+  border-bottom: 0;
+
+  .el-collapse-item__header {
+    height: 32px;
+    padding-left: 12px;
+    font-size: 15px;
+    font-weight: 700;
+    line-height: 32px;
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__wrap {
+    border-bottom: 0;
+  }
+
+  .el-collapse-item__content {
+    padding-bottom: 4px;
+  }
+}
 </style>

Datei-Diff unterdrückt, da er zu groß ist
+ 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'

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

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

+ 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>

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

@@ -0,0 +1,901 @@
+<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 remainValueMap = {
+  remainH,
+  remainKmometers: remainKm,
+  remainDay
+}
+type RemainColumnProp = keyof typeof remainValueMap
+
+const isNegativeValue = (value: DisplayValue) => typeof value === 'number' && value < 0
+
+const isRemainColumnProp = (property?: string): property is RemainColumnProp =>
+  !!property && property in remainValueMap
+
+const cellClassName = ({
+  row,
+  column
+}: {
+  row: List
+  column: { type?: string; property?: string }
+}) => {
+  if (column.type === 'index' && checkRowFilled(row)) return 'all-filled'
+
+  const getRemainValue = isRemainColumnProp(column.property)
+    ? remainValueMap[column.property]
+    : undefined
+  return getRemainValue && isNegativeValue(getRemainValue(row)) ? 'negative-remain-value' : ''
+}
+
+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: null,
+          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;
+}
+
+:deep(.zm-table .negative-remain-value .cell) {
+  color: var(--el-color-danger);
+}
+
+@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
src/views/pms/stat/rhkb/deviceType.vue

@@ -168,7 +168,7 @@ onUnmounted(() => {
         <span></span>
         <span></span>
       </div>
-      设备类别/状态
+      设备类别
     </div>
     <div ref="chartRef" class="flex-1 min-h-0"></div>
   </div>

+ 6 - 0
src/views/pms/stat/rykb/inventory-situation.vue

@@ -237,6 +237,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['27%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data.map((item) => ({
           name: item.project,
           value: item.mayInventoryAmount
@@ -248,6 +251,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['73%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data
           .filter((item) => item.mayBacklogAmount > 0)
           .map((item) => ({

+ 4 - 4
src/views/pms/stat/rykb/rydeviceList.vue

@@ -1,6 +1,5 @@
 <script lang="ts" setup>
 import { IotStatApi } from '@/api/pms/stat'
-import { rangeShortcuts } from '@/utils/formatTime'
 import dayjs from 'dayjs'
 
 interface RhDeviceListRow {
@@ -22,9 +21,10 @@ interface RhTeamRateRow {
 }
 
 const TEAM_TABLE_HEIGHT = 500
-const DEFAULT_TIME_RANGE = rangeShortcuts[2]
-  .value()
-  .map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+const DEFAULT_TIME_RANGE = [
+  dayjs().subtract(7, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+  dayjs().subtract(1, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss')
+]
 
 const createTime = ref<string[]>(DEFAULT_TIME_RANGE)
 const loading = ref(false)

+ 0 - 1
src/views/pms/stat/rykb/xjwork.vue

@@ -75,7 +75,6 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
         color: THEME.text.regular,
         fontSize: 14,
         fontFamily: FONT_FAMILY,
-        width: 78,
         overflow: 'break'
       }
     },

+ 5 - 5
src/views/pms/stat/rykb/zjfinish.vue

@@ -46,7 +46,7 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
     grid: {
       ...THEME.grid,
       top: 50,
-      bottom: 10
+      bottom: 0
     },
     tooltip: createTooltip({
       trigger: 'axis',
@@ -76,11 +76,11 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
       },
       axisLabel: {
         interval: 0,
+        rotate: 30,
+        margin: 6,
         color: THEME.text.regular,
-        fontSize: 14,
-        fontFamily: FONT_FAMILY,
-        width: 54,
-        overflow: 'break'
+        fontSize: 12,
+        fontFamily: FONT_FAMILY
       }
     },
     yAxis: {

+ 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,

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.