Ver Fonte

Merge branch 'master' into feature/baoyang

Zimo há 1 semana atrás
pai
commit
6297e24eab
57 ficheiros alterados com 7043 adições e 2309 exclusões
  1. 1 1
      .env.dev
  2. 1 1
      .env.local
  3. 32 33
      src/api/pms/iotmaintenancebom/index.ts
  4. 28 0
      src/api/pms/qhse/index.ts
  5. BIN
      src/assets/images/process-demo/PSA.png
  6. BIN
      src/assets/images/process-demo/厨房.png
  7. BIN
      src/assets/images/process-demo/变压器.png
  8. BIN
      src/assets/images/process-demo/地罐.png
  9. BIN
      src/assets/images/process-demo/增压机.png
  10. BIN
      src/assets/images/process-demo/房子.png
  11. BIN
      src/assets/images/process-demo/水罐.png
  12. BIN
      src/assets/images/process-demo/注水泵.png
  13. BIN
      src/assets/images/process-demo/空压机.png
  14. BIN
      src/assets/images/process-demo/空气处理撬.png
  15. BIN
      src/assets/images/process-demo/采油树.png
  16. 4 6
      src/components/ZmTable/README.md
  17. 13 16
      src/components/ZmTable/ZmTableColumn.vue
  18. 42 17
      src/components/ZmTable/index.vue
  19. 2 0
      src/components/mt-edit/components/layout/right-aside/select-item-props-setting.vue
  20. 3 1
      src/components/mt-edit/store/config.ts
  21. 2 0
      src/components/mt-edit/store/types.ts
  22. 7 2
      src/layout/components/Menu/src/components/useRenderMenuItem.tsx
  23. 11 14
      src/router/modules/remaining.ts
  24. 12 24
      src/views/pms/device/DeviceInfo.vue
  25. 178 183
      src/views/pms/device/maintenance/MaintenanceDetail.vue
  26. 285 273
      src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue
  27. 1 1
      src/views/pms/iotrhdailyreport/components/DailyStatistics.vue
  28. 43 7
      src/views/pms/iotrhdailyreport/index.vue
  29. 29 20
      src/views/pms/iotrhdailyreport/rh-table.vue
  30. 45 11
      src/views/pms/iotrydailyreport/components/equipment-form.vue
  31. 2 2
      src/views/pms/iotrydailyreport/equipment.vue
  32. 43 13
      src/views/pms/iotrydailyreport/index.vue
  33. 49 19
      src/views/pms/iotrydailyreport/xjindex.vue
  34. 311 293
      src/views/pms/maintenance/IotMaintenancePlan.vue
  35. 212 186
      src/views/pms/maintenance/IotMaintenancePlanDetail.vue
  36. 58 115
      src/views/pms/maintenance/IotMaintenancePlanEdit.vue
  37. 1605 0
      src/views/pms/maintenance/IotMaintenancePlanManage.vue
  38. 214 0
      src/views/pms/maintenance/maintenance-device-list.vue
  39. 526 0
      src/views/pms/maintenance/maintenance-plan-form.vue
  40. 901 0
      src/views/pms/maintenance/maintenance-plan-manage.vue
  41. 59 0
      src/views/pms/maintenance/types.ts
  42. 363 0
      src/views/pms/qhse/deviceCert/DeviceCertForm.vue
  43. 442 0
      src/views/pms/qhse/deviceCert/index.vue
  44. 236 197
      src/views/pms/qhse/index.vue
  45. 108 94
      src/views/pms/qhse/iotmeasuredetect/index.vue
  46. 797 0
      src/views/pms/qhse/monthlyReport/MonthlyReport.vue
  47. 113 104
      src/views/pms/qhse/monthlyReport/MonthlyReportAdd.vue
  48. 0 7
      src/views/pms/qhse/monthlyReport/MonthlyReportEdit.vue
  49. 73 605
      src/views/pms/qhse/monthlyReport/index.vue
  50. 86 19
      src/views/pms/qhse/safety/index.vue
  51. 89 33
      src/views/pms/stat/maintain.vue
  52. 1 1
      src/views/pms/stat/rhkb/deviceType.vue
  53. 6 0
      src/views/pms/stat/rykb/inventory-situation.vue
  54. 4 4
      src/views/pms/stat/rykb/rydeviceList.vue
  55. 0 1
      src/views/pms/stat/rykb/xjwork.vue
  56. 5 5
      src/views/pms/stat/rykb/zjfinish.vue
  57. 1 1
      stylelint.config.js

+ 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://192.168.188.200:48080'
+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 })
-  },
-
+  }
 }

+ 28 - 0
src/api/pms/qhse/index.ts

@@ -534,3 +534,31 @@ export const QhseMonthReportApi = {
     return await request.download({ url: `/rq/qhse-month-report/export-excel`, params })
   }
 }
+
+// 应检设备证书
+export const InspectDeviceCertApi = {
+  // 获得设备证书分页 rq/qhse-device-cert/create
+  getInspectDeviceCertList: async (params) => {
+    return await request.get({ url: `/rq/qhse-device-cert/page`, params })
+  },
+  // 删除设备证书
+  deleteInspectDeviceCert: async (id) => {
+    return await request.delete({ url: `/rq/qhse-device-cert/delete?id=` + id })
+  },
+  // 添加设备证书
+  createInspectDeviceCert: async (data) => {
+    return await request.post({ url: `/rq/qhse-device-cert/create`, data })
+  },
+  // 获取详情
+  getInspectDeviceCert: async (id) => {
+    return await request.get({ url: `/rq/qhse-device-cert/get?id=` + id })
+  },
+  // 修改设备证书
+  updateInspectDeviceCert: async (data) => {
+    return await request.put({ url: `/rq/qhse-device-cert/update`, data })
+  },
+  // 导出设备证书 Excel
+  exportInspectDeviceCert: async (params) => {
+    return await request.download({ url: `/rq/qhse-device-cert/export-excel`, params })
+  }
+}

BIN
src/assets/images/process-demo/PSA.png


BIN
src/assets/images/process-demo/厨房.png


BIN
src/assets/images/process-demo/变压器.png


BIN
src/assets/images/process-demo/地罐.png


BIN
src/assets/images/process-demo/增压机.png


BIN
src/assets/images/process-demo/房子.png


BIN
src/assets/images/process-demo/水罐.png


BIN
src/assets/images/process-demo/注水泵.png


BIN
src/assets/images/process-demo/空压机.png


BIN
src/assets/images/process-demo/空气处理撬.png


BIN
src/assets/images/process-demo/采油树.png


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

+ 2 - 0
src/components/mt-edit/components/layout/right-aside/select-item-props-setting.vue

@@ -29,6 +29,8 @@
       <el-color-picker
         v-else-if="attr_item.type === 'color' && !attr_item.disabled"
         v-model="attr_item.val"
+        :show-alpha="attr_item.showAlpha"
+        :color-format="attr_item.colorFormat"
         :disabled="attr_item?.disabled" />
       <el-switch
         v-else-if="attr_item.type === 'switch' && !attr_item.disabled"

+ 3 - 1
src/components/mt-edit/store/config.ts

@@ -299,7 +299,9 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
       backGroundColor: {
         title: '背景颜色',
         type: 'color',
-        val: '#ffffff'
+        val: '#ffffff',
+        showAlpha: true,
+        colorFormat: 'rgb'
       },
       boxShadow: {
         title: '阴影颜色',

+ 2 - 0
src/components/mt-edit/store/types.ts

@@ -15,6 +15,8 @@ export type ILeftAsideConfigItemPublicProps = Record<
     type: ILeftAsideConfigItemPublicPropsType //属性的类型决定了修改属性的方式
     val: any
     options?: any //比如说修改属性的时候用到了下拉框,这里面就可以放下拉框的选项
+    showAlpha?: boolean //颜色选择器是否支持透明度
+    colorFormat?: string //颜色选择器的输出格式
     disabled?: boolean //如果禁用了将不会显示到右侧属性面板里,但是仍然可以通过代码修改属性
   }
 >

+ 7 - 2
src/layout/components/Menu/src/components/useRenderMenuItem.tsx

@@ -77,6 +77,7 @@ export const useRenderMenuItem = () =>
                 '应急体系',
                 '突发事件应急处理',
                 '环境合规',
+                'QHSE月报管理',
                 '环境因素识别',
                 '职业健康',
                 '劳保用品',
@@ -127,10 +128,12 @@ export const useRenderMenuItem = () =>
                 '安全培训计划',
                 '安全培训档案',
 
-                '隐患排查',
+                '隐患排查治理',
                 '隐患排查分类',
                 '隐患排查记录',
-                '应急体系',
+                '应急体系管理',
+                '排查治理分类',
+                '排查治理记录',
                 '预案台账',
                 '应急物资',
                 '海外应急处置',
@@ -147,6 +150,8 @@ export const useRenderMenuItem = () =>
                 '职业健康体检',
                 '健康培训',
                 '突发事件处置',
+                'QHSE月报管理',
+                '月报填报',
                 'QHSE资料库',
                 'QHSE资质证书',
                 'QHSE体系文件',

+ 11 - 14
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,
@@ -2202,26 +2199,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: 'month_report/add',
+        path: 'month_report/add/:id',
         component: () => import('@/views/pms/qhse/monthlyReport/MonthlyReportAdd.vue'),
         name: 'MonthlyReportAdd',
         meta: {
           noCache: true,
           canto: true,
           icon: 'ep:plus',
-          title: '新增月报',
+          title: '月报填报',
           hidden: true
         }
       },
       {
-        path: 'month_report/edit/:id',
-        component: () => import('@/views/pms/qhse/monthlyReport/MonthlyReportEdit.vue'),
-        name: 'MonthlyReportEdit',
+        path: 'month_report/detail/:id',
+        component: () => import('@/views/pms/qhse/monthlyReport/MonthlyReport.vue'),
+        name: 'MonthlyReportInfo',
         meta: {
           noCache: true,
           canto: true,
-          icon: 'ep:edit',
-          title: '编辑月报',
+          icon: 'ep:view',
+          title: '月报详情',
           hidden: true
         }
       }

+ 12 - 24
src/views/pms/device/DeviceInfo.vue

@@ -7,8 +7,7 @@
           :src="defaultPicUrl"
           style="width: 35em; height: 12em"
           @click="imagePreview(defaultPicUrl)"
-          fit="contain"
-        />
+          fit="contain" />
       </div>
       <div style="flex: 2; height: 12em; margin-top: 23px">
         <el-form ref="formRef" :disabled="false" :model="formData" label-width="120px">
@@ -169,8 +168,7 @@
           ref="fileRef"
           :deviceId="id"
           :deviceName="formData.deviceName"
-          v-if="loadedTabs.includes('1')"
-        />
+          v-if="loadedTabs.includes('1')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.deviceBOM')" name="bom">
         <BomList
@@ -178,72 +176,63 @@
           v-model:activeName="activeName"
           :deviceId="id"
           :deviceCategoryName="formData.assetClassName"
-          v-if="loadedTabs.includes('2')"
-        />
+          v-if="loadedTabs.includes('2')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.operationRecords')" name="record">
         <RecordList
           ref="recordRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('3')"
-        />
+          v-if="loadedTabs.includes('3')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.faultRecords')" name="failure">
         <FailureList
           ref="failureRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('4')"
-        />
+          v-if="loadedTabs.includes('4')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.repairRecords')" name="maintain">
         <MaintainList
           ref="maintainRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('5')"
-        />
+          v-if="loadedTabs.includes('5')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.maintenanceRecords')" name="maintenance">
         <MaintenanceList
           ref="maintenanceRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('6')"
-        />
+          v-if="loadedTabs.includes('6')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.inspectionRecords')" name="inspect">
         <InspectList
           ref="inspectRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('7')"
-        />
+          v-if="loadedTabs.includes('7')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.transferRecords')" name="allot">
         <AllotLogList
           ref="allotRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('8')"
-        />
+          v-if="loadedTabs.includes('8')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.statusChangeRecords')" name="status">
         <DeviceStatusLogList
           ref="statusRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('9')"
-        />
+          v-if="loadedTabs.includes('9')" />
       </el-tab-pane>
       <el-tab-pane :label="t('deviceInfo.RPAdjustmentRecords')" name="person">
         <PersonList
           ref="personRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('10')"
-        />
+          v-if="loadedTabs.includes('10')" />
       </el-tab-pane>
 
       <!-- 关联设备 -->
@@ -252,8 +241,7 @@
           ref="personRef"
           v-model:activeName="activeName"
           :deviceId="id"
-          v-if="loadedTabs.includes('11')"
-        />
+          v-if="loadedTabs.includes('11')" />
       </el-tab-pane>
     </el-tabs>
   </ContentWrap>

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

Diff do ficheiro suprimidas por serem muito extensas
+ 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 - 7
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,13 +401,25 @@ const openUnfilledDialog = () => {
       is-index
       @current-change="handleCurrentChange"
       @size-change="handleSizeChange" />
-    <div class="p-2 bg-white dark:bg-[#1d1e1f] rounded-lg shadow">
-      <el-alert
-        class="h-8!"
-        title="运行时效=当日注气量/产能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;超过120%红色预警"
-        type="error"
-        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>
 
@@ -422,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>

+ 29 - 20
src/views/pms/iotrhdailyreport/rh-table.vue

@@ -85,6 +85,8 @@ const { list, loading, total, pageNo, pageSize, showAction, isIndex } = toRefs(p
 
 const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
 
+const GAS_ELECTRICITY_RATIO_WARNING_THRESHOLD = 15
+
 function percentageFormatter(row: ListItem) {
   const capacity = Number(row?.capacity)
   const dailyGasInjection = Number(row?.dailyGasInjection)
@@ -107,6 +109,12 @@ function unitformatter(
   return (value / 10000).toFixed(4)
 }
 
+const isGasElectricityRatioWarning = (value: unknown) => {
+  const ratio = Number(value)
+
+  return Number.isFinite(ratio) && ratio > GAS_ELECTRICITY_RATIO_WARNING_THRESHOLD
+}
+
 const cellStyle = ({ row, column }: { row: any; column: any }) => {
   if (column.property === 'transitTime') {
     const capacity = Number(row?.capacity)
@@ -122,6 +130,17 @@ const cellStyle = ({ row, column }: { row: any; column: any }) => {
       }
     }
   }
+
+  if (
+    column.property === 'gasElectricityRatio' &&
+    isGasElectricityRatioWarning(row?.gasElectricityRatio)
+  ) {
+    return {
+      color: 'var(--el-color-warning)',
+      fontWeight: 'bold'
+    }
+  }
+
   return {}
 }
 
@@ -146,22 +165,19 @@ function handleCurrentChange(val: number) {
             :max-height="height"
             :height="height"
             show-border
-            :cell-style="cellStyle"
-          >
+            :cell-style="cellStyle">
             <zm-table-column
               v-if="isIndex"
               type="index"
               :label="t('monitor.serial')"
               :width="60"
-              fixed="left"
-            />
+              fixed="left" />
             <zm-table-column
               label="日期"
               prop="createTime"
               fixed="left"
               cover-formatter
-              :real-value="(row: ListItem) => dayjs(row.createTime).format('YYYY-MM-DD')"
-            />
+              :real-value="(row: ListItem) => dayjs(row.createTime).format('YYYY-MM-DD')" />
             <zm-table-column label="施工队伍" prop="deptName" fixed="left" />
             <zm-table-column label="任务" prop="taskName" fixed="left" />
             <zm-table-column
@@ -171,13 +187,11 @@ function handleCurrentChange(val: number) {
               :real-value="
                 (row: ListItem) =>
                   realValue(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE, row.constructionStatus ?? '')
-              "
-            >
+              ">
               <template #default="scope">
                 <dict-tag
                   :type="DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE"
-                  :value="scope.row.constructionStatus ?? ''"
-                />
+                  :value="scope.row.constructionStatus ?? ''" />
               </template>
             </zm-table-column>
             <zm-table-column prop="auditStatus" label="审批状态" v-if="!isIndex">
@@ -204,15 +218,13 @@ function handleCurrentChange(val: number) {
               label="运行时效"
               prop="transitTime"
               cover-formatter
-              :real-value="percentageFormatter"
-            />
+              :real-value="percentageFormatter" />
             <zm-table-column label="当日">
               <zm-table-column
                 label="注气量(万方)"
                 prop="dailyGasInjection"
                 cover-formatter
-                :real-value="unitformatter"
-              />
+                :real-value="unitformatter" />
               <zm-table-column label="注水量(方)" prop="dailyWaterInjection" />
               <zm-table-column label="注气时间(H)" prop="dailyInjectGasTime" />
               <zm-table-column label="注水时间(H)" prop="dailyInjectWaterTime" />
@@ -224,8 +236,7 @@ function handleCurrentChange(val: number) {
               prop="nonProductionRate"
               label="非生产时效"
               cover-formatter
-              :real-value="(row) => (Number(row.nonProductionRate ?? 0) * 100).toFixed(2) + '%'"
-            />
+              :real-value="(row) => (Number(row.nonProductionRate ?? 0) * 100).toFixed(2) + '%'" />
             <zm-table-column label="非生产时间">
               <zm-table-column prop="accidentTime" label="工程质量" />
               <zm-table-column prop="repairTime" label="设备故障" />
@@ -265,8 +276,7 @@ function handleCurrentChange(val: number) {
               label="产能(万方)"
               cover-formatter
               :action="!showAction"
-              :real-value="unitformatter"
-            />
+              :real-value="unitformatter" />
 
             <zm-table-column label="操作" :width="120" fixed="right" action v-if="showAction">
               <template #default="scope">
@@ -288,8 +298,7 @@ function handleCurrentChange(val: number) {
         :total="total"
         layout="total, sizes, prev, pager, next, jumper"
         @size-change="handleSizeChange"
-        @current-change="handleCurrentChange"
-      />
+        @current-change="handleCurrentChange" />
     </div>
   </div>
 </template>

+ 45 - 11
src/views/pms/iotrydailyreport/components/equipment-form.vue

@@ -23,6 +23,7 @@ interface EquipmentReportForm {
 }
 
 interface ImproveReportDetail {
+  detailKey?: string
   projectName: string
   constructionDetail: string
   nextPlan: string
@@ -70,6 +71,8 @@ const auditStatusMap = {
   40: { label: '已取消', type: 'info' }
 } as const
 
+let detailKeySeed = 0
+
 const rules: FormRules = {
   title: [{ required: true, message: '请输入标题', trigger: ['blur', 'change'] }],
   createTime: [{ required: true, message: '请选择汇报时间', trigger: ['blur', 'change'] }],
@@ -103,12 +106,18 @@ function getAuditStatus(status?: number) {
 
 function createDefaultDetail(): ImproveReportDetail {
   return {
+    detailKey: createDetailKey(),
     projectName: '',
     constructionDetail: '',
     nextPlan: ''
   }
 }
 
+function createDetailKey() {
+  detailKeySeed += 1
+  return `detail-${Date.now()}-${detailKeySeed}`
+}
+
 function getDefaultDetails() {
   return [createDefaultDetail()]
 }
@@ -119,12 +128,32 @@ function normalizeDetails(details?: ImproveReportDetail[]) {
   }
 
   return details.map((item) => ({
+    detailKey: item.detailKey || createDetailKey(),
     projectName: item.projectName || '',
     constructionDetail: item.constructionDetail || '',
     nextPlan: item.nextPlan || ''
   }))
 }
 
+function resolveDetailIndex(row: ImproveReportDetail) {
+  const index = form.value.improveReportDetails.findIndex(
+    (item) => item.detailKey === row.detailKey
+  )
+  return index >= 0 ? index : 0
+}
+
+function getDetailFieldProp(row: ImproveReportDetail, field: keyof ImproveReportDetail) {
+  return `improveReportDetails.${resolveDetailIndex(row)}.${field}`
+}
+
+function getDetailRowKey(row: ImproveReportDetail) {
+  return row.detailKey || ''
+}
+
+function getSubmitDetails(details: ImproveReportDetail[]) {
+  return details.map(({ detailKey: _detailKey, ...item }) => item)
+}
+
 function toFormData(row?: any): EquipmentReportForm {
   if (!row) return { ...defaultForm, improveReportDetails: getDefaultDetails() }
 
@@ -173,12 +202,12 @@ function addImproveReportDetail() {
   form.value.improveReportDetails.push(createDefaultDetail())
 }
 
-function removeImproveReportDetail(index: number) {
+function removeImproveReportDetail(row: ImproveReportDetail) {
   if (form.value.improveReportDetails.length <= 1) {
     return
   }
 
-  form.value.improveReportDetails.splice(index, 1)
+  form.value.improveReportDetails.splice(resolveDetailIndex(row), 1)
 }
 
 async function submitForm() {
@@ -188,7 +217,11 @@ async function submitForm() {
     formLoading.value = true
     await formRef.value.validate()
 
-    const data = { ...form.value, createTime: Number(form.value.createTime) }
+    const data = {
+      ...form.value,
+      createTime: Number(form.value.createTime),
+      improveReportDetails: getSubmitDetails(form.value.improveReportDetails)
+    }
 
     if (data.id) {
       await IotRyImproveDailyReportApi.updateIotRyImproveDailyReport({
@@ -355,14 +388,15 @@ defineExpose({ handleOpenForm })
               :data="form.improveReportDetails"
               :loading="false"
               class="detail-table"
+              :row-key="getDetailRowKey"
               :show-border="true"
               :show-overflow-tooltip="false">
               <ZmTableColumn label="项目名称" prop="projectName" :width="220">
-                <template #default="{ row, $index }">
+                <template #default="{ row }">
                   <el-form-item
                     class="mb-0!"
                     :show-message="false"
-                    :prop="`improveReportDetails.${$index}.projectName`"
+                    :prop="getDetailFieldProp(row, 'projectName')"
                     :rules="{
                       required: true,
                       message: '请输入项目名称',
@@ -378,11 +412,11 @@ defineExpose({ handleOpenForm })
               </ZmTableColumn>
 
               <ZmTableColumn label="施工动态" min-width="280">
-                <template #default="{ row, $index }">
+                <template #default="{ row }">
                   <el-form-item
                     class="mb-0!"
                     :show-message="false"
-                    :prop="`improveReportDetails.${$index}.constructionDetail`"
+                    :prop="getDetailFieldProp(row, 'constructionDetail')"
                     :rules="{
                       required: true,
                       message: '请输入施工动态',
@@ -401,11 +435,11 @@ defineExpose({ handleOpenForm })
               </ZmTableColumn>
 
               <ZmTableColumn label="下步计划" min-width="240">
-                <template #default="{ row, $index }">
+                <template #default="{ row }">
                   <el-form-item
                     class="mb-0!"
                     :show-message="false"
-                    :prop="`improveReportDetails.${$index}.nextPlan`"
+                    :prop="getDetailFieldProp(row, 'nextPlan')"
                     :rules="{
                       required: true,
                       message: '请输入下步计划',
@@ -424,12 +458,12 @@ defineExpose({ handleOpenForm })
               </ZmTableColumn>
 
               <ZmTableColumn label="操作" width="90" fixed="right">
-                <template #default="{ $index }">
+                <template #default="{ row }">
                   <el-button
                     link
                     type="danger"
                     :icon="Delete"
-                    @click="removeImproveReportDetail($index)"
+                    @click="removeImproveReportDetail(row)"
                     :disabled="mainFieldDisabled || form.improveReportDetails.length <= 1">
                     删除
                   </el-button>

+ 2 - 2
src/views/pms/iotrydailyreport/equipment.vue

@@ -135,12 +135,12 @@ onMounted(() => {
         </el-form-item>
       </div>
       <el-form-item>
-        <el-button
+        <!-- <el-button
           type="primary"
           @click="handleOpenForm('create')"
           v-hasPermi="['pms:iot-ry-improve-daily-report:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
+        </el-button> -->
         <el-button type="primary" @click="handleQuery">
           <Icon icon="ep:search" class="mr-5px" /> 搜索
         </el-button>

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

Diff do ficheiro suprimidas por serem muito extensas
+ 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
+}

+ 363 - 0
src/views/pms/qhse/deviceCert/DeviceCertForm.vue

@@ -0,0 +1,363 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="auto"
+      v-loading="formLoading">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="关联设备" prop="deviceId">
+            <el-input
+              v-model="formData.deviceName"
+              disabled
+              placeholder="请选择关联设备"
+              style="width: 100%">
+              <template #append>
+                <el-link @click="selectDevice" :underline="false">选择</el-link>
+              </template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="证书编号" prop="certNo">
+            <el-input v-model="formData.certNo" placeholder="请输入证书编号" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检测单位" prop="certOrg">
+            <el-input v-model="formData.certOrg" placeholder="请输入检测单位" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="检测日期" prop="certTime">
+            <el-date-picker
+              v-model="formData.certTime"
+              type="date"
+              value-format="x"
+              placeholder="请选择检测日期"
+              style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="有效期至" prop="certExpire">
+            <el-date-picker
+              v-model="formData.certExpire"
+              type="date"
+              value-format="x"
+              placeholder="请选择有效期至"
+              style="width: 100%" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="部门" prop="deptId">
+            <el-tree-select
+              v-model="formData.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              node-key="id"
+              filterable
+              :check-strictly="false"
+              clearable
+              placeholder="请选择部门"
+              style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="附件" prop="file">
+            <UploadFile
+              v-model="formData.file"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :limit="3"
+              :file-size="100"
+              class="min-w-80px" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入备注" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+
+  <Dialog title="选择设备" v-model="deviceDialogVisible" width="70%">
+    <ContentWrap>
+      <el-form class="-mb-15px" :model="deviceQueryParams" :inline="true">
+        <el-form-item label="设备名称" prop="deviceName">
+          <el-input
+            v-model="deviceQueryParams.deviceName"
+            placeholder="请输入设备名称"
+            clearable
+            @keyup.enter="handleDeviceQuery"
+            class="!w-200px" />
+        </el-form-item>
+        <el-form-item label="设备编码" prop="deviceCode">
+          <el-input
+            v-model="deviceQueryParams.deviceCode"
+            placeholder="请输入设备编码"
+            clearable
+            @keyup.enter="handleDeviceQuery"
+            class="!w-200px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleDeviceQuery">
+            <Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}
+          </el-button>
+          <el-button @click="resetDeviceQuery">
+            <Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <div class="pb-10">
+      <zm-table
+        :loading="deviceLoading"
+        :data="deviceList"
+        :stripe="true"
+        :show-overflow-tooltip="true">
+        <zm-table-column width="50" align="center">
+          <template #default="scope">
+            <el-radio
+              :model-value="selectedDeviceId"
+              :label="scope.row.id"
+              @change="handleDeviceRadioChange(scope.row)">
+              &nbsp;
+            </el-radio>
+          </template>
+        </zm-table-column>
+        <zm-table-column :label="t('monitor.serial')" width="70" align="center">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </zm-table-column>
+        <zm-table-column label="设备编码" align="center" prop="deviceCode" min-width="140" />
+        <zm-table-column label="设备名称" align="center" prop="deviceName" min-width="160" />
+        <zm-table-column label="所在部门" align="center" prop="deptName" min-width="140" />
+        <zm-table-column label="设备状态" align="center" prop="deviceStatusName" min-width="120" />
+        <zm-table-column label="位置" align="center" prop="location" min-width="140" />
+        <zm-table-column label="备注" align="center" prop="remark" min-width="160" />
+      </zm-table>
+
+      <Pagination
+        :total="deviceTotal"
+        v-model:page="deviceQueryParams.pageNo"
+        v-model:limit="deviceQueryParams.pageSize"
+        @pagination="getDeviceList" />
+    </div>
+
+    <template #footer>
+      <el-button @click="confirmSelectDevice" type="primary">确 定</el-button>
+      <el-button @click="closeDeviceDialog">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { InspectDeviceCertApi } from '@/api/pms/qhse/index'
+import { IotDeviceApi } from '@/api/pms/device'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { onMounted, reactive, ref } from 'vue'
+import * as DeptApi from '@/api/system/dept'
+import { useUserStore } from '@/store/modules/user'
+defineOptions({ name: 'QHSEDeviceCertForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+const userStore = useUserStore()
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const formRef = ref()
+const deptList = ref<Tree[]>([])
+
+const createDefaultFormData = () => ({
+  id: undefined,
+  deviceId: undefined,
+  deviceName: '',
+  deptId: undefined,
+  certNo: '',
+  certOrg: '',
+  certTime: undefined,
+  certExpire: undefined,
+  file: '',
+  remark: ''
+})
+
+const formData = ref(createDefaultFormData())
+
+const formRules = reactive({
+  deviceId: [{ required: true, message: '关联设备不能为空', trigger: 'blur' }],
+  certNo: [{ required: true, message: '证书编号不能为空', trigger: 'blur' }],
+  certOrg: [{ required: true, message: '检测单位不能为空', trigger: 'blur' }],
+  certTime: [{ required: true, message: '检测日期不能为空', trigger: 'blur' }],
+  certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
+  file: [{ required: true, message: '请上传附件', trigger: 'blur' }],
+  deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }]
+})
+
+const emit = defineEmits(['success'])
+
+const resetForm = () => {
+  formData.value = createDefaultFormData()
+  formRef.value?.resetFields()
+}
+
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.deptId = userStore.getUser.deptId
+  if (id) {
+    formLoading.value = true
+    try {
+      const res = await InspectDeviceCertApi.getInspectDeviceCert(id)
+      formData.value = {
+        ...createDefaultFormData(),
+        ...res,
+        certTime: res.certTime ? Number(res.certTime) : undefined,
+        certExpire: res.certExpire ? Number(res.certExpire) : undefined
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+defineExpose({ open })
+
+const submitForm = async () => {
+  await formRef.value.validate()
+  formLoading.value = true
+  try {
+    const data = { ...formData.value } as any
+    if (data.file instanceof Array) {
+      data.file = data.file.join(',')
+    }
+
+    if (formType.value === 'create') {
+      await InspectDeviceCertApi.createInspectDeviceCert(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await InspectDeviceCertApi.updateInspectDeviceCert(data)
+      message.success(t('common.updateSuccess'))
+    }
+
+    dialogVisible.value = false
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const deviceDialogVisible = ref(false)
+const deviceLoading = ref(false)
+const deviceList = ref<any[]>([])
+const deviceTotal = ref(0)
+const selectedDeviceId = ref<number | undefined>(undefined)
+const selectedDevice = ref<any>(null)
+
+const deviceQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceName: undefined as string | undefined,
+  deviceCode: undefined as string | undefined
+})
+
+const getDeviceList = async () => {
+  deviceLoading.value = true
+  try {
+    const data = await IotDeviceApi.getIotDevicePage(deviceQueryParams)
+    deviceList.value = data.list
+    deviceTotal.value = data.total
+
+    if (selectedDeviceId.value) {
+      selectedDevice.value =
+        deviceList.value.find((item) => item.id === selectedDeviceId.value) || selectedDevice.value
+    }
+  } finally {
+    deviceLoading.value = false
+  }
+}
+
+const handleDeviceQuery = () => {
+  deviceQueryParams.pageNo = 1
+  getDeviceList()
+}
+
+const resetDeviceQuery = () => {
+  deviceQueryParams.deviceName = undefined
+  deviceQueryParams.deviceCode = undefined
+  handleDeviceQuery()
+}
+
+const handleDeviceRadioChange = (row: any) => {
+  selectedDeviceId.value = row.id
+  selectedDevice.value = row
+}
+
+const selectDevice = () => {
+  deviceDialogVisible.value = true
+  selectedDeviceId.value = formData.value.deviceId as number | undefined
+  selectedDevice.value =
+    selectedDeviceId.value && formData.value.deviceName
+      ? {
+          id: formData.value.deviceId,
+          deviceName: formData.value.deviceName,
+          deptId: formData.value.deptId
+        }
+      : null
+  getDeviceList()
+}
+
+const closeDeviceDialog = () => {
+  deviceDialogVisible.value = false
+  selectedDeviceId.value = undefined
+  selectedDevice.value = null
+}
+
+const confirmSelectDevice = () => {
+  if (!selectedDevice.value) {
+    message.warning('请先选择一个设备')
+    return
+  }
+
+  formData.value.deviceId = selectedDevice.value.id
+  formData.value.deviceName = selectedDevice.value.deviceName
+  formData.value.deptId = selectedDevice.value.deptId
+  closeDeviceDialog()
+}
+
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>

+ 442 - 0
src/views/pms/qhse/deviceCert/index.vue

@@ -0,0 +1,442 @@
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)] gap-0 gap-x-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      class="row-span-4"
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      @node-click="handleDeptNodeClick" />
+
+    <div class="mb-1">
+      <el-form
+        :model="queryParams"
+        ref="queryFormRef"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-2 pt-4 flex items-center flex-wrap min-w-0">
+        <div class="flex items-center gap-4 flex-wrap">
+          <el-form-item label="设备名称" prop="deviceName">
+            <el-input
+              v-model="queryParams.deviceName"
+              placeholder="请输入设备名称"
+              clearable
+              class="!w-150px" />
+          </el-form-item>
+          <el-form-item label="证书编号" prop="certNo">
+            <el-input
+              v-model="queryParams.certNo"
+              placeholder="请输入证书编号"
+              clearable
+              class="!w-150px" />
+          </el-form-item>
+          <el-form-item label="检测日期" prop="certTime">
+            <el-date-picker
+              v-model="queryParams.certTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-200px" />
+          </el-form-item>
+          <el-form-item label="检测单位" prop="certOrg">
+            <el-input
+              v-model="queryParams.certOrg"
+              placeholder="请输入检测单位"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-150px" />
+          </el-form-item>
+          <el-form-item label="有效期" prop="certExpire">
+            <el-date-picker
+              v-model="queryParams.certExpire"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-200px" />
+          </el-form-item>
+
+          <el-form-item>
+            <el-button @click="handleQuery">
+              <Icon icon="ep:search" class="mr-5px" /> 搜索
+            </el-button>
+            <el-button @click="resetQuery">
+              <Icon icon="ep:refresh" class="mr-5px" /> 重置
+            </el-button>
+            <el-button type="primary" plain @click="openForm('create')">
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>
+            <el-button type="success" plain @click="handleExport" :loading="exportLoading">
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+
+    <div class="min-w-0"></div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              :row-style="tableRowStyle"
+              :row-class-name="tableRowClassName">
+              <zm-table-column :label="t('monitor.serial')" width="70" align="center">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="设备名称" align="center" prop="deviceName" />
+              <zm-table-column label="证书编号" align="center" prop="certNo" />
+              <zm-table-column label="检测日期" align="center" prop="certTime" width="140">
+                <template #default="scope">
+                  <span class="iot-md-date">{{ formatDateCorrectly(scope.row.certTime) }}</span>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="检测单位" align="center" prop="certOrg" />
+              <zm-table-column label="有效期" align="center" prop="certExpire" width="140">
+                <template #default="scope">
+                  <span class="iot-md-date">{{ formatDateCorrectly(scope.row.certExpire) }}</span>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="部门名称" align="center" prop="deptName" />
+              <zm-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+              <zm-table-column label="附件" align="center" prop="file" min-width="90">
+                <template #default="scope">
+                  <el-button
+                    v-if="scope.row.file"
+                    link
+                    type="primary"
+                    @click="viewFile(scope.row.file)">
+                    查看
+                  </el-button>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="操作" align="center" width="140" fixed="right" action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="openForm('update', scope.row.id)">
+                    编辑
+                  </el-button>
+                  <el-button link type="danger" @click="handleDelete(scope.row.id)">
+                    删除
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList" />
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-3 min-w-0 mt-2">
+      <el-alert title="应检设备证书已过期红色预警" type="error" show-icon :closable="false">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+      <el-alert
+        title="应检设备证书90天橙色预警"
+        type="warning"
+        show-icon
+        :closable="false"
+        style="margin-top: 5px">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+    </div>
+  </div>
+
+  <Dialog v-model="dialogFileView" title="附件" width="500">
+    <div
+      v-for="(file, index) in fileList"
+      :key="index"
+      class="flex items-center justify-between mt-5">
+      <span class="file-name-text">{{ extractFileName(file) }}</span>
+      <div>
+        <el-button link type="primary" @click="viewFileInfo(file)">
+          <Icon icon="ep:view" class="mr-2px" />查看</el-button
+        >
+        <el-button link type="primary" @click="handleDownload(file)">
+          <Icon icon="ep:download" class="mr-2px" />下载</el-button
+        >
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer mt-10">
+        <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
+      </div>
+    </template>
+  </Dialog>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DeviceCertForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import download from '@/utils/download'
+import { InspectDeviceCertApi } from '@/api/pms/qhse/index'
+import DeviceCertForm from './DeviceCertForm.vue'
+import { formatDate } from '@/utils/formatTime'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+const { ZmTable, ZmTableColumn } = useTableComponents()
+import { useUserStore } from '@/store/modules/user'
+
+/** 应检设备证书 列表 */
+defineOptions({ name: 'QHSEDeviceCert' })
+// const userStore = useUserStore()
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceId: undefined,
+  deviceName: undefined,
+  certNo: undefined,
+  certOrg: undefined,
+  certTime: [],
+  certExpire: undefined,
+  createTime: [],
+  deptId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await InspectDeviceCertApi.getInspectDeviceCertList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const tableRowStyle = ({ row }) => {
+  if (row.expired) {
+    return { backgroundColor: '#ffe6e6' }
+  }
+  if (row.alertWarn) {
+    return { backgroundColor: '#e19f1a' }
+  }
+  return {}
+}
+
+const tableRowClassName = ({ row }) => {
+  if (row.expired) {
+    return 'expired-row'
+  }
+  if (row.alertWarn) {
+    return 'alert-warn-row'
+  }
+  return ''
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+const selectedDept = ref<{ id: number; name: string }>()
+
+const handleDeptNodeClick = async (row) => {
+  selectedDept.value = { id: row.id, name: row.name }
+  queryParams.deptId = row.id
+  await getList()
+}
+
+const formatDateCorrectly = (timestamp) => {
+  if (!timestamp) return ''
+
+  // 确保处理各种可能的时间戳格式
+  let time = Number(timestamp)
+
+  // 处理不同时间戳格式
+  if (time < 10000000000) {
+    time = time * 1000
+  }
+
+  // 检查是否为有效日期
+  const date = new Date(time)
+  if (isNaN(date.getTime())) {
+    return ''
+  }
+
+  // 验证日期合理性(例如:不能是过于久远的日期)
+  const minValidYear = 1900
+  if (date.getFullYear() < minValidYear) {
+    console.warn('Invalid date detected:', timestamp)
+    return ''
+  }
+
+  return formatDate(time).substring(0, 10)
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await InspectDeviceCertApi.deleteInspectDeviceCert(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await InspectDeviceCertApi.exportInspectDeviceCert(queryParams)
+    download.excel(data, '应检设备证书.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+let dialogFileView = ref(false)
+let fileList = ref([])
+const viewFile = (file) => {
+  fileList.value = file.split(',')
+  dialogFileView.value = true
+  // window.open(file)
+}
+
+const viewFileInfo = (file) => {
+  window.open(
+    'http://doc.deepoil.cc:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(file))
+  )
+}
+
+const extractFileName = (url: string): string => {
+  try {
+    // 移除查询参数和哈希
+    const cleanUrl = url.split('?')[0].split('#')[0]
+    // 获取最后一个斜杠后的内容
+    const parts = cleanUrl.split('/')
+    const fileName = parts[parts.length - 1]
+    // URL 解码
+    return decodeURIComponent(fileName) || url
+  } catch {
+    // 如果解析失败,返回原始 URL
+    return url
+  }
+}
+
+const handleDownload = async (url) => {
+  try {
+    const response = await fetch(url)
+    const blob = await response.blob()
+    const downloadUrl = window.URL.createObjectURL(blob)
+
+    const link = document.createElement('a')
+    link.href = downloadUrl
+    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.click()
+
+    URL.revokeObjectURL(downloadUrl)
+  } catch (error) {
+    console.error('下载失败:', error)
+  }
+}
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped>
+.file-name-text {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-right: 12px;
+  color: var(--el-text-color-primary);
+}
+
+/* 过期行的红色背景 - 基础状态 */
+:deep(.el-table__body tr.expired-row > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+/* 过期行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.expired-row:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 确保斑马纹不影响过期行 */
+:deep(.el-table__body tr.expired-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+:deep(.el-table__body tr.expired-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 预警行的橙色背景 - 基础状态 */
+:deep(.el-table__body tr.alert-warn-row > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+/* 预警行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.alert-warn-row:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
+
+/* 确保斑马纹不影响预警行 */
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
+</style>

+ 236 - 197
src/views/pms/qhse/index.vue

@@ -1,203 +1,220 @@
 <template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
+  <div
+    class="qhse-page grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3 gap-x-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      class="row-span-4"
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      @node-click="handleDeptNodeClick" />
 
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <ContentWrap style="border: none">
-        <!-- 搜索工作栏 -->
-        <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
-          <el-form-item label="计量器具名称" prop="measureName">
-            <el-input
-              v-model="queryParams.measureName"
-              placeholder="请输入计量器具名称"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-150px" />
-          </el-form-item>
-          <el-form-item label="是否过期" prop="expired">
-            <el-select
-              v-model="queryParams.expired"
-              placeholder="请选择是否过期"
-              clearable
-              style="width: 120px">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item label="是否预警" prop="alertWarn">
-            <el-select
-              v-model="queryParams.alertWarn"
-              placeholder="请选择是否预警"
-              clearable
-              style="width: 120px">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
-          </el-form-item>
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="small"
+      label-width="auto"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-4 flex items-center flex-wrap min-w-0">
+      <div class="flex items-center gap-1 flex-wrap">
+        <el-form-item label="计量器具名称" prop="measureName">
+          <el-input
+            v-model="queryParams.measureName"
+            placeholder="请输入计量器具名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-120px" />
+        </el-form-item>
+        <el-form-item label="编码" prop="measureCode">
+          <el-input
+            v-model="queryParams.measureCode"
+            placeholder="请输入计量器具编码"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-120px" />
+        </el-form-item>
+        <el-form-item label="是否过期" prop="expired">
+          <el-select
+            v-model="queryParams.expired"
+            placeholder="请选择是否过期"
+            clearable
+            class="!w-120px">
+            <el-option
+              v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="是否预警" prop="alertWarn">
+          <el-select
+            v-model="queryParams.alertWarn"
+            placeholder="请选择是否预警"
+            clearable
+            class="!w-120px">
+            <el-option
+              v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}
+          </el-button>
+          <el-button @click="handleAdd" type="primary">
+            <Icon icon="ep:plus" class="mr-5px" />新增
+          </el-button>
+          <el-button @click="handleExport" type="success" plain :loading="exportLoading">
+            <Icon icon="ep:download" class="mr-5px" /> 导出
+          </el-button>
+        </el-form-item>
+      </div>
+    </el-form>
 
-          <el-form-item>
-            <el-button @click="handleAdd" type="primary"
-              ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
-            >
-            <el-button @click="handleExport" type="success" plain :loading="exportLoading"
-              ><Icon icon="ep:download" class="mr-5px" /> 导出</el-button
-            >
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
-        <div class="stats-cards">
-          <div class="stats-card stats-card--expired">
-            <div class="stats-card__header">
-              <el-icon class="stats-card__icon" :size="28">
-                <Icon icon="ep:info-filled" />
-              </el-icon>
-              <div class="stats-card__label">已过期</div>
-            </div>
-            <div class="stats-card__value text-[40px]! pt-10 text-center!">
-              <CountTo
-                :duration="2600"
-                :end-val="expired"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!"
-            /></div>
-          </div>
-          <div class="stats-card stats-card--warn">
-            <div class="stats-card__header">
-              <el-icon class="stats-card__icon" :size="28">
-                <Icon icon="ep:bell-filled" />
-              </el-icon>
-              <div class="stats-card__label">90天预警</div>
-            </div>
-            <div class="stats-card__value text-[40px]! pt-10 text-center!">
-              <CountTo
-                :duration="2600"
-                :end-val="warn"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#df8a28]!" />
-            </div>
-          </div>
-          <div class="stats-chart-card">
-            <div class="stats-card__label">分类统计</div>
-            <div ref="staticChartRef" class="stats-chart"></div>
-          </div>
+    <div class="stats-cards min-w-0">
+      <div class="stats-card stats-card--expired">
+        <div class="stats-card__header">
+          <el-icon class="stats-card__icon" :size="28">
+            <Icon icon="ep:info-filled" />
+          </el-icon>
+          <div class="stats-card__label">已过期</div>
+        </div>
+        <div class="stats-card__value text-[40px]! pt-10 text-center!">
+          <CountTo
+            :duration="2600"
+            :end-val="expired"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
         </div>
-        <zm-table
-          :loading="loading"
-          :data="list"
-          height="calc(46vh - 130px)"
-          :show-overflow-tooltip="true"
-          :row-style="tableRowStyle"
-          :row-class-name="tableRowClassName">
-          <zm-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column label="名称" align="center" prop="measureName" fixed="left" />
-          <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
-          <zm-table-column label="部门名称" align="center" prop="deptName" />
-          <zm-table-column label="计量单位" align="center" prop="measureUnit" />
-
-          <zm-table-column label="责任人" align="center" prop="dutyPerson" />
-          <zm-table-column label="品牌" align="center" prop="brand" />
-          <zm-table-column label="规格型号" align="center" prop="modelName" />
-          <zm-table-column label="分类" align="center" prop="classify" min-width="120px">
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
-            </template>
-          </zm-table-column>
-          <zm-table-column label="采购日期" align="center" prop="buyDate">
-            <template #default="scope">
-              {{ formatDateCorrectly(scope.row.buyDate) }}
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="价格" align="center" prop="measurePrice">
-            <template #default="scope">
-              {{ scope.row.measurePrice }}
-            </template>
-          </zm-table-column>
-          <zm-table-column label="备注" align="center" prop="remark" />
-
-          <zm-table-column label="检测信息" align="center" fixed="right">
-            <template #default="scope">
-              <div>
-                <el-button link type="primary" @click="handleView(scope.row.id)"> 查看 </el-button>
-              </div>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column
-            :label="t('devicePerson.operation')"
-            align="center"
-            fixed="right"
-            action
-            min-width="120px">
-            <template #default="scope">
-              <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
-              <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
+      </div>
+      <div class="stats-card stats-card--warn">
+        <div class="stats-card__header">
+          <el-icon class="stats-card__icon" :size="28">
+            <Icon icon="ep:bell-filled" />
+          </el-icon>
+          <div class="stats-card__label">90天预警</div>
+        </div>
+        <div class="stats-card__value text-[40px]! pt-10 text-center!">
+          <CountTo
+            :duration="2600"
+            :end-val="warn"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#df8a28]!" />
+        </div>
+      </div>
+      <div class="stats-chart-card">
+        <div class="stats-card__label">分类统计</div>
+        <div ref="staticChartRef" class="stats-chart"></div>
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-3 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              :show-overflow-tooltip="true"
+              :row-style="tableRowStyle"
+              :row-class-name="tableRowClassName">
+              <zm-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="名称" align="center" prop="measureName" fixed="left" />
+              <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
+              <zm-table-column label="部门名称" align="center" prop="deptName" />
+              <zm-table-column label="计量单位" align="center" prop="measureUnit" />
+              <zm-table-column label="责任人" align="center" prop="dutyPerson" />
+              <zm-table-column label="品牌" align="center" prop="brand" />
+              <zm-table-column label="规格型号" align="center" prop="modelName" />
+              <zm-table-column label="分类" align="center" prop="classify" min-width="120px">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="采购日期" align="center" prop="buyDate">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.buyDate) }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="价格" align="center" prop="measurePrice">
+                <template #default="scope">
+                  {{ scope.row.measurePrice }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="备注" align="center" prop="remark" />
+              <zm-table-column label="检测信息" align="center" fixed="right">
+                <template #default="scope">
+                  <el-button link type="primary" @click="handleView(scope.row.id)">
+                    查看
+                  </el-button>
+                </template>
+              </zm-table-column>
+              <zm-table-column
+                :label="t('devicePerson.operation')"
+                align="center"
+                fixed="right"
+                action
+                min-width="120px">
+                <template #default="scope">
+                  <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+                  <el-button link type="danger" @click="handleDelete(scope.row.id)">
+                    删除
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="h-8 mt-2 flex items-center justify-end">
         <Pagination
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
           @pagination="getList" />
-      </ContentWrap>
-      <ContentWrap style="margin-top: -5px">
-        <el-alert title="台账已过期红色预警" type="error" show-icon :closable="false">
-          <template #icon>
-            <Bell />
-          </template>
-        </el-alert>
-
-        <el-alert
-          title="台账90天橙色预警"
-          type="warning"
-          show-icon
-          :closable="false"
-          style="margin-top: 5px">
-          <template #icon>
-            <Bell />
-          </template>
-        </el-alert>
-      </ContentWrap>
-    </el-col>
-  </el-row>
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-3 min-w-0">
+      <el-alert title="台账已过期红色预警" type="error" show-icon :closable="false">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+      <el-alert
+        title="台账90天橙色预警"
+        type="warning"
+        show-icon
+        :closable="false"
+        style="margin-top: 5px">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+    </div>
+  </div>
 
   <!-- 新增/编辑台账对话框 -->
-  <el-dialog
-    :title="dialogTitle"
-    v-model="dialogVisible"
-    width="800px"
-    destroy-on-close
-    @close="closeDialog">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" destroy-on-close @close="closeDialog">
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
-      v-loading="formLoading">
+      v-loading="formLoading"
+      label-width="120px">
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="计量器具名称" prop="measureName">
@@ -323,7 +340,7 @@
       <el-button @click="closeDialog">取 消</el-button>
       <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
     </template>
-  </el-dialog>
+  </Dialog>
 
   <el-drawer
     v-model="drawerVisible"
@@ -401,7 +418,7 @@
     </div>
   </el-drawer>
 
-  <el-dialog v-model="dialogFileView" title="附件" width="500">
+  <Dialog v-model="dialogFileView" title="附件" width="500">
     <div
       v-for="(file, index) in fileList"
       :key="index"
@@ -422,18 +439,16 @@
         <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
       </div>
     </template>
-  </el-dialog>
+  </Dialog>
 </template>
 
 <script setup lang="ts">
 import * as echarts from 'echarts'
 import { IotInstrumentApi, IotMeasureDetectApi } from '@/api/pms/qhse/index'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { handleTree } from '@/utils/tree'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 import { defaultProps } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import { ElMessageBox } from 'element-plus'
-const deptList = ref<Tree[]>([]) // 树形结构
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
 import { useUserStore } from '@/store/modules/user'
@@ -442,6 +457,8 @@ import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 
 const userStore = useUserStore()
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 
 defineOptions({ name: 'IotQHSEMeasure' })
 
@@ -450,8 +467,6 @@ const formLoading = ref(false) // 表单加载中
 const submitLoading = ref(false) // 提交按钮加载中
 let exportLoading = ref(false)
 
-const isLeftContentCollapsed = ref(false)
-
 const { t } = useI18n()
 
 type StaticItem = {
@@ -466,6 +481,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   measureName: undefined,
+  measureCertNo: undefined,
   deptId: undefined,
   expired: undefined,
   alertWarn: undefined
@@ -957,8 +973,7 @@ onMounted(async () => {
   getList()
   getStatic()
 
-  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-  deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+  deptList2.value = await DeptApi.getSimpleDeptList()
   window.addEventListener('resize', resizeStaticChart)
 })
 
@@ -976,11 +991,14 @@ onUnmounted(() => {
   height: 700px !important;
 }
 
+.qhse-page {
+  grid-template-columns: auto minmax(0, 1fr);
+}
+
 .stats-cards {
   display: grid;
-  grid-template-columns: 180px 180px minmax(0, 1fr);
+  grid-template-columns: minmax(180px, 0.7fr) minmax(180px, 0.7fr) minmax(360px, 1.6fr);
   gap: 12px;
-  margin-bottom: 16px;
   align-items: stretch;
 }
 
@@ -1025,6 +1043,27 @@ onUnmounted(() => {
   color: #d97706;
 }
 
+@media (width <= 1440px) {
+  .stats-cards {
+    grid-template-columns: minmax(180px, 0.8fr) minmax(180px, 0.8fr) minmax(300px, 1.3fr);
+  }
+}
+
+@media (max-width: 768px) {
+  .qhse-page {
+    grid-template-columns: 1fr;
+    grid-template-rows: auto auto auto minmax(420px, 1fr) auto;
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  .stats-cards {
+    grid-template-columns: 1fr;
+  }
+}
+
 .stats-chart-card {
   padding: 14px 16px 10px;
   background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);

+ 108 - 94
src/views/pms/qhse/iotmeasuredetect/index.vue

@@ -1,11 +1,22 @@
 <template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-
-    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <ContentWrap style="border: none">
-        <!-- 搜索工作栏 -->
-        <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)] gap-0 gap-x-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      class="row-span-4"
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      @node-click="handleDeptNodeClick" />
+
+    <div class="mb-1">
+      <el-form
+        :model="queryParams"
+        ref="queryFormRef"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-2 pt-4 flex items-center flex-wrap min-w-0">
+        <div class="flex items-center gap-4 flex-wrap">
           <el-form-item label="计量器具编码" prop="measureCode">
             <el-input
               v-model="queryParams.measureCode"
@@ -32,14 +43,6 @@
               class="!w-150px" />
           </el-form-item>
           <el-form-item label="检测/校准有效期" prop="validityPeriod">
-            <!-- <el-date-picker
-              v-model="queryParams.validityPeriod"
-              value-format="YYYY-MM-DD"
-              type="daterange"
-              placeholder="选择检测/校准有效期"
-              clearable
-              class="!w-150px" /> -->
-
             <el-date-picker
               v-model="queryParams.validityPeriod"
               value-format="YYYY-MM-DD HH:mm:ss"
@@ -64,76 +67,91 @@
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
-        </el-form>
-      </ContentWrap>
-
-      <!-- 列表 -->
-      <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
-        <zm-table
-          :loading="loading"
-          :data="list"
-          :stripe="true"
-          height="calc(85vh - 195px)"
-          :show-overflow-tooltip="true">
-          <zm-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-          <zm-table-column label="计量器具编码" align="center" prop="measureCode" />
-          <zm-table-column label="计量器具名称" align="center" prop="measureName" />
-          <zm-table-column label="证书编码" align="center" prop="measureCertNo" />
-          <zm-table-column label="检测/校准日期" align="center" prop="detectDate" width="140">
-            <template #default="scope">
-              <span class="iot-md-date">{{ formatDateCorrectly(scope.row.detectDate) }}</span>
-            </template>
-          </zm-table-column>
-          <zm-table-column label="检测/校准机构" align="center" prop="detectOrg" />
-          <zm-table-column label="检测/校准标准" align="center" prop="detectStandard" />
-          <zm-table-column label="检测/校准内容" align="center" prop="detectContent">
-            <template #default="scope">
-              <div class="detect-content" v-html="scope.row.detectContent"></div>
-            </template>
-          </zm-table-column>
-          <zm-table-column label="检测/校准有效期" align="center" prop="validityPeriod" width="140">
-            <template #default="scope">
-              <span class="iot-md-date">{{ formatDateCorrectly(scope.row.validityPeriod) }}</span>
-            </template>
-          </zm-table-column>
-          <zm-table-column label="校准金额" align="center" prop="detectAmount" />
-          <zm-table-column label="部门名称" align="center" prop="deptName" />
-          <zm-table-column label="附件" align="center" prop="file" min-width="90">
-            <template #default="scope">
-              <el-button
-                v-if="scope.row.file"
-                link
-                type="primary"
-                @click="viewFile(scope.row.file)">
-                查看
-              </el-button>
-            </template>
-          </zm-table-column>
-          <zm-table-column label="操作" align="center" width="140" fixed="right" action>
-            <template #default="scope">
-              <el-button link type="primary" @click="openForm('update', scope.row.id)">
-                编辑
-              </el-button>
-              <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <div class="iot-md-pagination">
-          <Pagination
-            :total="total"
-            v-model:page="queryParams.pageNo"
-            v-model:limit="queryParams.pageSize"
-            @pagination="getList" />
         </div>
-      </ContentWrap>
-    </el-col>
-  </el-row>
+      </el-form>
+    </div>
 
-  <el-dialog v-model="dialogFileView" title="附件" width="500">
+    <div class="min-w-0"></div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :max-height="height">
+              <zm-table-column :label="t('monitor.serial')" width="70" align="center">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="计量器具编码" align="center" prop="measureCode" />
+              <zm-table-column label="计量器具名称" align="center" prop="measureName" />
+              <zm-table-column label="证书编码" align="center" prop="measureCertNo" />
+              <zm-table-column label="检测/校准日期" align="center" prop="detectDate" width="140">
+                <template #default="scope">
+                  <span class="iot-md-date">{{ formatDateCorrectly(scope.row.detectDate) }}</span>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="检测/校准机构" align="center" prop="detectOrg" />
+              <zm-table-column label="检测/校准标准" align="center" prop="detectStandard" />
+              <zm-table-column label="检测/校准内容" align="center" prop="detectContent">
+                <template #default="scope">
+                  <div class="detect-content" v-html="scope.row.detectContent"></div>
+                </template>
+              </zm-table-column>
+              <zm-table-column
+                label="检测/校准有效期"
+                align="center"
+                prop="validityPeriod"
+                width="140">
+                <template #default="scope">
+                  <span class="iot-md-date">{{
+                    formatDateCorrectly(scope.row.validityPeriod)
+                  }}</span>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="校准金额" align="center" prop="detectAmount" />
+              <zm-table-column label="部门名称" align="center" prop="deptName" />
+              <zm-table-column label="附件" align="center" prop="file" min-width="90">
+                <template #default="scope">
+                  <el-button
+                    v-if="scope.row.file"
+                    link
+                    type="primary"
+                    @click="viewFile(scope.row.file)">
+                    查看
+                  </el-button>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="操作" align="center" width="140" fixed="right" action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="openForm('update', scope.row.id)">
+                    编辑
+                  </el-button>
+                  <el-button link type="danger" @click="handleDelete(scope.row.id)">
+                    删除
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList" />
+      </div>
+    </div>
+  </div>
+
+  <Dialog v-model="dialogFileView" title="附件" width="500">
     <div
       v-for="(file, index) in fileList"
       :key="index"
@@ -154,7 +172,7 @@
         <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
       </div>
     </template>
-  </el-dialog>
+  </Dialog>
 
   <!-- 表单弹窗:添加/修改 -->
   <IotMeasureDetectForm ref="formRef" @success="getList" />
@@ -165,16 +183,19 @@ import download from '@/utils/download'
 import { IotMeasureDetectApi, IotMeasureDetectVO } from '@/api/pms/qhse/index'
 import IotMeasureDetectForm from './IotMeasureDetectForm.vue'
 import { formatDate } from '@/utils/formatTime'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
+import { useUserStore } from '@/store/modules/user'
 
 /** 计量器具-检测校准明细 列表 */
 defineOptions({ name: 'IotMeasureDetect' })
+// const userStore = useUserStore()
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-const isLeftContentCollapsed = ref(false)
 
 const loading = ref(true) // 列表的加载中
 const list = ref<IotMeasureDetectVO[]>([]) // 列表的数据
@@ -338,14 +359,7 @@ onMounted(() => {
 })
 </script>
 
-<style scoped>
-::deep(.el-tree--highlight-current) {
-  height: 200px !important;
-}
-::deep(.el-transfer-panel__body) {
-  height: 700px !important;
-}
-
+<style scoped lang="scss">
 .file-name-text {
   flex: 1;
   overflow: hidden;

+ 797 - 0
src/views/pms/qhse/monthlyReport/MonthlyReport.vue

@@ -0,0 +1,797 @@
+<template>
+  <div class="monthly-report-add">
+    <el-form
+      ref="formRef"
+      :model="reportInfo"
+      :rules="formRules"
+      label-width="auto"
+      class="report-form">
+      <!-- 1. 基本信息 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:document" class="mr-5px" />
+            <span>基本信息</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+            <el-form-item label="部门名称" prop="deptId">
+              <el-tree-select
+                clearable
+                disabled
+                v-model="reportInfo.deptId"
+                :data="deptList2"
+                :props="defaultProps"
+                :check-strictly="false"
+                node-key="id"
+                filterable
+                placeholder="请选择所属队伍" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+            <el-form-item label="月报标题" prop="title">
+              <el-input disabled v-model="reportInfo.title" placeholder="请输入月报标题" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+            <el-form-item label="年月" prop="yearMonths">
+              <el-date-picker
+                disabled
+                v-model="reportInfo.yearMonths"
+                type="month"
+                placeholder="选择年月"
+                format="YYYY-MM"
+                value-format="YYYY-MM"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 2. 人工时和安全行驶公里数 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:user" class="mr-5px" />
+            <span>人工时和安全行驶公里数</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="员工人数" prop="employee">
+              <el-input-number
+                v-model="reportInfo.employee"
+                :min="0"
+                disabled
+                :precision="0"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="分包商人数" prop="subcontractors">
+              <el-input-number
+                v-model="reportInfo.subcontractors"
+                :min="0"
+                disabled
+                :precision="0"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="安全行驶里程数(公里)" prop="drivingMileage">
+              <el-input-number
+                v-model="reportInfo.drivingMileage"
+                :min="0"
+                disabled
+                :precision="2"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="总人工时数(小时)" prop="totalManHours">
+              <el-input-number
+                v-model="reportInfo.totalManHours"
+                :min="0"
+                disabled
+                :precision="2"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 3. QHSE被动性指标统计 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:warning" class="mr-5px" />
+            <span>QHSE被动性指标统计</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="无事故累计天数" prop="withoutAccident">
+              <el-input-number
+                v-model="reportInfo.withoutAccident"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="死亡事故(起)" prop="fatality">
+              <el-input-number
+                v-model="reportInfo.fatality"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="损失工时事故(起)" prop="injury">
+              <el-input-number
+                v-model="reportInfo.injury"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="工作受限事件(起)" prop="restrictedCase">
+              <el-input-number
+                v-model="reportInfo.restrictedCase"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="医疗处理事件(起)" prop="medicalCase">
+              <el-input-number
+                v-model="reportInfo.medicalCase"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="急救箱事件(起)" prop="firstAidCase">
+              <el-input-number
+                v-model="reportInfo.firstAidCase"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="交通事故(起)" prop="vehicleAccident">
+              <el-input-number
+                v-model="reportInfo.vehicleAccident"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="未遂事件(起)" prop="nearMiss">
+              <el-input-number
+                v-model="reportInfo.nearMiss"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="泄漏事件(起)" prop="spill">
+              <el-input-number
+                v-model="reportInfo.spill"
+                :min="0"
+                disabled
+                :precision="0"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="违反保命规则的次数(次)" prop="lifeSavingRules">
+              <el-input-number
+                v-model="reportInfo.lifeSavingRules"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 4. QHSE主动性指标统计 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:checked" class="mr-5px" />
+            <span>QHSE主动性指标统计</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="班前会(次)" prop="toolboxTalk">
+              <el-input-number
+                v-model="reportInfo.toolboxTalk"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE管理委员会会议(次)" prop="committeeMeeting">
+              <el-input-number
+                v-model="reportInfo.committeeMeeting"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE月度例会(次)" prop="monthlyMeeting">
+              <el-input-number
+                v-model="reportInfo.monthlyMeeting"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="公司级隐患排查(次)" prop="companyHazard">
+              <el-input-number
+                v-model="reportInfo.companyHazard"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE检查(次)" prop="qhseInspection">
+              <el-input-number
+                v-model="reportInfo.qhseInspection"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="安全观察卡(张)" prop="socCards">
+              <el-input-number
+                v-model="reportInfo.socCards"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="工作许可审核(份)" prop="ptwAudit">
+              <el-input-number
+                v-model="reportInfo.ptwAudit"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="工作安全分析(次)" prop="jsa">
+              <el-input-number
+                v-model="reportInfo.jsa"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="演练次数" prop="drills">
+              <el-input-number
+                v-model="reportInfo.drills"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE培训次数" prop="training">
+              <el-input-number
+                v-model="reportInfo.training"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE培训人次" prop="participantsTraining">
+              <el-input-number
+                v-model="reportInfo.participantsTraining"
+                :min="0"
+                :precision="0"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="QHSE培训学时数(小时)" prop="trainingsHours">
+              <el-input-number
+                v-model="reportInfo.trainingsHours"
+                :min="0"
+                :precision="2"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 5. 环境数据 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:sunrise" class="mr-5px" />
+            <span>环境数据</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="水消耗(吨)" prop="waterConsumption">
+              <el-input-number
+                v-model="reportInfo.waterConsumption"
+                :min="0"
+                :precision="2"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="柴油消耗(升)" prop="dieselConsumption">
+              <el-input-number
+                v-model="reportInfo.dieselConsumption"
+                :min="0"
+                :precision="2"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="用电量(千瓦·小时)" prop="electricityConsumption">
+              <el-input-number
+                v-model="reportInfo.electricityConsumption"
+                :min="0"
+                :precision="2"
+                disabled
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
+            <el-form-item label="天然气消耗量(立方米)" prop="naturalGasConsumption">
+              <el-input-number
+                v-model="reportInfo.naturalGasConsumption"
+                :min="0"
+                disabled
+                :precision="2"
+                controls-position="right"
+                style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 6. 其他信息 -->
+      <el-card class="form-section" shadow="hover">
+        <template #header>
+          <div class="section-header">
+            <Icon icon="ep:info-filled" class="mr-5px" />
+            <span>其他信息</span>
+          </div>
+        </template>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+            <el-form-item label="工单填报人" prop="dutyPerson">
+              <el-select
+                v-model="reportInfo.dutyPerson"
+                filterable
+                clearable
+                disabled
+                placeholder="请选择填报人"
+                style="width: 100%">
+                <el-option
+                  v-for="item in userList"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input
+                v-model="reportInfo.remark"
+                type="textarea"
+                disabled
+                :rows="4"
+                placeholder="请输入备注信息" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 操作按钮 -->
+      <div class="form-actions">
+        <el-button @click="handleCancel">返 回</el-button>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { FormInstance, FormRules } from 'element-plus'
+import { QhseMonthReportApi } from '@/api/pms/qhse'
+import { handleTree, defaultProps } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { getUserProfile } from '@/api/system/user/profile'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+const { delView } = useTagsViewStore()
+
+defineOptions({ name: 'MonthlyReportAdd' })
+
+const router = useRouter()
+const route = useRoute()
+const message = useMessage()
+const deptList2 = ref<Tree[]>([]) // 树形结构
+const userList = ref<any[]>([])
+
+// 表单引用
+const formRef = ref<FormInstance>()
+const submitLoading = ref(false)
+
+// 表单数据
+let reportInfo = reactive({
+  title: '',
+  yearMonths: '',
+  deptId: 0,
+  employee: '0',
+  subcontractors: '0',
+  drivingMileage: '0',
+  totalManHours: '0',
+  withoutAccident: '0',
+  fatality: '0',
+  injury: '0',
+  restrictedCase: '0',
+  medicalCase: '0',
+  firstAidCase: '0',
+  vehicleAccident: '0',
+  nearMiss: '0',
+  spill: '0',
+  lifeSavingRules: '0',
+  toolboxTalk: '0',
+  committeeMeeting: '0',
+  monthlyMeeting: '0',
+  companyHazard: '0',
+  qhseInspection: '0',
+  socCards: '0',
+  ptwAudit: '0',
+  jsa: '0',
+  drills: '0',
+  training: '0',
+  participantsTraining: '0',
+  trainingsHours: '0',
+  waterConsumption: '0',
+  dieselConsumption: '0',
+  electricityConsumption: '0',
+  naturalGasConsumption: '0',
+  dutyPerson: 0,
+  remark: ''
+})
+
+// 表单校验规则
+const formRules = reactive<FormRules>({
+  title: [{ required: true, message: '月报标题不能为空', trigger: 'blur' }],
+  yearMonth: [{ required: true, message: '年月不能为空', trigger: 'change' }],
+  deptId: [{ required: true, message: '部门不能为空', trigger: 'change' }],
+  dutyPerson: [{ required: true, message: '工单填报人不能为空', trigger: 'change' }]
+})
+
+// 部门名称显示
+const deptName = ref('')
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  if (!formRef.value) return
+
+  // 校验表单
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  submitLoading.value = true
+  try {
+    await QhseMonthReportApi.createQhseMonthReport(reportInfo)
+    message.success('新增成功')
+    // 返回列表页或上一页
+    router.back()
+  } catch (error) {
+    console.error('提交失败:', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+/** 取消 */
+const handleCancel = () => {
+  delView(unref(router.currentRoute))
+  router.push({ name: 'QhseMonthlyReport', params: {} })
+}
+
+const userInfo = ref<any>({})
+onMounted(async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+  deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+  userList.value = await UserApi.selectedDeptsEmployee({
+    deptIds: userInfo.value.dept.id
+  })
+  const res = await QhseMonthReportApi.getQhseMonthReport(route.params.id)
+  const data = (res as any)?.data ?? res ?? {}
+
+  Object.assign(reportInfo, data)
+})
+</script>
+<style scoped lang="scss">
+.monthly-report-add {
+  padding: 12px;
+  min-height: 100vh;
+  background-color: #f5f7fa;
+
+  .report-form {
+    max-width: 1400px;
+    margin: 0 auto;
+
+    .form-section {
+      margin-bottom: 16px;
+      border-radius: 8px;
+
+      :deep(.el-card__header) {
+        padding: 12px 16px;
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        color: white;
+        border-radius: 8px 8px 0 0;
+      }
+
+      :deep(.el-card__body) {
+        padding: 16px;
+      }
+
+      .section-header {
+        display: flex;
+        align-items: center;
+        font-size: 15px;
+        font-weight: 600;
+      }
+
+      :deep(.el-form-item) {
+        margin-bottom: 16px;
+      }
+
+      :deep(.el-form-item__label) {
+        font-size: 14px;
+        line-height: 1.5;
+      }
+
+      :deep(.el-input),
+      :deep(.el-input-number),
+      :deep(.el-date-editor) {
+        width: 100%;
+      }
+
+      :deep(.el-input-number) {
+        .el-input__inner {
+          text-align: left;
+        }
+      }
+    }
+
+    .form-actions {
+      display: flex;
+      justify-content: center;
+      gap: 12px;
+      padding: 16px 0;
+      margin-top: 16px;
+      background: white;
+      border-radius: 8px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+      position: sticky;
+      bottom: 0;
+      z-index: 10;
+
+      .el-button {
+        min-width: 100px;
+        flex: 1;
+        max-width: 150px;
+        height: 44px;
+        font-size: 15px;
+      }
+    }
+  }
+}
+
+// 移动端优化
+@media screen and (max-width: 768px) {
+  .monthly-report-add {
+    padding: 8px;
+
+    .report-form {
+      .form-section {
+        margin-bottom: 12px;
+
+        :deep(.el-card__header) {
+          padding: 10px 12px;
+        }
+
+        :deep(.el-card__body) {
+          padding: 12px;
+        }
+
+        .section-header {
+          font-size: 14px;
+        }
+
+        :deep(.el-form-item__label) {
+          font-size: 13px;
+          padding-right: 8px;
+        }
+
+        :deep(.el-input__inner),
+        :deep(.el-textarea__inner) {
+          font-size: 14px;
+          min-height: 40px;
+        }
+
+        :deep(.el-input-number) {
+          .el-input-number__decrease,
+          .el-input-number__increase {
+            width: 32px;
+            height: 40px;
+            line-height: 40px;
+          }
+
+          .el-input__inner {
+            height: 40px;
+            line-height: 40px;
+          }
+        }
+
+        :deep(.el-date-editor) {
+          .el-input__inner {
+            height: 40px;
+            line-height: 40px;
+          }
+        }
+      }
+
+      .form-actions {
+        padding: 12px;
+        gap: 10px;
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        margin: 0;
+        border-radius: 0;
+        box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
+
+        .el-button {
+          height: 44px;
+          font-size: 15px;
+        }
+      }
+    }
+  }
+}
+
+// 超小屏幕优化
+@media screen and (max-width: 480px) {
+  .monthly-report-add {
+    padding: 6px;
+
+    .report-form {
+      .form-section {
+        :deep(.el-card__body) {
+          padding: 10px;
+        }
+
+        :deep(.el-form-item) {
+          margin-bottom: 12px;
+        }
+
+        :deep(.el-form-item__label) {
+          font-size: 12px;
+        }
+
+        :deep(.el-input__inner),
+        :deep(.el-textarea__inner) {
+          font-size: 13px;
+        }
+      }
+    }
+  }
+}
+
+:deep(.el-input.is-disabled) {
+  .el-input__inner {
+    color: #606266 !important;
+    -webkit-text-fill-color: #606266 !important;
+  }
+}
+
+:deep(.el-textarea.is-disabled) {
+  .el-textarea__inner {
+    color: #606266 !important;
+    -webkit-text-fill-color: #606266 !important;
+  }
+}
+</style>

+ 113 - 104
src/views/pms/qhse/monthlyReport/MonthlyReportAdd.vue

@@ -2,7 +2,7 @@
   <div class="monthly-report-add">
     <el-form
       ref="formRef"
-      :model="formData"
+      :model="reportInfo"
       :rules="formRules"
       label-width="auto"
       class="report-form">
@@ -17,28 +17,28 @@
         <el-row :gutter="16">
           <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
             <el-form-item label="部门名称" prop="deptId">
-              <el-input
-                v-model="deptName"
-                placeholder="请选择部门"
-                readonly
-                @click="openDeptSelect">
-                <template #suffix>
-                  <Icon icon="ep:search" class="cursor-pointer" />
-                </template>
-              </el-input>
+              <el-tree-select
+                clearable
+                v-model="reportInfo.deptId"
+                :data="deptList2"
+                :props="defaultProps"
+                :check-strictly="false"
+                node-key="id"
+                filterable
+                placeholder="请选择所属队伍" />
             </el-form-item>
           </el-col>
           <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
             <el-form-item label="月报标题" prop="title">
-              <el-input v-model="formData.title" placeholder="请输入月报标题" />
+              <el-input v-model="reportInfo.title" placeholder="请输入月报标题" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="16">
           <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-            <el-form-item label="年月" prop="yearMonth">
+            <el-form-item label="年月" prop="yearMonths">
               <el-date-picker
-                v-model="formData.yearMonth"
+                v-model="reportInfo.yearMonths"
                 type="month"
                 placeholder="选择年月"
                 format="YYYY-MM"
@@ -61,7 +61,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="员工人数" prop="employee">
               <el-input-number
-                v-model="formData.employee"
+                v-model="reportInfo.employee"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -71,7 +71,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="分包商人数" prop="subcontractors">
               <el-input-number
-                v-model="formData.subcontractors"
+                v-model="reportInfo.subcontractors"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -81,7 +81,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="安全行驶里程数(公里)" prop="drivingMileage">
               <el-input-number
-                v-model="formData.drivingMileage"
+                v-model="reportInfo.drivingMileage"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -91,7 +91,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="总人工时数(小时)" prop="totalManHours">
               <el-input-number
-                v-model="formData.totalManHours"
+                v-model="reportInfo.totalManHours"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -113,7 +113,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="无事故累计天数" prop="withoutAccident">
               <el-input-number
-                v-model="formData.withoutAccident"
+                v-model="reportInfo.withoutAccident"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -123,7 +123,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="死亡事故(起)" prop="fatality">
               <el-input-number
-                v-model="formData.fatality"
+                v-model="reportInfo.fatality"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -133,7 +133,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="损失工时事故(起)" prop="injury">
               <el-input-number
-                v-model="formData.injury"
+                v-model="reportInfo.injury"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -143,7 +143,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="工作受限事件(起)" prop="restrictedCase">
               <el-input-number
-                v-model="formData.restrictedCase"
+                v-model="reportInfo.restrictedCase"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -155,7 +155,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="医疗处理事件(起)" prop="medicalCase">
               <el-input-number
-                v-model="formData.medicalCase"
+                v-model="reportInfo.medicalCase"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -165,7 +165,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="急救箱事件(起)" prop="firstAidCase">
               <el-input-number
-                v-model="formData.firstAidCase"
+                v-model="reportInfo.firstAidCase"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -175,7 +175,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="交通事故(起)" prop="vehicleAccident">
               <el-input-number
-                v-model="formData.vehicleAccident"
+                v-model="reportInfo.vehicleAccident"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -185,7 +185,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="未遂事件(起)" prop="nearMiss">
               <el-input-number
-                v-model="formData.nearMiss"
+                v-model="reportInfo.nearMiss"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -197,7 +197,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="泄漏事件(起)" prop="spill">
               <el-input-number
-                v-model="formData.spill"
+                v-model="reportInfo.spill"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -207,7 +207,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="违反保命规则的次数(次)" prop="lifeSavingRules">
               <el-input-number
-                v-model="formData.lifeSavingRules"
+                v-model="reportInfo.lifeSavingRules"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -229,7 +229,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="班前会(次)" prop="toolboxTalk">
               <el-input-number
-                v-model="formData.toolboxTalk"
+                v-model="reportInfo.toolboxTalk"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -239,7 +239,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE管理委员会会议(次)" prop="committeeMeeting">
               <el-input-number
-                v-model="formData.committeeMeeting"
+                v-model="reportInfo.committeeMeeting"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -249,7 +249,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE月度例会(次)" prop="monthlyMeeting">
               <el-input-number
-                v-model="formData.monthlyMeeting"
+                v-model="reportInfo.monthlyMeeting"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -259,7 +259,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="公司级隐患排查(次)" prop="companyHazard">
               <el-input-number
-                v-model="formData.companyHazard"
+                v-model="reportInfo.companyHazard"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -271,7 +271,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE检查(次)" prop="qhseInspection">
               <el-input-number
-                v-model="formData.qhseInspection"
+                v-model="reportInfo.qhseInspection"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -281,19 +281,21 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="安全观察卡(张)" prop="socCards">
               <el-input-number
-                v-model="formData.socCards"
+                v-model="reportInfo.socCards"
                 :min="0"
                 :precision="0"
                 controls-position="right"
+                disabled
                 style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="工作许可审核(份)" prop="ptwAudit">
               <el-input-number
-                v-model="formData.ptwAudit"
+                v-model="reportInfo.ptwAudit"
                 :min="0"
                 :precision="0"
+                disabled
                 controls-position="right"
                 style="width: 100%" />
             </el-form-item>
@@ -301,9 +303,10 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="工作安全分析(次)" prop="jsa">
               <el-input-number
-                v-model="formData.jsa"
+                v-model="reportInfo.jsa"
                 :min="0"
                 :precision="0"
+                disabled
                 controls-position="right"
                 style="width: 100%" />
             </el-form-item>
@@ -313,7 +316,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="演练次数" prop="drills">
               <el-input-number
-                v-model="formData.drills"
+                v-model="reportInfo.drills"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -323,7 +326,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE培训次数" prop="training">
               <el-input-number
-                v-model="formData.training"
+                v-model="reportInfo.training"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -333,7 +336,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE培训人次" prop="participantsTraining">
               <el-input-number
-                v-model="formData.participantsTraining"
+                v-model="reportInfo.participantsTraining"
                 :min="0"
                 :precision="0"
                 controls-position="right"
@@ -343,7 +346,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="QHSE培训学时数(小时)" prop="trainingsHours">
               <el-input-number
-                v-model="formData.trainingsHours"
+                v-model="reportInfo.trainingsHours"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -365,7 +368,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="水消耗(吨)" prop="waterConsumption">
               <el-input-number
-                v-model="formData.waterConsumption"
+                v-model="reportInfo.waterConsumption"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -375,7 +378,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="柴油消耗(升)" prop="dieselConsumption">
               <el-input-number
-                v-model="formData.dieselConsumption"
+                v-model="reportInfo.dieselConsumption"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -385,7 +388,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="用电量(千瓦·小时)" prop="electricityConsumption">
               <el-input-number
-                v-model="formData.electricityConsumption"
+                v-model="reportInfo.electricityConsumption"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -395,7 +398,7 @@
           <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
             <el-form-item label="天然气消耗量(立方米)" prop="naturalGasConsumption">
               <el-input-number
-                v-model="formData.naturalGasConsumption"
+                v-model="reportInfo.naturalGasConsumption"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -416,15 +419,18 @@
         <el-row :gutter="16">
           <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
             <el-form-item label="工单填报人" prop="dutyPerson">
-              <el-input
-                v-model="dutyPersonName"
+              <el-select
+                v-model="reportInfo.dutyPerson"
+                filterable
+                clearable
                 placeholder="请选择填报人"
-                readonly
-                @click="openUserSelect">
-                <template #suffix>
-                  <Icon icon="ep:search" class="cursor-pointer" />
-                </template>
-              </el-input>
+                style="width: 100%">
+                <el-option
+                  v-for="item in userList"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id" />
+              </el-select>
             </el-form-item>
           </el-col>
         </el-row>
@@ -432,7 +438,7 @@
           <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
             <el-form-item label="备注" prop="remark">
               <el-input
-                v-model="formData.remark"
+                v-model="reportInfo.remark"
                 type="textarea"
                 :rows="4"
                 placeholder="请输入备注信息" />
@@ -447,39 +453,37 @@
         <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> 提交 </el-button>
       </div>
     </el-form>
-
-    <!-- 部门选择弹窗 -->
-    <DeptSelectForm ref="deptSelectFormRef" :multiple="false" @confirm="handleDeptConfirm" />
-
-    <!-- 用户选择弹窗 -->
-    <UserSelectForm ref="userSelectFormRef" @confirm="handleUserConfirm" />
   </div>
 </template>
 
 <script setup lang="ts">
 import { reactive, ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import { FormInstance, FormRules } from 'element-plus'
 import { QhseMonthReportApi } from '@/api/pms/qhse'
-// import DeptSelectForm from '@/components/DeptSelectForm/qhseDept.vue'
-import UserSelectForm from '@/components/UserSelectForm/index.vue'
 import { handleTree, defaultProps } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
-const deptList2 = ref<Tree[]>([]) // 树形结构
+import * as UserApi from '@/api/system/user'
+import { getUserProfile } from '@/api/system/user/profile'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+const { delView } = useTagsViewStore()
 
 defineOptions({ name: 'MonthlyReportAdd' })
 
 const router = useRouter()
+const route = useRoute()
 const message = useMessage()
+const deptList2 = ref<Tree[]>([]) // 树形结构
+const userList = ref<any[]>([])
 
 // 表单引用
 const formRef = ref<FormInstance>()
 const submitLoading = ref(false)
 
 // 表单数据
-const formData = reactive({
+let reportInfo = reactive({
   title: '',
-  yearMonth: '',
+  yearMonths: '',
   deptId: 0,
   employee: '0',
   subcontractors: '0',
@@ -520,49 +524,43 @@ const formRules = reactive<FormRules>({
   title: [{ required: true, message: '月报标题不能为空', trigger: 'blur' }],
   yearMonth: [{ required: true, message: '年月不能为空', trigger: 'change' }],
   deptId: [{ required: true, message: '部门不能为空', trigger: 'change' }],
-  dutyPerson: [{ required: true, message: '工单填报人不能为空', trigger: 'change' }]
+  dutyPerson: [{ required: true, message: '工单填报人不能为空', trigger: 'change' }],
+  employee: [{ required: true, message: '员工数不能为空', trigger: 'blur' }],
+  subcontractors: [{ required: true, message: '分包商人数不能为空', trigger: 'blur' }],
+  drivingMileage: [{ required: true, message: '行驶里程不能为空', trigger: 'blur' }],
+  totalManHours: [{ required: true, message: '总工时不能为空', trigger: 'blur' }],
+  withoutAccident: [{ required: true, message: '无事故累计天数不能为空', trigger: 'blur' }],
+  fatality: [{ required: true, message: '死亡不能为空', trigger: 'blur' }],
+  injury: [{ required: true, message: '损失工时事故不能为空', trigger: 'blur' }],
+  restrictedCase: [{ required: true, message: '受限事件不能为空', trigger: 'blur' }],
+  medicalCase: [{ required: true, message: '医疗事件不能为空', trigger: 'blur' }],
+  firstAidCase: [{ required: true, message: '急救事件不能为空', trigger: 'blur' }],
+  vehicleAccident: [{ required: true, message: '车辆事故不能为空', trigger: 'blur' }],
+  nearMiss: [{ required: true, message: '未遂事件次数不能为空', trigger: 'blur' }],
+  spill: [{ required: true, message: '泄漏事件次数不能为空', trigger: 'blur' }],
+  lifeSavingRules: [{ required: true, message: '违反保命规则的次数不能为空', trigger: 'blur' }],
+  toolboxTalk: [{ required: true, message: '班前会次不能为空', trigger: 'blur' }],
+  committeeMeeting: [{ required: true, message: '委员会会议不能为空', trigger: 'blur' }],
+  monthlyMeeting: [{ required: true, message: '月度会议不能为空', trigger: 'blur' }],
+  companyHazard: [{ required: true, message: '隐患排查次数不能为空', trigger: 'blur' }],
+  qhseInspection: [{ required: true, message: '请填写QHSE检查次数', trigger: 'blur' }],
+  socCards: [{ required: true, message: 'SOC 卡片不能为空', trigger: 'blur' }],
+  ptwAudit: [{ required: true, message: 'PTW 审核不能为空', trigger: 'blur' }],
+  jsa: [{ required: true, message: 'JSA 不能为空', trigger: 'blur' }],
+  drills: [{ required: true, message: '演练次数不能为空', trigger: 'blur' }],
+  training: [{ required: true, message: '培训次数不能为空', trigger: 'blur' }],
+  participantsTraining: [{ required: true, message: 'QHSE培训人次不能为空', trigger: 'blur' }],
+  waterConsumption: [{ required: true, message: '水耗不能为空', trigger: 'blur' }],
+  dieselConsumption: [{ required: true, message: '柴油耗能不能为空', trigger: 'blur' }],
+  electricityConsumption: [{ required: true, message: '电耗不能为空', trigger: 'blur' }],
+  naturalGasConsumption: [{ required: true, message: '天然气耗不能为空', trigger: 'blur' }],
+  trainingsHours: [{ required: true, message: '培训课时不能为空', trigger: 'blur' }],
+  yearMonths: [{ required: true, message: '年月不能为空', trigger: 'change' }]
 })
 
 // 部门名称显示
 const deptName = ref('')
 
-// 填报人名称显示
-const dutyPersonName = ref('')
-
-// 部门选择弹窗引用
-const deptSelectFormRef = ref()
-
-// 用户选择弹窗引用
-const userSelectFormRef = ref()
-
-/** 打开部门选择 */
-const openDeptSelect = () => {
-  deptSelectFormRef.value?.open()
-}
-
-/** 部门选择确认 */
-const handleDeptConfirm = async (deptList: any[]) => {
-  if (deptList && deptList.length > 0) {
-    const dept = deptList[0]
-    formData.deptId = dept.id
-    deptName.value = dept.name
-  }
-}
-
-/** 打开用户选择 */
-const openUserSelect = () => {
-  userSelectFormRef.value?.open()
-}
-
-/** 用户选择确认 */
-const handleUserConfirm = (userId: any, userList: any[]) => {
-  if (userList && userList.length > 0) {
-    const user = userList[0]
-    formData.dutyPerson = user.id
-    dutyPersonName.value = user.nickname
-  }
-}
-
 /** 提交表单 */
 const handleSubmit = async () => {
   if (!formRef.value) return
@@ -573,7 +571,7 @@ const handleSubmit = async () => {
 
   submitLoading.value = true
   try {
-    await QhseMonthReportApi.createQhseMonthReport(formData)
+    await QhseMonthReportApi.updateQhseMonthReport(reportInfo)
     message.success('新增成功')
     // 返回列表页或上一页
     router.back()
@@ -586,11 +584,22 @@ const handleSubmit = async () => {
 
 /** 取消 */
 const handleCancel = () => {
-  router.back()
+  delView(unref(router.currentRoute))
+  router.push({ name: 'QhseMonthlyReport', params: {} })
 }
 
+const userInfo = ref<any>({})
 onMounted(async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+  userList.value = await UserApi.selectedDeptsEmployee({
+    deptIds: userInfo.value.dept.id
+  })
+  const res = await QhseMonthReportApi.getQhseMonthReport(route.params.id)
+  const data = (res as any)?.data ?? res ?? {}
+
+  Object.assign(reportInfo, data)
 })
 </script>
 <style scoped lang="scss">

+ 0 - 7
src/views/pms/qhse/monthlyReport/MonthlyReportEdit.vue

@@ -1,7 +0,0 @@
-<template>
-  <div>addddd</div>
-</template>
-
-<script setup lang="ts">
-defineOptions({ name: 'MonthlyReportEdit' })
-</script>

+ 73 - 605
src/views/pms/qhse/monthlyReport/index.vue

@@ -5,30 +5,31 @@
       <ContentWrap style="border: 0">
         <!-- 搜索工作栏 -->
         <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
-          <el-form-item label="风险等级" prop="riskGrade">
-            <el-select
-              v-model="queryParams.riskGrade"
-              placeholder="请选择风险等级"
-              clearable
-              style="width: 200px">
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
+          <el-form-item label="月报标题" prop="title">
+            <el-input v-model="queryParams.title" placeholder="请输入月报标题" />
+          </el-form-item>
+
+          <el-form-item label="创建日期" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-200px" />
           </el-form-item>
 
           <el-form-item>
-            <el-button @click="handleQuery"
+            <el-button @click="handleQuery" v-hasPermi="['rq:qhse-month-report:query']"
               ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
             >
             <el-button @click="resetQuery"
               ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
             >
-            <el-button type="primary" @click="add" color="#626aef">
+            <!-- <el-button type="primary" @click="add" color="#626aef">
               <Icon icon="ep:plus" class="mr-5px" /> 新增
-            </el-button>
+            </el-button> -->
             <el-button type="success" plain @click="handleExport" :loading="exportLoading">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
@@ -37,98 +38,31 @@
       </ContentWrap>
 
       <ContentWrap style="border: 0">
-        <div v-loading="!staticData.length" class="stats-cards">
-          <div
-            v-for="item in statsDisplayCards"
-            :key="item.key"
-            class="stats-card"
-            :style="getStatsCardStyle(item.accent, item.glow)">
-            <div
-              class="stats-card__decor stats-card__decor--left"
-              :style="{ background: item.glow }"></div>
-            <div
-              class="stats-card__decor stats-card__decor--right"
-              :style="{ background: item.glow }"></div>
-            <div class="stats-card__header">
-              <div class="stats-card__icon-wrap">
-                <div class="stats-card__icon" :style="{ color: item.accent }">
-                  <Icon :icon="item.icon" />
-                </div>
-              </div>
-              <div class="stats-card__title">{{ item.label }}</div>
-            </div>
-            <div class="stats-card__body">
-              <CountTo
-                :duration="2600"
-                :end-val="item.count"
-                :start-val="0"
-                class="stats-card__count"
-                :style="{ color: item.accent }" />
-            </div>
-          </div>
-        </div>
         <zm-table
           :data="tableData"
           border
           style="width: 100%"
           :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
           :cell-style="{ padding: '12px 8px' }"
-          height="52.7vh">
-          <!-- 区域/位置 列(已合并) -->
-          <zm-table-column prop="region" label="区域/位置" align="center" fixed="left" />
-
-          <!-- 其他列保持不变 -->
-          <zm-table-column label="序号" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
+          height="70.8vh">
+          <zm-table-column
+            prop="title"
+            label="月报标题"
+            align="center"
+            fixed="left"
+            show-overflow-tooltip />
+          <zm-table-column prop="personName" label="工单填报人" align="center" />
+          <zm-table-column label="创建日期" prop="createTime" align="center">
+            <template #default="{ row }">
+              {{ formatDate(row.createTime) }}
             </template>
           </zm-table-column>
-          <zm-table-column prop="elementDescription" label="危害因素描述" align="center" />
-          <zm-table-column prop="maybeResult" label="可导致的后果" align="center" />
-
-          <!-- 风险评价列保持不变 -->
-          <zm-table-column label="风险评价" align="center">
-            <zm-table-column prop="evalKn" label="可能性 (L)" width="80" align="center">
-              <template #default="{ row }">
-                {{ row.evalKn }}
-              </template>
-            </zm-table-column>
-            <zm-table-column prop="evalYz" label="严重性 (S)" width="80" align="center">
-              <template #default="{ row }">
-                {{ row.evalYz }}
-              </template>
-            </zm-table-column>
-            <zm-table-column prop="evalFxz" label="风险值 (R)" width="80" align="center">
-              <template #default="{ row }">
-                {{ row.evalFxz }}
-              </template>
-            </zm-table-column>
-            <zm-table-column prop="riskGrade" label="风险等级" width="100" align="center">
-              <template #default="scope">
-                <dict-tag :type="DICT_TYPE.DANGER_GRADE" :value="scope.row.riskGrade" />
-              </template>
-            </zm-table-column>
-          </zm-table-column>
 
-          <zm-table-column
-            prop="controlMethod"
-            label="控制措施"
-            show-overflow-tooltip
-            align="center" />
-          <zm-table-column prop="charge" label="责任人" align="center" />
           <zm-table-column label="操作" width="150" align="center" fixed="right" action>
             <template #default="{ row }">
-              <div class="flex gap-3 justify-center">
-                <el-link
-                  :underline="false"
-                  size="small"
-                  type="primary"
-                  @click="openForm('edit', row)">
-                  编辑
-                </el-link>
-                <el-link :underline="false" size="small" type="danger" @click="deleteRow(row)">
-                  删除
-                </el-link>
+              <div class="flex gap-1 justify-center">
+                <el-button text type="primary" @click="add(row)"> 填报 </el-button>
+                <el-button text size="small" type="success" @click="detail(row)"> 查看 </el-button>
               </div>
             </template>
           </zm-table-column>
@@ -148,126 +82,17 @@
       </ContentWrap>
     </el-col>
   </el-row>
-  <!-- 新增/编辑弹窗 -->
-  <!-- 新增/编辑弹窗 -->
-  <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%" @close="resetForm">
-    <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
-      <el-row :gutter="20">
-        <!-- 第一行 -->
-        <el-col :span="12">
-          <el-form-item label="区域/位置" prop="region">
-            <el-input v-model="formData.region" placeholder="请输入区域/位置" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="危害因素描述" prop="elementDescription">
-            <el-input v-model="formData.elementDescription" placeholder="请输入危害因素描述" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row :gutter="20">
-        <!-- 第二行 -->
-        <el-col :span="12">
-          <el-form-item label="可能导致的后果" prop="maybeResult">
-            <el-input
-              v-model="formData.maybeResult"
-              placeholder="请输入可能导致的后果"
-              type="textarea"
-              :rows="1" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="风险评价可能性" prop="evalKn">
-            <el-input-number
-              v-model="formData.evalKn"
-              controls-position="right"
-              style="width: 100%" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row :gutter="20">
-        <!-- 第三行 -->
-        <el-col :span="12">
-          <el-form-item label="风险评价严重性" prop="evalYz">
-            <el-input-number
-              v-model="formData.evalYz"
-              controls-position="right"
-              style="width: 100%" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="风险评价风险值" prop="evalFxz">
-            <el-input-number v-model="formData.evalFxz" disabled style="width: 100%" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row :gutter="20">
-        <!-- 第四行 -->
-        <el-col :span="12">
-          <el-form-item label="风险等级" prop="riskGrade">
-            <el-select
-              v-model="formData.riskGrade"
-              placeholder="请选择风险等级"
-              clearable
-              style="width: 100%">
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="责任人" prop="charge">
-            <el-input v-model="formData.charge" placeholder="请输入责任人" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <el-row :gutter="20">
-        <el-col :span="24">
-          <el-form-item label="备注" prop="remark">
-            <el-input
-              v-model="formData.remark"
-              type="textarea"
-              placeholder="请输入备注"
-              :rows="1" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-
-      <!-- 控制措施单独一行(占满) -->
-      <el-row :gutter="20">
-        <el-col :span="24">
-          <el-form-item label="控制措施" prop="controlMethod">
-            <el-input
-              v-model="formData.controlMethod"
-              type="textarea"
-              :rows="4"
-              placeholder="请输入控制措施" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
-
-    <template #footer>
-      <el-button @click="dialogVisible = false">取消</el-button>
-      <el-button type="primary" @click="submitForm">确定</el-button>
-    </template>
-  </el-dialog>
 </template>
 
 <script setup>
 import { ref, reactive, watch, onMounted, computed } from 'vue'
-import { IotDangerApi } from '@/api/pms/qhse/index'
+import { QhseMonthReportApi } from '@/api/pms/qhse'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import DeptTree from '@/views/system/user/HazardTree.vue'
+import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { useUserStore } from '@/store/modules/user'
+import { getUserProfile } from '@/api/system/user/profile'
 import { useRouter } from 'vue-router'
+import { formatDate } from '@/utils/formatTime'
 
 defineOptions({ name: 'QhseMonthlyReport' })
 
@@ -275,62 +100,29 @@ const userStore = useUserStore()
 const router = useRouter()
 // 查询参数
 const queryParams = reactive({
-  riskGrade: '',
+  createTime: null,
+  title: '',
   deptId: ''
 })
 
+let queryFormRef = ref(null)
+
 // 表格数据
 const tableData = ref([])
 const isLeftContentCollapsed = ref(false)
-// 弹窗控制
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增')
-const formData = reactive({
-  region: '', // 区域/位置
-  charge: '',
-  elementDescription: '', // 危害因素描述
-  maybeResult: '', // 可能导致的后果
-  evalKn: 1, // 可能性
-  evalYz: 1, // 严重性
-  evalFxz: 1, // 风险值(自动计算)
-  riskGrade: '一般风险', // 风险等级
-  controlMethod: '', // 控制措施
-  remark: '' // 备注
-})
 
-// 表单校验规则
-const rules = {
-  region: [{ required: true, message: '请输入区域/位置', trigger: 'blur' }],
-  charge: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
-  elementDescription: [{ required: true, message: '请输入危害因素描述', trigger: 'blur' }],
-  maybeResult: [{ required: true, message: '请输入可能导致的后果', trigger: 'blur' }],
-  evalKn: [{ required: true, message: '请输入风险评价可能性', trigger: 'change' }],
-  evalYz: [{ required: true, message: '请输入风险评价严重性', trigger: 'change' }],
-  riskGrade: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
-  controlMethod: [{ required: true, message: '请输入控制措施', trigger: 'blur' }]
-}
+const dialogTitle = ref('新增')
 
-watch(
-  () => [formData.evalKn, formData.evalYz],
-  ([kn, yz]) => {
-    if (kn && yz) {
-      formData.evalFxz = kn * yz
-    }
-  },
-  { immediate: true }
-)
 // 搜索
 const handleQuery = () => {
   pagination.pageNo = 1 // 搜索后回到第一页
   loadTableData()
-  getStatic()
 }
 
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   pagination.pageNo = 1
   loadTableData()
-  getStatic()
 }
 
 const downloadFile = (response) => {
@@ -340,7 +132,7 @@ const downloadFile = (response) => {
   })
 
   // 获取文件名
-  let fileName = '危险源.xlsx'
+  let fileName = '月报.xlsx'
   const disposition = response.headers ? response.headers['content-disposition'] : ''
   if (disposition) {
     const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
@@ -369,7 +161,7 @@ const handleExport = async () => {
   try {
     exportLoading.value = true
     // 调用导出接口
-    const response = await IotDangerApi.exportDanger(queryParams)
+    const response = await QhseMonthReportApi.exportQhseMonthReport(queryParams)
 
     // 下载文件
     downloadFile(response)
@@ -383,10 +175,9 @@ const handleExport = async () => {
 
 // 重置查询
 const resetQuery = () => {
-  queryParams.riskGrade = '' // 清空风险等级筛选
   pagination.pageNo = 1 // 重置为第一页
+  queryFormRef.value.resetFields()
   loadTableData()
-  getStatic()
 }
 
 // 删除确认
@@ -398,7 +189,7 @@ const deleteRow = async (row) => {
       type: 'warning'
     })
 
-    await IotDangerApi.deleteDanger(row.id)
+    await QhseMonthReportApi.deleteQhseMonthReport(row.id)
     ElMessage.success('删除成功')
     loadTableData() // 重新加载数据
   } catch (error) {
@@ -416,103 +207,40 @@ const handleSizeChange = (val) => {
   loadTableData()
 }
 
-// 预先计算合并信息
-const spanArr = ref([])
-const pos = ref(0)
-
-// 计算合并信息
-const getSpanArr = (data) => {
-  spanArr.value = []
-  pos.value = 0
-
-  data.forEach((item, index) => {
-    if (index === 0) {
-      spanArr.value.push(1)
-      pos.value = 0
-    } else {
-      if (data[index].region === data[index - 1].region) {
-        spanArr.value[pos.value] += 1
-        spanArr.value.push(0)
-      } else {
-        spanArr.value.push(1)
-        pos.value = index
-      }
-    }
-  })
-}
-
-// 行合并方法
-const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
-  if (columnIndex === 0) {
-    const _row = spanArr.value[rowIndex]
-    const _col = _row > 0 ? 1 : 0
-    return {
-      rowspan: _row,
-      colspan: _col
-    }
-  }
-  return {
-    rowspan: 1,
-    colspan: 1
-  }
-}
-
 // 当前页变化
 const handleCurrentChange = (val) => {
   pagination.pageNo = val
   loadTableData()
 }
 
-const add = () => {
+const add = (row) => {
+  if (row.dutyPerson !== userInfo.value.id) {
+    ElMessage.error('您没有权限进行此操作')
+    return
+  }
   router.push({
-    name: 'MonthlyReportAdd'
+    name: 'MonthlyReportAdd',
+    params: {
+      id: row.id
+    }
   })
 }
-// 打开表单
-const openForm = (type, row = null) => {
-  dialogTitle.value = type === 'create' ? '新增' : '编辑'
-  if (type === 'edit') {
-    Object.assign(formData, row)
-    // 计算风险值 R = L × S
-    formData.riskValue = formData.possibility * formData.severity
-  } else {
-    resetForm()
-  }
-  dialogVisible.value = true
-}
 
-// 重置表单
-const resetForm = () => {
-  Object.keys(formData).forEach((key) => {
-    formData[key] = ''
+const detail = (row) => {
+  if (row.dutyPerson !== userInfo.value.id) {
+    ElMessage.error('您没有权限进行此操作')
+    return
+  }
+  router.push({
+    name: 'MonthlyReportInfo',
+    params: {
+      id: row.id
+    }
   })
 }
 
 // 提交表单
 const formRef = ref(null)
-const submitForm = async () => {
-  await formRef.value.validate()
-  try {
-    const params = {
-      ...formData,
-      evalFxz: formData.evalFxz // 使用已计算的值
-    }
-
-    if (dialogTitle.value === '新增') {
-      await IotDangerApi.createDanger(params)
-      ElMessage.success('新增成功')
-    } else {
-      params.id = formData.id
-      await IotDangerApi.updateDanger(params)
-      ElMessage.success('修改成功')
-    }
-
-    loadTableData()
-    dialogVisible.value = false
-  } catch (error) {
-    ElMessage.error('提交失败')
-  }
-}
 
 // 加载数据
 const exportLoading = ref(false)
@@ -526,110 +254,29 @@ const loadTableData = async () => {
     const params = {
       pageNo: pagination.pageNo,
       pageSize: pagination.pageSize,
-      riskGrade: queryParams.riskGrade, // 添加搜索参数
+      createTime: queryParams.createTime,
+
+      title: queryParams.title,
       deptId: queryParams.deptId
     }
-    const res = await IotDangerApi.getDangerList(params)
+    const res = await QhseMonthReportApi.getQhseMonthReportPage(params)
     tableData.value = res.list || []
     total.value = res.total || 0
-
-    // // 按 region 排序(支持中文)
-    // tableData.value.sort((a, b) => {
-    //   return a.region.localeCompare(b.region, 'zh-CN')
-    // })
-
-    // 计算合并信息
-    getSpanArr(tableData.value)
   } catch (error) {
     console.error('加载失败:', error)
   }
 }
 
-let staticData = ref([])
-const totalRiskCount = computed(() =>
-  staticData.value.reduce((sum, item) => sum + (Number(item.count) || 0), 0)
-)
-
-const getStatsCardMeta = (classify) => {
-  const value = String(classify || '')
-  if (value.includes('重大') || value.includes('閲嶅ぇ')) {
-    return {
-      accent: '#ff5b61',
-      glow: 'radial-gradient(circle, rgba(255, 91, 97, 0.22) 0%, rgba(255, 91, 97, 0) 72%)',
-      icon: 'ep:warning-filled'
-    }
-  }
-  if (value.includes('较大') || value.includes('杈冨ぇ')) {
-    return {
-      accent: '#ff9827',
-      glow: 'radial-gradient(circle, rgba(255, 152, 39, 0.24) 0%, rgba(255, 152, 39, 0) 72%)',
-      icon: 'ep:opportunity'
-    }
-  }
-  if (value.includes('一般') || value.includes('涓€鑸')) {
-    return {
-      accent: '#3d7cff',
-      glow: 'radial-gradient(circle, rgba(61, 124, 255, 0.2) 0%, rgba(61, 124, 255, 0) 72%)',
-      icon: 'ep:info-filled'
-    }
-  }
-  if (value.includes('低') || value.includes('浣')) {
-    return {
-      accent: '#25b36a',
-      glow: 'radial-gradient(circle, rgba(37, 179, 106, 0.22) 0%, rgba(37, 179, 106, 0) 72%)',
-      icon: 'ep:success-filled'
-    }
-  }
-  return {
-    accent: '#5f7da8',
-    glow: 'radial-gradient(circle, rgba(95, 125, 168, 0.18) 0%, rgba(95, 125, 168, 0) 72%)',
-    icon: 'ep:data-analysis'
-  }
-}
-
-const statsDisplayCards = computed(() =>
-  staticData.value.map((item, index) => {
-    const meta = getStatsCardMeta(item.classify)
-    const count = Number(item.count) || 0
-    const rate = totalRiskCount.value ? ((count / totalRiskCount.value) * 100).toFixed(1) : '0.0'
-    return {
-      key: `${item.classify}-${index}`,
-      label: item.classify,
-      count,
-      note: `占比:${rate}%`,
-      ...meta
-    }
-  })
-)
-
-const getStatsCardStyle = (accent, glow) => ({
-  '--stats-accent': accent,
-  '--stats-glow': glow
-})
-
-const getStatsCardClass = (classify) => {
-  const value = String(classify || '')
-  if (value.includes('重大')) return 'stats-card--major'
-  if (value.includes('较大')) return 'stats-card--high'
-  if (value.includes('一般')) return 'stats-card--medium'
-  if (value.includes('低')) return 'stats-card--low'
-  return 'stats-card--default'
-}
-
-async function getStatic() {
-  if (queryParams.deptId) {
-    const res = await IotDangerApi.getDangerStatistics(queryParams.deptId)
-    staticData.value = res.classify
-  } else {
-    const res = await IotDangerApi.getDangerStatistics(userStore.user.deptId)
-    staticData.value = res.classify
-  }
-}
+let userInfo = ref({})
 
 // 页面挂载后加载数据
-onMounted(() => {
+onMounted(async () => {
   loadTableData()
-  getStatic()
+
+  const users = await getUserProfile()
+  userInfo.value = users
+
+  console.log('xxxxxxxxxxxxxxxxxx', userInfo.value)
 })
 </script>
 
@@ -654,183 +301,4 @@ onMounted(() => {
   background-color: #f5f7fa;
   border-bottom: 1px solid #ddd;
 }
-
-.sub-row {
-  padding: 12px 0;
-  text-align: left;
-}
-
-.risk-evaluation {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  align-items: center;
-}
-
-.risk-item {
-  font-size: 12px;
-  padding: 4px 8px;
-  border-radius: 4px;
-  background-color: #f5f7fa;
-  color: #333;
-}
-
-.risk-level {
-  font-weight: bold;
-  color: #333;
-  padding: 6px 12px;
-  border-radius: 4px;
-}
-
-.stats-cards {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
-  gap: 12px;
-  margin-bottom: 16px;
-}
-
-.stats-card {
-  position: relative;
-  overflow: hidden;
-  min-height: 132px;
-  padding: 18px 18px 16px;
-  border-radius: 22px;
-  background: radial-gradient(
-      circle at 18% 22%,
-      rgb(255 255 255 / 92%) 0%,
-      rgb(255 255 255 / 0%) 20%
-    ),
-    radial-gradient(circle at 88% 80%, rgb(255 215 158 / 22%) 0%, rgb(255 215 158 / 0%) 16%),
-    linear-gradient(135deg, rgb(239 245 255 / 96%) 0%, rgb(217 230 248 / 88%) 100%);
-  border: 1px solid rgb(255 255 255 / 62%);
-  box-shadow:
-    inset 0 1px 0 rgb(255 255 255 / 86%),
-    0 14px 30px rgb(116 146 191 / 12%);
-}
-
-.stats-card__decor {
-  position: absolute;
-  border-radius: 999px;
-  pointer-events: none;
-  filter: blur(8px);
-  opacity: 0.95;
-}
-
-.stats-card__decor--left {
-  width: 72px;
-  height: 72px;
-  left: -10px;
-  top: -8px;
-}
-
-.stats-card__decor--right {
-  width: 88px;
-  height: 88px;
-  right: -18px;
-  bottom: -24px;
-}
-
-.stats-card__header {
-  position: relative;
-  z-index: 1;
-  display: flex;
-  align-items: center;
-  gap: 14px;
-}
-
-.stats-card__icon-wrap {
-  width: 48px;
-  height: 48px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border-radius: 14px;
-  box-shadow:
-    inset 0 1px 0 rgb(255 255 255 / 85%),
-    0 10px 24px rgb(118 144 187 / 10%);
-}
-
-.stats-card__icon {
-  font-size: 24px;
-  /* line-height: 1; */
-}
-
-.stats-card__title {
-  font-size: 16px;
-  font-weight: 700;
-  color: #324b72;
-  letter-spacing: 0;
-}
-
-.stats-card__body {
-  position: relative;
-  z-index: 1;
-  display: flex;
-  align-items: flex-end;
-  gap: 8px;
-  margin-top: 14px;
-  padding-left: 62px;
-}
-
-.stats-card__count {
-  display: block;
-  font-size: 38px !important;
-  font-weight: 800;
-  line-height: 0.92;
-  letter-spacing: 1px;
-  font-style: italic;
-  text-shadow: 0 8px 18px rgb(68 110 183 / 10%);
-}
-
-.stats-card__note {
-  padding-bottom: 4px;
-  font-size: 16px;
-  font-weight: 700;
-  line-height: 1;
-}
-
-@media (max-width: 768px) {
-  .stats-cards {
-    grid-template-columns: 1fr;
-  }
-
-  .stats-card {
-    min-height: 160px;
-    padding: 24px 22px 22px;
-    border-radius: 22px;
-  }
-
-  .stats-card__header {
-    gap: 18px;
-  }
-
-  .stats-card__icon-wrap {
-    width: 58px;
-    height: 58px;
-    border-radius: 16px;
-  }
-
-  .stats-card__icon {
-    font-size: 28px;
-  }
-
-  .stats-card__title {
-    font-size: 17px;
-  }
-
-  .stats-card__body {
-    margin-top: 18px;
-    padding-left: 76px;
-    gap: 10px;
-  }
-
-  .stats-card__count {
-    font-size: 46px !important;
-  }
-
-  .stats-card__note {
-    font-size: 18px;
-    padding-bottom: 6px;
-  }
-}
 </style>

+ 86 - 19
src/views/pms/qhse/safety/index.vue

@@ -159,6 +159,8 @@
               <span v-else>-</span>
             </template>
           </zm-table-column>
+          <zm-table-column label="整改人" align="center" prop="correctPersonName" />
+          <zm-table-column label="整改要求" align="center" prop="correctStandard" />
 
           <zm-table-column label="整改情况" align="center" prop="rectifyDesc" />
 
@@ -178,7 +180,7 @@
 
           <zm-table-column prop="createTime" label="排查时间" align="center" min-width="150">
             <template #default="{ row }">
-              {{ formatDate(row.createTime) }}
+              {{ formatDate(row.checkTime) }}
             </template>
           </zm-table-column>
 
@@ -246,17 +248,6 @@
       :rules="formRules"
       label-width="120px"
       v-loading="formLoading">
-      <el-form-item label="所属队伍" prop="deptId">
-        <el-tree-select
-          clearable
-          v-model="formData.deptId"
-          :data="deptList2"
-          :props="defaultProps"
-          :check-strictly="false"
-          node-key="id"
-          filterable
-          placeholder="请选择所属队伍" />
-      </el-form-item>
       <el-form-item label="隐患排查类型" prop="classify">
         <el-tree-select
           style="width: 500px; overflow: auto"
@@ -269,7 +260,11 @@
       </el-form-item>
 
       <el-form-item label="隐患排查来源" prop="source">
-        <el-select v-model="formData.source" placeholder="请选择隐患排查来源" clearable>
+        <el-select
+          v-model="formData.source"
+          placeholder="请选择隐患排查来源"
+          clearable
+          @change="handleChange">
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.QHSE_HAZARD_SOURCE)"
             :key="dict.value"
@@ -278,6 +273,52 @@
         </el-select>
       </el-form-item>
 
+      <el-form-item label="所属队伍" prop="deptId">
+        <el-tree-select
+          clearable
+          v-model="formData.deptId"
+          :data="deptList2"
+          :props="defaultProps"
+          :check-strictly="
+            formData.source === 'xm' || formData.source === 'zfjf' || formData.source === 'gsjt'
+          "
+          node-key="id"
+          filterable
+          placeholder="请选择所属队伍" />
+      </el-form-item>
+
+      <el-form-item label="排查时间" prop="checkTime">
+        <el-date-picker
+          v-model="formData.checkTime"
+          type="datetime"
+          value-format="x"
+          placeholder="请选择排查时间"
+          style="width: 100%" />
+      </el-form-item>
+
+      <el-form-item label="整改要求" prop="correctStandard">
+        <el-input
+          type="textarea"
+          :rows="2"
+          v-model="formData.correctStandard"
+          placeholder="请输入整改要求" />
+      </el-form-item>
+
+      <el-form-item label="整改人" prop="correctPerson">
+        <el-select
+          v-model="formData.correctPerson"
+          filterable
+          clearable
+          placeholder="请选择整改人"
+          style="width: 100%">
+          <el-option
+            v-for="item in userList"
+            :key="item.id"
+            :label="item.nickname"
+            :value="item.id" />
+        </el-select>
+      </el-form-item>
+
       <el-form-item label="地点" prop="address">
         <el-input v-model="formData.address" placeholder="请输入地点" />
       </el-form-item>
@@ -385,8 +426,9 @@
 import { ref, reactive, watch, nextTick } from 'vue'
 import { IotHiddenApi, IotHiddenTypeApi } from '@/api/pms/qhse/index'
 import DeptTree from '@/views/system/user/DeptTree2.vue'
-
+import { getUserProfile } from '@/api/system/user/profile'
 import * as DeptApi from '@/api/system/dept'
+import { selectedDeptsEmployee } from '@/api/system/user'
 import { ElMessageBox, ElMessage } from 'element-plus'
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
@@ -456,7 +498,10 @@ const formData = ref({
   rectifyFile: '',
   status: '',
   classify: '',
-  source: ''
+  source: '',
+  checkTime: '',
+  correctStandard: '',
+  correctPerson: ''
 })
 
 // 表单验证规则
@@ -469,7 +514,10 @@ const formRules = {
   rectifyFile: [{ required: true, message: '整改附件不能为空', trigger: 'blur' }],
   hazardFile: [{ required: true, message: '照片不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
-  source: [{ required: true, message: '来源不能为空', trigger: 'blur' }]
+  source: [{ required: true, message: '来源不能为空', trigger: 'blur' }],
+  checkTime: [{ required: true, message: '检查时间不能为空', trigger: 'blur' }],
+  correctStandard: [{ required: true, message: '整改标准不能为空', trigger: 'blur' }],
+  correctPerson: [{ required: true, message: '整改人不能为空', trigger: 'blur' }]
 }
 
 /** 查询列表 */
@@ -518,6 +566,12 @@ const resetQuery = () => {
   handleQuery()
 }
 
+const handleChange = (val) => {
+  console.log('handleChange', val)
+  if (val === 'dw') {
+    formData.value.deptId = ''
+  }
+}
 // 显示新增对话框
 const handleAdd = async () => {
   isEdit.value = false
@@ -529,12 +583,14 @@ const handleAdd = async () => {
 }
 
 // 显示编辑对话框
-const handleEdit = (row) => {
+const handleEdit = async (row) => {
   isEdit.value = true
   dialogTitle.value = '修改'
+  await getTree()
 
   formData.value = {
     ...row,
+    classify: row.classify ? Number(row.classify) : row.classify,
     // 确保日期字段正确处理
     issueDate: row.issueDate ? ensureMillisecondTimestamp(row.issueDate) : null,
     validityPeriod: row.validityPeriod ? ensureMillisecondTimestamp(row.validityPeriod) : null
@@ -811,11 +867,22 @@ const resizePieChart = () => {
 
 let pieChartResizeObserver: ResizeObserver | null = null
 
+let userList = ref([])
 onMounted(async () => {
+  const users = await getUserProfile()
+
+  userList.value = await selectedDeptsEmployee({
+    deptIds: users.dept.id
+  })
   getList()
+
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
 
-  getStatic()
+  try {
+    getStatic()
+  } catch (error) {
+    console.error(error)
+  }
 
   window.addEventListener('resize', resizePieChart)
 
@@ -827,7 +894,7 @@ onMounted(async () => {
   }
 })
 
-onUnmounted(() => {
+onUnmounted(async () => {
   if (pieChart) {
     pieChart.dispose()
     pieChart = null

+ 89 - 33
src/views/pms/stat/maintain.vue

@@ -700,9 +700,11 @@ const initChart = async () => {
         barGap: 0,
         itemStyle: {
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: '#188df0' },
+            { offset: 0, color: '#83bff6' },
+            { offset: 0.5, color: '#188df0' },
             { offset: 1, color: '#188df0' }
-          ])
+          ]),
+          borderRadius: [5, 5, 0, 0]
         },
         emphasis: {
           focus: 'series'
@@ -714,10 +716,11 @@ const initChart = async () => {
         type: 'bar',
         itemStyle: {
           color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: '#d3a137' },
-
+            { offset: 0, color: '#ffb600' },
+            { offset: 0.5, color: '#d3a137' },
             { offset: 1, color: '#d3a137' }
-          ])
+          ]),
+          borderRadius: [5, 5, 0, 0]
         },
         emphasis: {
           focus: 'series'
@@ -755,14 +758,22 @@ onUnmounted(() => {
 // 统计卡片基础样式
 .stat-card {
   border-radius: 12px;
-  border: none;
+  border: 1px solid rgba(207, 220, 237, 0.9);
   transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
   overflow: hidden;
   position: relative;
+  background: linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.95),
+    inset 0 -10px 24px rgba(210, 225, 244, 0.26),
+    0 10px 24px rgba(32, 66, 120, 0.08);
 
   &:hover {
     transform: translateY(-4px);
-    box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.15);
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.98),
+      inset 0 -12px 26px rgba(204, 220, 243, 0.32),
+      0 16px 32px rgba(32, 66, 120, 0.12);
   }
 
   :deep(.el-card__body) {
@@ -772,25 +783,29 @@ onUnmounted(() => {
 
 // 渐变色背景
 .stat-card-gradient-1 {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background: radial-gradient(circle at top left, rgba(121, 164, 255, 0.16), transparent 42%),
+    linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
 }
 
 .stat-card-gradient-2 {
-  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+  background: radial-gradient(circle at top left, rgba(118, 186, 255, 0.14), transparent 42%),
+    linear-gradient(180deg, #ffffff 0%, #f3f8ff 100%);
 }
 
 .stat-card-gradient-3 {
-  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  background: radial-gradient(circle at top left, rgba(96, 154, 241, 0.14), transparent 42%),
+    linear-gradient(180deg, #ffffff 0%, #f5f9ff 100%);
 }
 
 .stat-card-gradient-4 {
-  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+  background: radial-gradient(circle at top left, rgba(137, 176, 242, 0.14), transparent 42%),
+    linear-gradient(180deg, #ffffff 0%, #f4f8fe 100%);
 }
 
 // 统计内容区域
 .stat-content {
   padding: 20px;
-  color: white;
+  color: #1f2a44;
 }
 
 .stat-header {
@@ -803,27 +818,31 @@ onUnmounted(() => {
 .stat-icon-wrapper {
   width: 48px;
   height: 48px;
-  background: rgba(255, 255, 255, 0.2);
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(232, 240, 252, 0.92) 100%);
+  border: 1px solid rgba(205, 219, 239, 0.9);
   border-radius: 12px;
   display: flex;
   align-items: center;
   justify-content: center;
-  backdrop-filter: blur(10px);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.95),
+    inset 0 -6px 12px rgba(205, 220, 242, 0.34),
+    0 6px 14px rgba(66, 104, 168, 0.08);
 }
 
 .stat-icon {
   font-size: 24px;
-  color: white;
+  color: #5d79b7;
 }
 
 .stat-title {
   font-size: 16px;
   font-weight: 600;
-  color: rgba(255, 255, 255, 0.95);
+  color: #000;
 }
 
 .stat-divider {
-  border-color: rgba(255, 255, 255, 0.2);
+  border-color: rgba(201, 214, 234, 0.85);
   margin: 12px 0;
 }
 
@@ -835,14 +854,14 @@ onUnmounted(() => {
 
 .stat-label {
   font-size: 13px;
-  color: rgba(255, 255, 255, 0.8);
+  color: #7182a1;
   margin-bottom: 4px;
 }
 
 .stat-value {
   font-size: 36px;
   font-weight: 700;
-  color: white;
+  color: #1f2f54;
   line-height: 1;
 }
 
@@ -854,30 +873,44 @@ onUnmounted(() => {
 
 .stat-sub-label {
   font-size: 13px;
-  color: rgba(255, 255, 255, 0.85);
+  color: #7182a1;
 }
 
 .stat-sub-value {
   font-size: 28px;
   font-weight: 700;
   line-height: 1;
+  color: #1f2f54;
 }
 
 // 图表卡片增强样式
 .chart-card-enhanced {
   border-radius: 12px;
-  border: 1px solid #e5e7eb;
+  border: 1px solid rgba(207, 220, 237, 0.9);
   transition: all 0.3s ease;
+  background: linear-gradient(180deg, #ffffff 0%, #f5f9ff 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.96),
+    inset 0 -12px 26px rgba(212, 226, 244, 0.2),
+    0 10px 24px rgba(32, 66, 120, 0.07);
 
   &:hover {
-    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
-    border-color: #d1d5db;
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.98),
+      inset 0 -14px 30px rgba(208, 223, 244, 0.24),
+      0 16px 30px rgba(32, 66, 120, 0.1);
+    border-color: rgba(193, 209, 231, 0.95);
   }
 
   :deep(.el-card__header) {
     padding: 16px 20px;
-    border-bottom: 1px solid #f3f4f6;
-    background: linear-gradient(to right, #fafafa, #ffffff);
+    border-bottom: 1px solid rgba(223, 232, 245, 0.95);
+    background: linear-gradient(
+      90deg,
+      rgba(244, 248, 255, 0.96) 0%,
+      rgba(255, 255, 255, 0.98) 100%
+    );
+    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.78);
   }
 
   :deep(.el-card__body) {
@@ -901,14 +934,16 @@ onUnmounted(() => {
   width: 8px;
   height: 8px;
   border-radius: 50%;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
+  background: linear-gradient(135deg, #84aef7 0%, #5e87d8 100%);
+  box-shadow:
+    0 0 0 4px rgba(122, 162, 235, 0.12),
+    0 2px 6px rgba(86, 124, 189, 0.24);
 }
 
 .chart-title {
   font-size: 16px;
   font-weight: 600;
-  color: #1f2937;
+  color: #233554;
   letter-spacing: 0.3px;
 }
 
@@ -922,10 +957,17 @@ onUnmounted(() => {
   align-items: center;
   padding: 12px;
   transition: all 0.2s ease;
+  border-radius: 12px;
 
   &:hover {
-    background: rgba(102, 126, 234, 0.03);
-    border-radius: 8px;
+    background: linear-gradient(
+      180deg,
+      rgba(244, 248, 255, 0.9) 0%,
+      rgba(237, 244, 255, 0.95) 100%
+    );
+    box-shadow:
+      inset 0 1px 0 rgba(255, 255, 255, 0.86),
+      inset 0 -8px 16px rgba(212, 225, 243, 0.22);
   }
 }
 
@@ -940,12 +982,21 @@ onUnmounted(() => {
   gap: 6px;
   margin-top: 8px;
   padding: 6px 12px;
-  background: #f9fafb;
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(242, 247, 255, 0.96) 100%);
+  border: 1px solid rgba(216, 228, 244, 0.95);
   border-radius: 20px;
   transition: all 0.2s ease;
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.9),
+    0 4px 10px rgba(58, 90, 145, 0.06);
 
   .chart-item:hover & {
-    background: #f3f4f6;
+    background: linear-gradient(
+      180deg,
+      rgba(255, 255, 255, 0.98) 0%,
+      rgba(238, 244, 255, 0.98) 100%
+    );
+    border-color: rgba(201, 217, 239, 0.98);
   }
 }
 
@@ -988,13 +1039,18 @@ onUnmounted(() => {
 
 .label-text {
   font-size: 13px;
-  color: #6b7280;
+  color: #64748f;
   font-weight: 500;
 }
 
 .bar-chart-container {
   height: 350px;
   width: 100%;
+  border-radius: 12px;
+  background: linear-gradient(180deg, rgba(252, 254, 255, 0.9) 0%, rgba(244, 249, 255, 0.92) 100%);
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.92),
+    inset 0 -10px 20px rgba(217, 230, 245, 0.16);
 }
 
 // 响应式优化

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

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff